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
16 changes: 11 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Architecture

## Runtime Shape

The Controller runs as a browser frontend backed by a local Rust server. Vite
serves the Svelte app from `src/`; the backend in `server/` exposes HTTP routes
under `/api/*` and broadcasts app events over `/ws`.

## Agent Compatibility: Claude Code & Codex

The Controller supports both Claude Code and Codex as coding agents. Compatibility is maintained through two mechanisms: a shared instruction file and skill synchronization.
Expand All @@ -13,7 +19,7 @@ Claude Code reads project instructions from `CLAUDE.md`. Rather than maintaining

The symlink is non-invasive: if a real `CLAUDE.md` already exists, it's left alone.

See: `src-tauri/src/commands.rs` (`ensure_claude_md_symlink`)
See: `server/src/commands.rs` (`ensure_claude_md_symlink`)

### Skill Synchronization on Bootstrap

Expand All @@ -24,7 +30,7 @@ Skills live in `skills/the-controller-*/` inside the repo. On app startup, `sync

The sync is idempotent, worktree-aware (resolves to the main repo via `git rev-parse --git-common-dir`), and cleans up stale symlinks whose targets no longer exist. Regular files are never overwritten — only symlinks are managed.

See: `src-tauri/src/skills.rs`
See: `server/src/skills.rs`

## Why We Vendorize Skills

Expand Down Expand Up @@ -90,7 +96,7 @@ This is the authoritative record. If a worktree directory exists but has no matc

Labels are auto-generated: `session-{N}-{6-char-uuid}`. The number increments based on the highest existing session number in the project. The UUID suffix ensures uniqueness across parallel creation.

See: `commands.rs` (`next_session_label`)
See: `server/src/commands.rs` (`next_session_label`)

### Worktree Creation

Expand All @@ -101,15 +107,15 @@ When a session is created:
3. The main repo's `.env` is symlinked into the worktree so secrets are shared
4. If the repo has no commits (unborn branch), the repo path is used directly — no worktree is created

See: `worktree.rs` (`create_worktree`)
See: `server/src/worktree.rs` (`create_worktree`)

### Worktree Cleanup

- **Close session** with `delete_worktree=true`: removes the directory, prunes the git worktree reference, and deletes the branch
- **Delete project**: iterates all sessions, closes PTYs, and removes each worktree
- **Failed spawn**: if PTY spawn fails after worktree creation, the worktree and branch are rolled back automatically

See: `worktree.rs` (`remove_worktree`), `commands.rs` (`cleanup_failed_session_spawn`)
See: `server/src/worktree.rs` (`remove_worktree`), `server/src/commands.rs` (`cleanup_failed_session_spawn`)

### Recovery

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

A shapeable, personal desktop system — starting with terminal multiplexing.
A shapeable, personal web control surface — starting with terminal multiplexing.

Built with Tauri v2 + Svelte 5 + Rust.
Built with Svelte 5 + a local Axum/Rust backend.

> grug have many claude terminal. alt-tab alt-tab alt-tab. where thing go. brain so smooth. so tired.
> then grug find controller. all terminal one place. grug not lose thing no more.
Expand All @@ -13,18 +13,20 @@ Built with Tauri v2 + Svelte 5 + Rust.

Install prerequisites:

- [Rust](https://rustup.rs/) + Tauri v2
- [Node.js](https://nodejs.org/) + npm
- [Rust](https://rustup.rs/)
- [Node.js](https://nodejs.org/) + pnpm
- tmux (`brew install tmux`)
- espeak-ng (`brew install espeak-ng`) — required for voice mode TTS

Then:

```bash
npm install
npm run tauri dev
pnpm install
./dev.sh
```

Open `http://localhost:1420`. `dev.sh` starts the Rust backend on port 3001
and the Vite frontend on port 1420.

### tmux Configuration

If you use Claude Code inside tmux to develop this project, add the following to `~/.tmux.conf` for a cleaner UI:
Expand Down Expand Up @@ -108,7 +110,8 @@ Not sure what a feature does or how something works? Just ask Claude. The defaul
Or browse the docs directly:

- [Keyboard Shortcuts & Modes](docs/keyboard-modes.md) — all hotkeys, workspace modes, and how to stage/preview changes
- [Domain Knowledge](docs/domain-knowledge.md) — hard-won lessons about Tauri, tmux, and session architecture
- [Domain Knowledge](docs/domain-knowledge.md) — hard-won lessons about the backend, tmux, and session architecture
- [Web Backend Parity Audit](docs/web-backend-parity-audit.md) — how the web frontend maps to the old desktop command surface
- [Demo Recording](docs/demo.md) — how to record demos of The Controller

## Caveats
Expand All @@ -117,6 +120,6 @@ The Controller is a strongly opinionated power tool — built for efficiency, si

This project is in early stages. Some features may be overhauled or removed entirely without concern for backwards compatibility. Things will stabilize eventually, but not in the near term.

**Maintain your own fork.** This is the single best way to use The Controller without being caught off guard by breaking changes. Keep your customizations on your own branch and periodically rebase onto the latest commits from `master`. We may provide a skill (`the-controller-maintain-fork`) to automate this — PRs welcome.
**Maintain your own fork.** This is the single best way to use The Controller without being caught off guard by breaking changes. Keep your customizations on your own branch and periodically rebase onto the latest commits from `main`. We may provide a skill (`the-controller-maintain-fork`) to automate this — PRs welcome.

Several things are still being refined, including the [contribution guide](CONTRIBUTING.md).
4 changes: 2 additions & 2 deletions docs/domain-knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Affected files:
- `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)
## Shell Environment Inheritance

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.
The server may start from a shell, launcher, or automation process that does not have the user's full login environment. `shell_env::inherit_shell_env()` resolves the user's shell env at startup and applies it to the process. It 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: `server/src/shell_env.rs`, `server/src/main.rs`, `server/src/tmux.rs`

Expand Down
43 changes: 12 additions & 31 deletions docs/keyboard-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ All keyboard input flows through `HotkeyManager.svelte`. Hotkey definitions live

## Workspace Modes

The Controller has six workspace modes, each with its own hotkeys. Press `Space` then a key to switch:
The Controller has four workspace modes, each with its own hotkeys. Press `Space` then a key to switch:

| Key | Mode |
|-----|------|
| d | Development — manage sessions, branches, projects |
| a | Agents — toggle auto-workers and maintainers |
| r | Architecture — generate project architecture docs |
| n | Notes — markdown notes organized by folder |
| i | Infrastructure — deploy and rollback projects |
| v | Voice — voice interaction mode |
| k | Kanban — organize GitHub issues |
| c | Chat — use daemon-backed chat sessions |

## Keyboard State Machine

Expand All @@ -36,8 +34,7 @@ The Controller has six workspace modes, each with its own hotkeys. Press `Space`
v
+-------------------+
| Workspace Mode |
| Picker (d/a/r/n/ |
| i/v) |
| Picker (d/a/k/c) |
+-------------------+
```

Expand Down Expand Up @@ -92,36 +89,20 @@ These work in all workspace modes when no terminal or editable element is focuse
| c | Clear maintainer reports |
| t | Toggle between Runs / Issues view |

## Ambient Mode — Architecture Keys
## Ambient Mode — Kanban Keys

| Key | Action |
|-----|--------|
| r | Generate / regenerate architecture for focused project |
| Space then k | Open the Kanban board for the focused project |
| Drag issue cards | Move issues between lifecycle columns |

## Ambient Mode — Notes Keys
## Ambient Mode — Chat Keys

| Key | Action |
|-----|--------|
| n | Create new note |
| d | Delete focused note or folder |
| r | Rename focused note or folder |
| y | Duplicate focused note |
| p | Cycle note preview mode (edit / preview / split) |
| o / i / a | Open note for editing (vim-style) |

## Ambient Mode — Infrastructure Keys

| Key | Action |
|-----|--------|
| d | Deploy focused project |
| r | Rollback last deployment |

## Ambient Mode — Voice Keys

| Key | Action |
|-----|--------|
| d | Toggle debug panel |
| t | Toggle transcript panel |
| Space then c | Open daemon-backed chat mode |
| j / k | Move through visible chat sessions |
| l / Enter | Select a chat session or expand/collapse a project |

## Agent Panel Keys

Expand All @@ -145,7 +126,7 @@ Press `v` again to unstage (kills the staged instance).
**What happens when you stage:**
1. Worktree is committed (prompts Claude to commit if dirty)
2. Branch is rebased onto main if behind
3. `npm install` runs in the worktree if needed
3. `pnpm install` runs in the worktree if needed
4. `./dev.sh <port>` launches a separate Controller instance
5. Main Controller title bar shows "staging: session-label"

Expand Down
77 changes: 77 additions & 0 deletions docs/web-backend-parity-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Web Backend Parity Audit

This audit tracks the removal of the Tauri desktop shell. The supported runtime
is now:

- `src/`: Svelte frontend served by Vite
- `server/`: local Axum backend exposing `/api/*` and `/ws`

The browser frontend keeps the desktop frontend's command surface by routing
former Tauri `invoke(...)` calls through HTTP. Events that used to travel
through Tauri now travel through the shared WebSocket broadcaster.

## Command Coverage

The old desktop frontend registered 59 Tauri commands. The web backend exposes
59 HTTP routes.

Two desktop-native commands do not have same-name HTTP routes:

- `capture_app_screenshot`: replaced by `src/lib/native.ts`, which captures the
browser DOM with `html2canvas`, then saves it through `/api/save_screenshot`.
- `copy_image_file_to_clipboard`: replaced by the browser drag/drop path in
`src/lib/Terminal.svelte`, which reads dropped image files and writes them
with `ClipboardItem`.

The backend also exposes two web-only routes:

- `save_screenshot`: persists browser-captured screenshots to a temporary PNG.
- `list_archived_projects`: supports archived project inventory reads.

## Event Coverage

The frontend listens through `src/lib/backend.ts`, which opens one shared
WebSocket connection to `/ws`. The Rust backend emits the same event names
through `server/src/emitter.rs`.

Covered event families:

- `pty-output:{session_id}`
- `session-status-changed:{session_id}`
- `session-status-hook:{session_id}`
- `session-cleanup:{session_id}`
- `staging-status`
- `merge-status`
- `secure-env-requested`
- `maintainer-status:{project_id}`
- `maintainer-error:{project_id}`
- `auto-worker-status:{project_id}`

## Regression Guard

`src/lib/web-backend-audit.test.ts` enforces three checks:

1. Every production frontend `command("...")` literal has a matching `/api/...`
route.
2. The old desktop command surface remains covered by HTTP routes or by the two
browser replacements.
3. Active docs do not point users at stale Tauri commands, `src-tauri/`, or
removed workspace modes.

Run it directly with:

```bash
pnpm test src/lib/web-backend-audit.test.ts
```

Run the whole web/backend validation set with:

```bash
pnpm check
pnpm test
pnpm build
cd server && cargo fmt --check
cd server && cargo clippy -- -D warnings
cd server && cargo test
pnpm exec playwright test --project=e2e e2e/specs/smoke.spec.ts
```
6 changes: 3 additions & 3 deletions e2e/eval.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ echo "Eval ports: Axum=$AXUM_PORT, Vite=$VITE_PORT"

# --- Ensure node_modules ---
if [[ ! -d "$WORKTREE/node_modules" ]]; then
echo "Installing npm dependencies in worktree..."
(cd "$WORKTREE" && npm install --silent)
echo "Installing pnpm dependencies in worktree..."
(cd "$WORKTREE" && pnpm install --silent)
fi

# --- Start Axum server ---
Expand All @@ -90,7 +90,7 @@ AXUM_PID=$!

# --- Start Vite dev server ---
echo "Starting Vite dev server on port $VITE_PORT..."
(cd "$WORKTREE" && DEV_PORT="$VITE_PORT" AXUM_PORT="$AXUM_PORT" npm run dev -- --strictPort) &
(cd "$WORKTREE" && DEV_PORT="$VITE_PORT" AXUM_PORT="$AXUM_PORT" pnpm dev -- --strictPort) &
VITE_PID=$!

# --- Wait for servers to be ready ---
Expand Down
4 changes: 2 additions & 2 deletions e2e/specs/chat-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ test("chat mode shows DaemonEmptyState when daemon is unreachable", async ({ pag
await switchToChatMode(page);

// Core assertion: the DaemonEmptyState component renders its heading when
// the daemon cannot be reached. In browser-mode e2e, `read_daemon_token`
// is not exposed by the Axum server, so bootstrap fails deterministically.
// the daemon cannot be reached. The Axum server exposes `read_daemon_token`,
// but this test does not start the daemon, so bootstrap fails deterministically.
await expect(page.getByRole("heading", { name: "Daemon not running" })).toBeVisible({
timeout: 5_000,
});
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default defineConfig({
timeout: 120_000,
},
{
command: "npm run dev",
command: "pnpm dev",
port: 1420,
reuseExistingServer: true,
},
Expand Down
Loading
Loading