codexmonitor-orchestration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCodexMonitor 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 per workspace, resume threads, track unread/running state
codex app-server - 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 supported)
.codex-worktrees - Git & GitHub: Diff stats, staged/unstaged files, commit log, branch management, GitHub Issues/PRs via
gh - Composer: Image attachments, autocomplete for skills (), prompts (
$), reviews (/prompts:), file paths (/review)@ - 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集成:差异统计、暂存/未暂存文件、提交日志、分支管理,通过工具集成GitHub Issues/PRs
gh - 编辑器:支持图片附件、技能自动补全()、提示词自动补全(
$)、代码评审(/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 (optional, for GitHub integrations)
gh
- Node.js + npm
- Rust工具链(稳定版)
- CMake(用于依赖本地库,如听写/Whisper)
- LLVM/Clang(仅Windows系统,用于bindgen)
- Codex CLI已安装并添加至
PATH - Git CLI(用于工作树操作)
- GitHub CLI (可选,用于GitHub集成)
gh
Install Dependencies
安装依赖
bash
npm installbash
npm installCheck Environment
检查环境
bash
npm run doctorbash
npm run doctorRun in Development
开发环境运行
bash
npm run tauri:devbash
npm run tauri:devBuild Production Bundle
构建生产包
bash
undefinedbash
undefinedmacOS/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 in app data directory.
workspaces.jsonVia UI: Sidebar → Add workspace → Select directory
Data structure ():
src-tauri/src/workspaces/mod.rsrust
#[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.rsrust
#[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.tstypescript
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.rsrust
#[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.tstypescript
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.rsrust
#[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.tstypescript
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;
}
}线程状态由目录下的Reducer分片管理。
src/features/threads/hooks/threadReducer/线程Reducer模式():
src/features/threads/hooks/threadReducer/index.tstypescript
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.tstypescript
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.rsrust
#[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.tstypescript
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.rsrust
#[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>/工作树代理会在目录下创建独立的Git工作树。
<app-data>/worktrees/<workspace-id>/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.rsrust
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.rsrust
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.rsrust
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.rsrust
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 CLI:
ghtypescript
// 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.rsrust
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)?)
}需要安装 CLI:
ghtypescript
// 列出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.rsrust
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:
- Settings → Server
- Set (shared secret)
Remote backend token - Click in
Start daemonMobile access daemon
Standalone Daemon CLI:
bash
undefined通过UI操作:
- 设置 → 服务器
- 设置(共享密钥)
Remote backend token - 在中点击
Mobile access daemonStart daemon
独立守护进程CLI:
bash
undefinedBuild 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
--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
--listen 0.0.0.0:4732
--token $REMOTE_TOKEN
--data-dir /path/to/app/data
undefinedDaemon RPC Architecture
守护进程RPC架构
Daemon entrypoint ():
src-tauri/src/bin/codex_monitor_daemon.rsrust
#[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.rsrust
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.rsrust
#[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.rsrust
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):
- Settings → Server → Tailscale helper
- Click → note suggested host (e.g.,
Detect Tailscale)your-mac.your-tailnet.ts.net:4732
iOS:
- Settings → Server
- Enter desktop Tailscale host and matching token
- Tap
Connect & test
Frontend client ():
src/services/remoteDaemon.tstypescript
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助手):
- 设置 → 服务器 → Tailscale助手
- 点击→ 记录推荐的主机地址(例如:
Detect Tailscale)your-mac.your-tailnet.ts.net:4732
iOS端:
- 设置 → 服务器
- 输入桌面端Tailscale主机地址和匹配的密钥
- 点击
Connect & test
前端客户端():
src/services/remoteDaemon.tstypescript
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: - — code review
/review - — file paths
@
Frontend autocomplete hook ():
src/features/composer/hooks/useAutocomplete.tstypescript
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.tstypescript
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 messages if agent is running
Queue - — interrupt current turn and steer
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 (or ).
$CODEX_HOME/prompts~/.codex/prompts提示词从(或)加载。
$CODEX_HOME/prompts~/.codex/promptsPrompt 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 in app data directory.
settings.jsontypescript
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.jsontypescript
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.tomltoml
[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.tomltoml
[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.shbash
./scripts/build_run_ios.shOptions: --simulator "<name>", --target aarch64-sim|x86_64-sim, --skip-build, --no-clean
可选参数: --simulator "<name>", --target aarch64-sim|x86_64-sim, --skip-build, --no-clean
undefinedundefinedBuild for USB Device
针对USB设备构建
bash
undefinedbash
undefinedList 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>
undefinedundefinedSigning Configuration
签名配置
Preferred: (gitignored):
src-tauri/tauri.ios.local.conf.jsonjson
{
"bundle": {
"iOS": {
"developmentTeam": "YOUR_TEAM_ID"
}
},
"identifier": "com.yourcompany.codexmonitor"
}推荐使用(已加入git忽略):
src-tauri/tauri.ios.local.conf.jsonjson
{
"bundle": {
"iOS": {
"developmentTeam": "YOUR_TEAM_ID"
}
},
"identifier": "com.yourcompany.codexmonitor"
}TestFlight Release
TestFlight发布
bash
undefinedbash
undefinedCopy .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_IDTerminal 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 doctorCommon 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 is in .
codexPATH在设置 → 通用 → Codex二进制文件路径中设置自定义路径,或确保已添加至。
codexPATHRemote Daemon Connection Fails
远程守护进程连接失败
- Confirm daemon is running:
./target/debug/codex_monitor_daemonctl status - Check token matches between desktop and client
- Verify host/port reachable (Tailscale: both devices online in same tailnet)
- Check firewall rules for listening port (default 4732)
- 确认守护进程正在运行:
./target/debug/codex_monitor_daemonctl status - 检查桌面端与客户端的密钥是否匹配
- 验证主机/端口是否可达(Tailscale:确保两台设备在同一网络中且在线)
- 检查监听端口的防火墙规则(默认端口4732)
Thread Resume Shows Stale Messages
线程恢复显示陈旧消息
Threads are restored from disk via . If messages are stale:
thread/resume- Ensure workspace matches thread working directory
cwd - Restart workspace server: Remove and re-add workspace
线程通过从磁盘恢复。若消息陈旧:
thread/resume- 确保工作区与线程工作目录一致
cwd - 重启工作区服务器:删除并重新添加工作区
Worktree Creation Fails
工作树创建失败
Ensure workspace is a git repository:
bash
cd /path/to/workspace
git statusLegacy worktrees under are supported but new ones use .
.codex-worktrees/<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:
- iPhone unlocked and trusted with Mac
- Developer Mode enabled on iPhone (Settings → Privacy & Security → Developer Mode)
- Open Xcode via and approve signing
./scripts/build_run_ios_device.sh --open-xcode
首次设备设置:
- iPhone解锁并信任Mac
- 在iPhone上启用开发者模式(设置 → 隐私与安全性 → 开发者模式)
- 通过打开Xcode并批准签名
./scripts/build_run_ios_device.sh --open-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 adapterssrc/
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
资源
- Homepage: https://www.codexmonitor.app
- Repository: https://github.com/Dimillian/CodexMonitor
- License: MIT
- Codebase Map: (task-oriented file lookup)
docs/codebase-map.md - iOS Tailscale Blueprint:
docs/mobile-ios-tailscale-blueprint.md
- 官网: https://www.codexmonitor.app
- 代码仓库: https://github.com/Dimillian/CodexMonitor
- 许可证: MIT
- 代码库映射: (面向任务的文件查找)
docs/codebase-map.md - iOS Tailscale指南:
docs/mobile-ios-tailscale-blueprint.md