Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ echo "[pre-commit] pnpm test"
pnpm test

echo "[pre-commit] cargo +stable fmt --check"
(cd src-tauri && cargo +stable fmt --check)
(cd server && cargo +stable fmt --check)

echo "[pre-commit] cargo +stable clippy -- -D warnings"
(cd src-tauri && cargo +stable clippy -- -D warnings)
(cd server && cargo +stable clippy -- -D warnings)

echo "[pre-commit] cargo +stable test"
# --test-threads=1 works around a shared-env-var flake between scaffold
# tests and worktree tests; see followup issue.
(cd src-tauri && cargo +stable test -- --test-threads=1)
(cd server && cargo +stable test -- --test-threads=1)
18 changes: 5 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libasound2-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- run: cd src-tauri && cargo fmt --check
- run: cd src-tauri && cargo clippy -- -D warnings
workspaces: server
- run: cd server && cargo fmt --check
- run: cd server && cargo clippy -- -D warnings

rust-test:
runs-on: ubuntu-latest
Expand All @@ -60,9 +56,5 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libasound2-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- run: cd src-tauri && cargo test
workspaces: server
- run: cd server && cargo test
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Bad examples:

## Quality bar

- PRs must pass all existing tests (`npx vitest run` and `cd src-tauri && cargo test`).
- PRs must pass all existing tests (`pnpm test` and `cd server && cargo test`).
- If your change is behavioral, include a test or explain why one isn't feasible.
- Keep changes focused — one concern per PR.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Reload with `tmux source-file ~/.tmux.conf`.
The app now includes a companion CLI for secure `.env` editing:

```bash
cd src-tauri
cd server
cargo run --bin controller-cli -- env set --project <project-name> --key <ENV_KEY>
```

Expand Down
12 changes: 6 additions & 6 deletions agents.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The Controller

Tauri v2 + Svelte 5 desktop app for orchestrating multiple Claude Code terminal sessions.
Axum + Svelte 5 web app for orchestrating multiple Claude Code terminal sessions.

## Task Structure (CRITICAL — NEVER SKIP)

Expand All @@ -14,13 +14,13 @@ Tauri v2 + Svelte 5 desktop app for orchestrating multiple Claude Code terminal

## Key Docs

- `docs/domain-knowledge.md` — Hard-won lessons (Tauri main thread blocking, CLAUDECODE env var). **Read this before modifying Tauri commands or spawning processes.**
- `docs/domain-knowledge.md` — Hard-won lessons (main-thread blocking, CLAUDECODE env var). **Read this before modifying request handlers or spawning processes.**
- `docs/plans/` — Design and implementation plans.

## Tech Stack

- **Frontend:** Svelte 5 (runes: `$state`, `$derived`, `$props`, `$effect`), xterm.js
- **Backend:** Rust (Tauri v2), portable-pty, git2
- **Frontend:** Svelte 5 (runes: `$state`, `$derived`, `$props`, `$effect`), xterm.js, vite
- **Backend:** Rust (axum), portable-pty, git2
- **Theme:** Catppuccin Mocha

## Branch Completion Rules
Expand All @@ -43,6 +43,6 @@ git branch -d <feature-branch>

## Dev Commands

- `pnpm tauri dev` — Run the app in development mode
- `cd src-tauri && cargo test` — Run Rust tests
- `./dev.sh` — Run the app (axum on 3001, vite on 1420); open http://localhost:1420
- `cd server && cargo test` — Run Rust tests
- `pnpm test` — Run frontend tests
21 changes: 18 additions & 3 deletions dev.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
#!/bin/bash
# Start the controller dev server on a custom port.
# Usage: ./dev.sh [port] (default: 1420)
# Start the controller web app (axum backend + vite frontend) on fixed ports.
# Usage: ./dev.sh [port] (default: vite=1420, axum=3001)
set -euo pipefail
PORT=${1:-1420}
DEV_PORT=$PORT npm run tauri dev -- --config "{\"build\":{\"devUrl\":\"http://localhost:$PORT\"}}"
AXUM_PORT=${AXUM_PORT:-3001}

cleanup() {
[[ -n "${AXUM_PID:-}" ]] && kill "$AXUM_PID" 2>/dev/null || true
[[ -n "${VITE_PID:-}" ]] && kill "$VITE_PID" 2>/dev/null || true
}
trap cleanup EXIT

(cd server && PORT="$AXUM_PORT" cargo run --bin the-controller-server) &
AXUM_PID=$!

DEV_PORT="$PORT" AXUM_PORT="$AXUM_PORT" pnpm dev -- --strictPort --port "$PORT" &
VITE_PID=$!

wait
56 changes: 27 additions & 29 deletions docs/domain-knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,31 @@

Lessons learned during development. Check this before making changes.

## Tauri v2: Synchronous Commands Block the Webview
## Axum handlers: Offload blocking work

**Problem:** Tauri commands defined as `pub fn` (synchronous) run on the **main thread**. If the command does anything slow (subprocess calls, file I/O, network), it freezes the entire webview — no rendering, no animations, no user interaction.
**Problem:** Synchronous code inside an axum handler blocks the tokio reactor thread. Subprocess calls, file I/O, and git operations can starve other in-flight requests and the WebSocket broadcaster.

**Symptom:** UI appears "stuck" even though JavaScript has already updated the state. The browser can't paint because the main thread is blocked by the Rust command.

**Fix:** Make slow commands `pub async fn` and use `tokio::task::spawn_blocking` for CPU/IO-bound work:
**Fix:** Use `tokio::task::spawn_blocking` for CPU- or IO-bound work:

```rust
// BAD: blocks main thread
#[tauri::command]
pub fn slow_command() -> Result<String, String> {
let result = expensive_operation(); // freezes webview
Ok(result)
// BAD: blocks the tokio thread
async fn slow_handler() -> Result<Json<Value>, (StatusCode, String)> {
let result = expensive_operation(); // starves the reactor
Ok(Json(result))
}

// GOOD: runs on background thread
#[tauri::command]
pub async fn slow_command() -> Result<String, String> {
let result = tokio::task::spawn_blocking(|| expensive_operation())
// GOOD: runs on the blocking thread pool
async fn slow_handler() -> Result<Json<Value>, (StatusCode, String)> {
let result = tokio::task::spawn_blocking(expensive_operation)
.await
.map_err(|e| format!("Task failed: {}", e))?;
Ok(result)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("task failed: {e}")))?;
Ok(Json(result))
}
```

**Rule of thumb:** Any command that shells out (`Command::new(...)`) or does significant I/O must be async + spawn_blocking.
**Rule of thumb:** Any handler that shells out (`Command::new(...)`) or does significant I/O must be async + spawn_blocking.

**Historical note:** This rule originated in the Tauri era (synchronous `#[tauri::command]` functions ran on the webview's main thread and froze the UI). The same hazard exists under axum — the blocking call just starves tokio instead.

## tmux Session Architecture

Expand All @@ -47,25 +45,25 @@ Key behaviors:
tmux binary: resolved at runtime by checking `/opt/homebrew/bin/tmux`, then `/usr/local/bin/tmux`, then `tmux` on `PATH`. Session naming: `ctrl-{uuid}`.

Affected files:
- `src-tauri/src/tmux.rs` — tmux binary interactions
- `src-tauri/src/pty_manager.rs` — `spawn_session`, `close_session`, `attach_tmux_session`
- `src-tauri/src/lib.rs` — exit handler that kills tmux sessions
- `server/src/tmux.rs` — tmux binary interactions
- `server/src/pty_manager.rs` — `spawn_session`, `close_session`, `attach_tmux_session`
- `server/src/main.rs` — axum entry point; schedulers + status_socket start here

## Shell Environment Inheritance (macOS GUI)

macOS GUI apps inherit a minimal launchd environment missing `.zshrc` vars. `shell_env::inherit_shell_env()` resolves the user's full shell env at startup and applies it to the process. Must run before any threads (`set_var` is not thread-safe). For tmux, all process env vars are passed via `-e` flags in `build_create_args` because tmux sessions inherit the **server's** environment, not the client's.

Affected files: `src-tauri/src/shell_env.rs`, `src-tauri/src/lib.rs`, `src-tauri/src/tmux.rs`
Affected files: `server/src/shell_env.rs`, `server/src/main.rs`, `server/src/tmux.rs`

## CLAUDECODE Environment Variable

Claude Code sets a `CLAUDECODE` env var to detect nested sessions. All `Command::new("claude")` calls and PTY `CommandBuilder` spawns must include `.env_remove("CLAUDECODE")` to prevent "cannot be launched inside another Claude Code session" errors.

Affected locations:
- `src-tauri/src/tmux.rs` — `create_session` (removes CLAUDECODE for tmux-backed sessions)
- `src-tauri/src/pty_manager.rs` — `spawn_command` (removes CLAUDECODE for direct commands)
- `src-tauri/src/config.rs` — `check_claude_cli_status`, `generate_names_via_cli`
- `src-tauri/src/maintainer.rs` — `run_health_check` (removes CLAUDECODE for health check subprocess)
- `server/src/tmux.rs` — `create_session` (removes CLAUDECODE for tmux-backed sessions)
- `server/src/pty_manager.rs` — `spawn_command` (removes CLAUDECODE for direct commands)
- `server/src/config.rs` — `check_claude_cli_status`, `generate_names_via_cli`
- `server/src/maintainer.rs` — `run_health_check` (removes CLAUDECODE for health check subprocess)

## Session Status Detection via Hooks

Expand All @@ -75,13 +73,13 @@ Session status (idle/working/exited) is detected using Claude Code hooks, not PT
1. On app startup, a Unix domain socket listener starts at `/tmp/the-controller.sock`.
2. When spawning Claude sessions, `--settings` is passed with hook config for `UserPromptSubmit` (→ working), `Stop` (→ idle), and `Notification[idle_prompt]` (→ idle).
3. Hook commands send `status:session-id` to the socket via `nc -U`.
4. The socket listener emits `session-status-hook:<session-id>` Tauri events.
4. The socket listener emits `session-status-hook:<session-id>` events over the WebSocket broadcaster.
5. PTY EOF (`session-status-changed`) still handles the "exited" state.

**Key files:**
- `src-tauri/src/status_socket.rs` — socket listener, message parsing, hook JSON generation
- `src-tauri/src/tmux.rs` — passes `--settings` and `THE_CONTROLLER_SESSION_ID` env var
- `src-tauri/src/pty_manager.rs` — same for direct (non-tmux) sessions
- `server/src/status_socket.rs` — socket listener, message parsing, hook JSON generation
- `server/src/tmux.rs` — passes `--settings` and `THE_CONTROLLER_SESSION_ID` env var
- `server/src/pty_manager.rs` — same for direct (non-tmux) sessions
- `src/lib/Sidebar.svelte` — listens for `session-status-hook` events

**Edge cases:**
Expand Down
Loading
Loading