- name
- codexmonitor-orchestration
- description
- Expert in CodexMonitor, a Tauri app for orchestrating multiple Codex agents across local workspaces with threads, git integration, and remote daemon support.
- triggers
- ["how do I use CodexMonitor","set up CodexMonitor workspaces","manage Codex agent threads","CodexMonitor remote backend","build CodexMonitor from source","configure CodexMonitor iOS","CodexMonitor git integration","run CodexMonitor daemon"]
# CodexMonitor Orchestration Skill
> Skill by [ara.so](https://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.
## What CodexMonitor Does
- **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)
## 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)
### Install Dependencies
```bash
npm install
```
### Check Environment
```bash
npm run doctor
```
### Run in Development
```bash
npm run tauri:dev
```
### Build Production Bundle
```bash
# macOS/Linux
npm run tauri:build
# Windows (opt-in, uses separate config)
npm run tauri:build:win
```
Artifacts: `src-tauri/target/release/bundle/` (platform-specific subfolders)
## 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>,
}
```
### 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())
}
```
## Thread Management
### Thread Reducer Architecture
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;
}
}
```
### 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())
}
```
### 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,
},
});
}
```
### 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 });
```
## Worktree Agents
Worktree agents create isolated git worktrees under `<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.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',
});
```
### Removing a Worktree
```typescript
await invoke('worktree_remove', {
workspaceId: 'workspace-id',
path: '/path/to/worktree',
});
```
## Git Integration
### Git Diff Stats
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 })
}
```
### 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',
});
```
### GitHub Integration
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', {
GitHub에서 보기