Compare commits

..

4 Commits

Author SHA1 Message Date
e482452ffd Expand MCP documentation with mode explanations and concrete examples
- Rewrite HOW-TO-USE.md MCP section with a mode matrix (stdio/http x
  manual/docker), four worked examples (filesystem, GitHub, custom HTTP,
  database), and detailed explanations of networking, auto-pull, and
  config injection
- Update README.md MCP architecture section with a mode table and
  key behaviors including auto-pull and Docker DNS details

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:19:03 -07:00
8c710fc7bf Auto-pull MCP Docker images and add mode hints to MCP UI
All checks were successful
Build App / build-macos (push) Successful in 2m25s
Build App / build-windows (push) Successful in 3m32s
Build App / build-linux (push) Successful in 5m34s
Build App / sync-to-github (push) Successful in 11s
- Automatically pull missing Docker images for MCP servers before
  starting containers, with progress streamed to the container
  progress modal
- Add contextual mode descriptions to MCP server cards explaining
  where commands run (project container vs separate MCP container)
- Clarify that HTTP+Docker URLs are auto-generated using the
  container hostname on the project network, not localhost

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:51:47 -07:00
b7585420ef Reconcile project statuses against Docker on startup, update docs and CI
All checks were successful
Build App / build-macos (push) Successful in 2m40s
Build App / build-windows (push) Successful in 4m12s
Build App / build-linux (push) Successful in 5m4s
Build Container / build-container (push) Successful in 2m41s
Build App / sync-to-github (push) Successful in 10s
- Add reconcile_project_statuses command that checks actual Docker container
  state on startup, preserving Running status for containers that are genuinely
  still running and resetting stale statuses to Stopped
- Add is_container_running helper using Docker inspect API
- Frontend calls reconciliation after Docker is confirmed available
- Update TECHNICAL.md project structure, auth modes, and file listings to
  match current codebase
- Update README.md and HOW-TO-USE.md with MCP servers, Mission Control,
  file manager, bash shells, clipboard/audio shims, and progress modal docs
- Add workflow file self-triggers to CI path filters for build-app.yml
  and build.yml
- Install Mission Control skills to ~/.claude/skills/ in entrypoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 08:29:06 -07:00
bf8ef3dba1 Fix file manager listing current folder as entry within itself
All checks were successful
Build App / build-macos (push) Successful in 2m36s
Build App / build-windows (push) Successful in 3m22s
Build App / build-linux (push) Successful in 4m39s
Build App / sync-to-github (push) Successful in 10s
The find command included the starting directory in results (e.g., listing
"workspace" inside /workspace). Replace `-not -name "."` with `-mindepth 1`
which correctly excludes the starting path from output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:38:48 -08:00
14 changed files with 439 additions and 90 deletions

View File

@@ -10,6 +10,7 @@ on:
branches: [main]
paths:
- "app/**"
- ".gitea/workflows/build-app.yml"
workflow_dispatch:
env:

View File

@@ -5,10 +5,12 @@ on:
branches: [main]
paths:
- "container/**"
- ".gitea/workflows/build.yml"
pull_request:
branches: [main]
paths:
- "container/**"
- ".gitea/workflows/build.yml"
env:
REGISTRY: repo.anhonesthost.net

View File

@@ -65,11 +65,11 @@ Switch to the **Projects** tab in the sidebar and click the **+** button.
### 3. Start the Container
Select your project in the sidebar and click **Start**. The status dot changes from gray (stopped) to orange (starting) to green (running).
Select your project in the sidebar and click **Start**. A progress modal appears showing real-time status as the container starts. The status dot changes from gray (stopped) to orange (starting) to green (running). The modal auto-closes on success.
### 4. Open a Terminal
Click the **Terminal** button (highlighted in accent color) to open an interactive terminal session. A new tab appears in the top bar and an xterm.js terminal loads in the main area.
Click the **Terminal** button to open an interactive terminal session. A new tab appears in the top bar and an xterm.js terminal loads in the main area.
Claude Code launches automatically with `--dangerously-skip-permissions` inside the sandboxed container.
@@ -99,16 +99,16 @@ Claude Code launches automatically with `--dangerously-skip-permissions` inside
│ Sidebar │ │
│ │ Terminal View │
│ Projects │ (xterm.js) │
│ MCP │ │
│ Settings │ │
│ │ │
├────────────┴────────────────────────────────────────┤
│ StatusBar X projects · X running · X terminals │
└─────────────────────────────────────────────────────┘
```
- **TopBar** — Terminal tabs for switching between sessions. Status dots on the right show Docker connection (green = connected) and image availability (green = ready).
- **Sidebar** — Toggle between the **Projects** list and **Settings** panel.
- **Terminal View** — Interactive terminal powered by xterm.js with WebGL rendering.
- **TopBar** — Terminal tabs for switching between sessions. Bash shell tabs show a "(bash)" suffix. Status dots on the right show Docker connection (green = connected) and image availability (green = ready).
- **Sidebar** — Toggle between the **Projects** list, **MCP** server configuration, and **Settings** panel.
- **Terminal View** — Interactive terminal powered by xterm.js with WebGL rendering. Includes a **Jump to Current** button that appears when you scroll up, so you can quickly return to the latest output.
- **StatusBar** — Counts of total projects, running containers, and open terminal sessions.
---
@@ -134,11 +134,17 @@ Select a project in the sidebar to see its action buttons:
|--------|---------------|--------------|
| **Start** | Stopped | Creates (if needed) and starts the container |
| **Stop** | Running | Stops the container but preserves its state |
| **Terminal** | Running | Opens a new terminal session in this container |
| **Terminal** | Running | Opens a new Claude Code terminal session |
| **Shell** | Running | Opens a bash login shell in the container (no Claude Code) |
| **Files** | Running | Opens the file manager to browse, download, and upload files |
| **Reset** | Stopped | Destroys and recreates the container from scratch |
| **Config** | Always | Toggles the configuration panel |
| **Remove** | Stopped | Deletes the project and its container (with confirmation) |
### Renaming a Project
Double-click the project name in the sidebar to rename it inline. Press **Enter** to confirm or **Escape** to cancel.
### Container Lifecycle
Containers use a **stop/start** model. When you stop a container, everything inside it is preserved — installed packages, modified files, downloaded tools. Starting it again resumes where you left off.
@@ -147,6 +153,10 @@ Containers use a **stop/start** model. When you stop a container, everything ins
Only **Remove** deletes everything, including the config volume and any stored credentials.
### Container Progress Feedback
When starting, stopping, or resetting a container, a progress modal shows real-time status messages (e.g., "Setting up MCP network...", "Starting MCP containers...", "Creating container..."). If an error occurs, the modal displays the error with a **Close** button. A **Force Stop** option is available if the operation stalls. The modal auto-closes on success.
---
## Project Configuration
@@ -177,6 +187,19 @@ When enabled, the host Docker socket is mounted into the container so Claude Cod
> Toggling this requires stopping and restarting the container to take effect.
### Mission Control
Toggle **Mission Control** to integrate [Flight Control](https://github.com/msieurthenardier/mission-control) — an AI-first development methodology — into the project. When enabled:
- The Flight Control repository is automatically cloned into the container
- Flight Control skills are installed to Claude Code's skill directory (`~/.claude/skills/`)
- Project instructions are appended with Flight Control workflow guidance
- The repository is symlinked at `/workspace/mission-control`
Available skills include `/mission`, `/flight`, `/leg`, `/agentic-workflow`, `/flight-debrief`, `/mission-debrief`, `/daily-briefing`, and `/init-project`.
> This setting can only be changed when the container is stopped. Toggling it triggers a container recreation on the next start.
### Environment Variables
Click **Edit** to open the environment variables modal. Add key-value pairs that will be injected into the container. Per-project variables override global variables with the same key.
@@ -188,8 +211,8 @@ Click **Edit** to open the environment variables modal. Add key-value pairs that
Click **Edit** to map host ports to container ports. This is useful when Claude Code starts a web server or other service inside the container and you want to access it from your host browser.
Each mapping specifies:
- **Host Port** — The port on your machine (165535)
- **Container Port** — The port inside the container (165535)
- **Host Port** — The port on your machine (1-65535)
- **Container Port** — The port inside the container (1-65535)
- **Protocol** — TCP (default) or UDP
### Claude Instructions
@@ -198,6 +221,128 @@ Click **Edit** to write per-project instructions for Claude Code. These are writ
---
## MCP Servers (Beta)
Triple-C supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, which extend Claude Code with access to external tools and data sources. MCP servers are configured in a **global library** and **enabled per-project**.
### How It Works
There are two dimensions to MCP server configuration:
| | **Manual** (no Docker image) | **Docker** (Docker image specified) |
|---|---|---|
| **Stdio** | Command runs inside the project container | Command runs in a separate MCP container via `docker exec` |
| **HTTP** | Connects to a URL you provide | Runs in a separate container, reached by hostname on a shared Docker network |
**Docker images are pulled automatically** if not already present when the project starts.
### Accessing MCP Configuration
Click the **MCP** tab in the sidebar to open the MCP server library. This is where you define all available MCP servers.
### Adding an MCP Server
1. Type a name in the input field and click **Add**.
2. Expand the server card and configure it.
The key decision is whether to set a **Docker Image**:
- **With Docker image** — The MCP server runs in its own isolated container. Best for servers that need specific dependencies or system-level packages.
- **Without Docker image** (manual) — The command runs directly inside your project container. Best for lightweight npx-based servers that just need Node.js.
Then choose the **Transport Type**:
- **Stdio** — The MCP server communicates over stdin/stdout. This is the most common type.
- **HTTP** — The MCP server exposes an HTTP endpoint (streamable HTTP transport).
### Configuration Examples
#### Example 1: Filesystem Server (Stdio, Manual)
A simple npx-based server that runs inside the project container. No Docker image needed since Node.js is already installed.
| Field | Value |
|-------|-------|
| **Docker Image** | *(empty)* |
| **Transport** | Stdio |
| **Command** | `npx` |
| **Arguments** | `-y @modelcontextprotocol/server-filesystem /workspace` |
This gives Claude Code access to browse and read files via MCP. The command runs directly inside the project container using the pre-installed Node.js.
#### Example 2: GitHub Server (Stdio, Manual)
Another npx-based server, with an environment variable for authentication.
| Field | Value |
|-------|-------|
| **Docker Image** | *(empty)* |
| **Transport** | Stdio |
| **Command** | `npx` |
| **Arguments** | `-y @modelcontextprotocol/server-github` |
| **Environment Variables** | `GITHUB_PERSONAL_ACCESS_TOKEN` = `ghp_your_token` |
#### Example 3: Custom MCP Server (HTTP, Docker)
An MCP server packaged as a Docker image that exposes an HTTP endpoint.
| Field | Value |
|-------|-------|
| **Docker Image** | `myregistry/my-mcp-server:latest` |
| **Transport** | HTTP |
| **Container Port** | `8080` |
| **Environment Variables** | `API_KEY` = `your_key` |
Triple-C will:
1. Pull the image automatically if not present
2. Start the container on the project's bridge network
3. Configure Claude Code to reach it at `http://triple-c-mcp-{id}:8080/mcp`
The hostname is the MCP container's name on the Docker network — **not** `localhost`.
#### Example 4: Database Server (Stdio, Docker)
An MCP server that needs its own runtime environment, communicating over stdio.
| Field | Value |
|-------|-------|
| **Docker Image** | `mcp/postgres-server:latest` |
| **Transport** | Stdio |
| **Command** | `node` |
| **Arguments** | `dist/index.js` |
| **Environment Variables** | `DATABASE_URL` = `postgresql://user:pass@host:5432/db` |
Triple-C will:
1. Pull the image and start it on the project network
2. Configure Claude Code to communicate via `docker exec -i triple-c-mcp-{id} node dist/index.js`
3. Automatically enable Docker socket access on the project container (required for `docker exec`)
### Enabling MCP Servers Per-Project
In a project's configuration panel (click **Config**), the **MCP Servers** section shows checkboxes for all globally defined servers. Toggle each server on or off for that project. Changes take effect on the next container start.
### How Docker-Based MCP Works
When a project with Docker-based MCP servers starts:
1. Missing Docker images are **automatically pulled** (progress shown in the progress modal)
2. A dedicated **bridge network** is created for the project (`triple-c-net-{projectId}`)
3. Each enabled Docker MCP server gets its own container on that network
4. The main project container is connected to the same network
5. MCP server configuration is written to `~/.claude.json` inside the container
**Networking**: Docker-based MCP containers are reached by their container name as a hostname (e.g., `triple-c-mcp-{serverId}`), not by `localhost`. Docker DNS resolves these names automatically on the shared bridge network.
**Stdio + Docker**: The project container uses `docker exec` to communicate with the MCP container over stdin/stdout. This automatically enables Docker socket access on the project container.
**HTTP + Docker**: The project container connects to the MCP container's HTTP endpoint using the container hostname and port (e.g., `http://triple-c-mcp-{serverId}:3000/mcp`).
**Manual (no Docker image)**: Stdio commands run directly inside the project container. HTTP URLs connect to wherever you point them (could be an external service or something running on the host).
### Configuration Change Detection
MCP server configuration is tracked via SHA-256 fingerprints stored as Docker labels. If you add, remove, or modify MCP servers for a project, the container is automatically recreated on the next start to apply the new configuration. The container filesystem is snapshotted first, so installed packages are preserved.
---
## AWS Bedrock Configuration
To use Claude via AWS Bedrock instead of Anthropic's API, switch the auth mode to **Bedrock** on the project card.
@@ -264,7 +409,11 @@ When an update is available, a pulsing **Update** button appears in the top bar.
### Multiple Sessions
You can open multiple terminal sessions (even for the same project). Each session gets its own tab in the top bar. Click a tab to switch, or click the **x** on a tab to close it.
You can open multiple terminal sessions (even for the same project). Each session gets its own tab in the top bar. Click a tab to switch, or click the **x** on a tab to close it. Tabs show the project name, with a "(bash)" suffix for shell sessions.
### Bash Shell Sessions
In addition to Claude Code terminals, you can open a plain **bash login shell** in any running container by clicking the **Shell** button. This is useful for manual inspection, package installation, debugging, or running commands that don't need Claude Code.
### URL Detection
@@ -272,9 +421,28 @@ When Claude Code prints a long URL (e.g., during `claude login`), Triple-C detec
Shorter URLs in terminal output are also clickable directly.
### Clipboard Support (OSC 52)
Programs inside the container can copy text to your host clipboard. When a container program uses `xclip`, `xsel`, or `pbcopy`, the text is transparently forwarded to your host clipboard via OSC 52 escape sequences. No additional configuration is required — this works out of the box.
### Image Paste
You can paste images from your clipboard into the terminal (Ctrl+V / Cmd+V). The image is uploaded to the container and the file path is injected into the terminal input so Claude Code can reference it.
You can paste images from your clipboard into the terminal (Ctrl+V / Cmd+V). The image is uploaded to the container as `/tmp/clipboard_<timestamp>.png` and the file path is injected into the terminal input so Claude Code can reference it. A toast notification confirms the upload.
### Jump to Current
When you scroll up in the terminal to review previous output, a **Jump to Current** button appears in the bottom-right corner. Click it to scroll back to the latest output.
### File Manager
Click the **Files** button on a running project to open the file manager modal. You can:
- **Browse** the container filesystem starting from `/workspace`, with breadcrumb navigation
- **Download** any file to your host machine via the download button on each file entry
- **Upload** files from your host into the current container directory
- **Refresh** the directory listing at any time
The file manager shows file names, sizes, and modification dates.
### Terminal Rendering
@@ -356,6 +524,8 @@ The sandbox container (Ubuntu 24.04) comes pre-installed with:
| build-essential | — | C/C++ compiler toolchain |
| openssh-client | — | SSH for git and remote access |
The container also includes **clipboard shims** (`xclip`, `xsel`, `pbcopy`) that forward copy operations to the host via OSC 52, and an **audio shim** (`rec`, `arecord`) for future voice mode support.
You can install additional tools at runtime with `sudo apt install`, `pip install`, `npm install -g`, etc. Installed packages persist across container stops (but not across resets).
---
@@ -378,7 +548,7 @@ You can install additional tools at runtime with `sudo apt install`, `pip instal
- Check that the Docker image is "Ready" in Settings.
- Verify that the mounted folder paths exist on your host.
- Look at the error message displayed in red below the project card.
- Look at the error message displayed in the progress modal.
### OAuth Login URL Not Opening
@@ -394,4 +564,10 @@ You can install additional tools at runtime with `sudo apt install`, `pip instal
### Settings Won't Save
- Most project settings can only be changed when the container is **stopped**. Stop the container first, make your changes, then start it again.
- Some changes (like toggling Docker access or changing mounted folders) trigger an automatic container recreation on the next start.
- Some changes (like toggling Docker access, Mission Control, or changing mounted folders) trigger an automatic container recreation on the next start.
### MCP Containers Not Starting
- Ensure the Docker image for the MCP server exists (pull it first if needed).
- Check that Docker socket access is available (stdio + Docker MCP servers auto-enable this).
- Try resetting the project container to force a clean recreation.

View File

@@ -27,10 +27,10 @@ Triple-C is a cross-platform desktop application that sandboxes Claude Code insi
### Container Lifecycle
1. **Create**: New container created with bind mounts, env vars, and labels
2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group
3. **Terminal**: `docker exec` launches Claude Code with a PTY
4. **Stop**: Container halted (filesystem persists in named volume)
5. **Restart**: Existing container restarted; recreated if settings changed (e.g., Docker access toggled)
2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group, sets up MCP servers
3. **Terminal**: `docker exec` launches Claude Code (or bash shell) with a PTY
4. **Stop**: Container halted (filesystem persists in named volume); MCP containers stopped
5. **Restart**: Existing container restarted; recreated if settings changed (detected via SHA-256 fingerprint)
6. **Reset**: Container removed and recreated from scratch (named volume preserved)
### Mounts
@@ -41,14 +41,14 @@ Triple-C is a cross-platform desktop application that sandboxes Claude Code insi
| `/home/claude/.claude` | `triple-c-claude-config-{projectId}` | Named Volume | Persists across container recreation |
| `/tmp/.host-ssh` | SSH key directory | Bind | Read-only; entrypoint copies to `~/.ssh` |
| `/home/claude/.aws` | AWS config directory | Bind | Read-only; for Bedrock auth |
| `/var/run/docker.sock` | Host Docker socket | Bind | Only if "Allow container spawning" is ON |
| `/var/run/docker.sock` | Host Docker socket | Bind | If "Allow container spawning" is ON, or auto-enabled by stdio+Docker MCP servers |
### Authentication Modes
Each project can independently use one of:
- **Anthropic** (OAuth): User runs `claude login` inside the terminal on first use. Token persisted in the config volume across restarts and resets.
- **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token).
- **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token). SSO sessions are validated before launching Claude for Profile auth.
### Container Spawning (Sibling Containers)
@@ -56,6 +56,31 @@ When "Allow container spawning" is enabled per-project, the host Docker socket i
If the Docker access setting is toggled after a container already exists, the container is automatically recreated on next start to apply the mount change. The named config volume (keyed by project ID) is preserved across recreation.
### MCP Server Architecture
Triple-C supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers as a Beta feature. MCP servers extend Claude Code with external tools and data sources.
**Modes**: Each MCP server operates in one of four modes based on transport type and whether a Docker image is specified:
| Mode | Where It Runs | How It Communicates |
|------|--------------|---------------------|
| Stdio + Manual | Inside the project container | Direct stdin/stdout (e.g., `npx -y @mcp/server`) |
| Stdio + Docker | Separate MCP container | `docker exec -i <mcp-container> <command>` from the project container |
| HTTP + Manual | External / user-provided | Connects to the URL you specify |
| HTTP + Docker | Separate MCP container | `http://<mcp-container>:<port>/mcp` via Docker DNS on a shared bridge network |
**Key behaviors**:
- **Global library**: MCP servers are defined globally in the MCP sidebar tab and stored in `mcp_servers.json`
- **Per-project toggles**: Each project enables/disables individual servers via checkboxes
- **Auto-pull**: Docker images for MCP servers are pulled automatically if not present when the project starts
- **Docker networking**: Docker-based MCP containers run on a per-project bridge network (`triple-c-net-{projectId}`), reachable by container name — not localhost
- **Auto-detection**: Config changes are detected via SHA-256 fingerprints and trigger automatic container recreation
- **Config injection**: MCP server configuration is written to `~/.claude.json` inside the container via the `MCP_SERVERS_JSON` environment variable, merged by the entrypoint using `jq`
### Mission Control Integration
Optional per-project integration with [Flight Control](https://github.com/msieurthenardier/mission-control) — an AI-first development methodology. When enabled, the repo is cloned into the container, skills are installed, and workflow instructions are injected into CLAUDE.md.
### Docker Socket Path
The socket path is OS-aware:
@@ -75,17 +100,32 @@ Users can override this in Settings via the global `docker_socket_path` option.
| `app/src/components/layout/StatusBar.tsx` | Running project/terminal counts |
| `app/src/components/projects/ProjectCard.tsx` | Project config, auth mode, action buttons |
| `app/src/components/projects/ProjectList.tsx` | Project list in sidebar |
| `app/src/components/settings/SettingsPanel.tsx` | API key, Docker, AWS settings |
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection |
| `app/src/components/terminal/TerminalTabs.tsx` | Tab bar for multiple terminal sessions |
| `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, inspection |
| `app/src-tauri/src/docker/exec.rs` | PTY exec sessions for terminal interaction |
| `app/src/components/projects/FileManagerModal.tsx` | File browser modal (browse, download, upload) |
| `app/src/components/projects/ContainerProgressModal.tsx` | Real-time container operation progress |
| `app/src/components/mcp/McpPanel.tsx` | MCP server library (global configuration) |
| `app/src/components/mcp/McpServerCard.tsx` | Individual MCP server configuration card |
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, and global settings |
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste |
| `app/src/components/terminal/TerminalTabs.tsx` | Tab bar for multiple terminal sessions (claude + bash) |
| `app/src/hooks/useTerminal.ts` | Terminal session management (claude and bash modes) |
| `app/src/hooks/useFileManager.ts` | File manager operations (list, download, upload) |
| `app/src/hooks/useMcpServers.ts` | MCP server CRUD operations |
| `app/src/hooks/useVoice.ts` | Voice mode audio capture (currently hidden) |
| `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, MCP injection, fingerprinting |
| `app/src-tauri/src/docker/exec.rs` | PTY exec sessions, file upload/download via tar |
| `app/src-tauri/src/docker/image.rs` | Image building/pulling |
| `app/src-tauri/src/docker/network.rs` | Per-project bridge networks for MCP containers |
| `app/src-tauri/src/commands/project_commands.rs` | Start/stop/rebuild Tauri command handlers |
| `app/src-tauri/src/models/project.rs` | Project struct (auth mode, Docker access, etc.) |
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS) |
| `container/Dockerfile` | Ubuntu 24.04 sandbox image with Claude Code + dev tools |
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config |
| `app/src-tauri/src/commands/file_commands.rs` | File manager Tauri commands (list, download, upload) |
| `app/src-tauri/src/commands/mcp_commands.rs` | MCP server CRUD Tauri commands |
| `app/src-tauri/src/models/project.rs` | Project struct (auth mode, Docker access, MCP servers, Mission Control) |
| `app/src-tauri/src/models/mcp_server.rs` | MCP server struct (transport, Docker image, env vars) |
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, microphone) |
| `app/src-tauri/src/storage/mcp_store.rs` | MCP server persistence (JSON with atomic writes) |
| `container/Dockerfile` | Ubuntu 24.04 sandbox image with Claude Code + dev tools + clipboard/audio shims |
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, MCP injection, Mission Control setup |
| `container/osc52-clipboard` | Clipboard shim (xclip/xsel/pbcopy via OSC 52) |
| `container/audio-shim` | Audio capture shim (rec/arecord via FIFO) for voice mode |
## CSS / Styling Notes
@@ -100,4 +140,6 @@ Users can override this in Settings via the global `docker_socket_path` option.
**Pre-installed tools**: Claude Code, Node.js 22 LTS + pnpm, Python 3.12 + uv + ruff, Rust (stable), Docker CLI, git + gh, AWS CLI v2, ripgrep, openssh-client, build-essential
**Shims**: `xclip`/`xsel`/`pbcopy` (OSC 52 clipboard forwarding), `rec`/`arecord` (audio FIFO for voice mode)
**Default user**: `claude` (UID/GID 1000, remapped by entrypoint to match host)

View File

@@ -154,13 +154,12 @@ The `.claude` configuration directory uses a **named Docker volume** (`triple-c-
### Authentication Modes
Each project independently chooses one of three authentication methods:
Each project independently chooses one of two authentication methods:
| Mode | How It Works | When to Use |
|------|-------------|-------------|
| **Login (OAuth)** | User runs `claude login` or `/login` inside the terminal. OAuth URL opens in host browser via the web links addon. Token persists in the `.claude` config volume. | Personal use, interactive sessions |
| **API Key** | Key stored in OS keychain, injected as `ANTHROPIC_API_KEY` env var at container creation. | Automated workflows, team-shared keys |
| **AWS Bedrock** | Per-project AWS credentials (static, profile, or bearer token) injected as env vars. `~/.aws` config optionally bind-mounted read-only. | Enterprise environments using Bedrock |
| **Anthropic (OAuth)** | User runs `claude login` or `/login` inside the terminal. OAuth URL opens in host browser via URL detection. Token persists in the `.claude` config volume. | Default — personal and team use |
| **AWS Bedrock** | Per-project AWS credentials (static keys, profile, or bearer token) injected as env vars. `~/.aws` config optionally bind-mounted read-only. | Enterprise environments using Bedrock |
### UID/GID Remapping
@@ -213,13 +212,26 @@ The `TerminalView` component works around this with a **URL accumulator**:
```
triple-c/
├── LICENSE # MIT
├── README.md # Architecture overview
├── TECHNICAL.md # This document
├── Triple-C.md # Project overview
├── HOW-TO-USE.md # User guide
├── BUILDING.md # Build instructions
├── CLAUDE.md # Claude Code instructions
├── container/
│ ├── Dockerfile # Ubuntu 24.04 + all dev tools + Claude Code
── entrypoint.sh # UID/GID remap, SSH setup, git config
── entrypoint.sh # UID/GID remap, SSH setup, git config, MCP injection
│ ├── osc52-clipboard # Clipboard shim (xclip/xsel/pbcopy via OSC 52)
│ ├── audio-shim # Audio capture shim (rec/arecord via FIFO)
│ ├── triple-c-scheduler # Bash-based cron task system
│ └── triple-c-task-runner # Task execution runner for scheduler
├── .gitea/
│ └── workflows/
│ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows)
│ ├── build.yml # Build container image (multi-arch)
│ ├── sync-release.yml # Mirror releases to GitHub
│ └── backfill-releases.yml # Bulk copy releases to GitHub
└── app/ # Tauri v2 desktop application
├── package.json # React, xterm.js, zustand, tailwindcss
@@ -231,22 +243,28 @@ triple-c/
│ ├── App.tsx # Top-level layout
│ ├── index.css # CSS variables, dark theme, scrollbars
│ ├── store/
│ │ └── appState.ts # Zustand store (projects, sessions, UI)
│ │ └── appState.ts # Zustand store (projects, sessions, MCP, UI)
│ ├── hooks/
│ │ ├── useDocker.ts # Docker status, image build
│ │ ├── useDocker.ts # Docker status, image build/pull
│ │ ├── useFileManager.ts # File manager operations
│ │ ├── useMcpServers.ts # MCP server CRUD
│ │ ├── useProjects.ts # Project CRUD operations
│ │ ├── useSettings.ts # API key, app settings
│ │ ── useTerminal.ts # Terminal I/O, resize, session events
│ │ ├── useSettings.ts # App settings
│ │ ── useTerminal.ts # Terminal I/O, resize, session events
│ │ ├── useUpdates.ts # App update checking
│ │ └── useVoice.ts # Voice mode audio capture
│ ├── lib/
│ │ ├── types.ts # TypeScript interfaces matching Rust models
│ │ ├── tauri-commands.ts # Typed invoke() wrappers
│ │ └── constants.ts # App-wide constants
│ └── components/
│ ├── layout/ # Sidebar, TopBar, StatusBar
│ ├── projects/ # ProjectList, ProjectCard, AddProjectDialog
│ ├── terminal/ # TerminalView (xterm.js), TerminalTabs
├── settings/ # ApiKeyInput, DockerSettings, AwsSettings
── containers/ # SiblingContainers
│ ├── mcp/ # McpPanel, McpServerCard
│ ├── projects/ # ProjectCard, ProjectList, AddProjectDialog,
│ # FileManagerModal, ContainerProgressModal, modals
── settings/ # SettingsPanel, DockerSettings, AwsSettings,
│ │ # UpdateDialog
│ └── terminal/ # TerminalView (xterm.js), TerminalTabs, UrlToast
└── src-tauri/ # Rust backend
├── Cargo.toml # Rust dependencies
@@ -256,23 +274,31 @@ triple-c/
└── src/
├── lib.rs # App builder, plugin + command registration
├── main.rs # Entry point
├── logging.rs # Log configuration
├── commands/ # Tauri command handlers
│ ├── docker_commands.rs
│ ├── project_commands.rs
│ ├── settings_commands.rs
── terminal_commands.rs
│ ├── docker_commands.rs # Docker status, image ops
│ ├── file_commands.rs # File manager (list/download/upload)
│ ├── mcp_commands.rs # MCP server CRUD
── project_commands.rs # Start/stop/rebuild containers
│ ├── settings_commands.rs # Settings CRUD
│ ├── terminal_commands.rs # Terminal I/O, resize
│ └── update_commands.rs # App update checking
├── docker/ # Docker API layer
│ ├── client.rs # bollard singleton connection
│ ├── container.rs # Create, start, stop, remove, inspect
│ ├── container.rs # Create, start, stop, remove, fingerprinting
│ ├── exec.rs # PTY exec sessions with bidirectional streaming
│ ├── image.rs # Build from embedded Dockerfile, pull from registry
│ └── sibling.rs # List non-Triple-C containers
│ ├── image.rs # Build from Dockerfile, pull from registry
│ └── network.rs # Per-project bridge networks for MCP
├── models/ # Data structures
│ ├── project.rs # Project, AuthMode, BedrockConfig
── container_config.rs
── mcp_server.rs # MCP server configuration
│ ├── app_settings.rs # Global settings (image source, AWS, etc.)
│ ├── container_config.rs # Image name resolution
│ └── update_info.rs # Update metadata
└── storage/ # Persistence
├── projects_store.rs # JSON file with atomic writes
├── settings_store.rs # App settings
├── mcp_store.rs # MCP server persistence
├── settings_store.rs # App settings (Tauri plugin-store)
└── secure.rs # OS keychain via keyring
```

View File

@@ -36,11 +36,10 @@ pub async fn list_container_files(
let cmd = vec![
"find".to_string(),
path.clone(),
"-mindepth".to_string(),
"1".to_string(),
"-maxdepth".to_string(),
"1".to_string(),
"-not".to_string(),
"-name".to_string(),
".".to_string(),
"-printf".to_string(),
"%f\t%y\t%s\t%T@\t%m\n".to_string(),
];

View File

@@ -202,6 +202,28 @@ pub async fn start_project_container(
// Set up Docker network and MCP containers if needed
let network_name = if !docker_mcp.is_empty() {
// Pull any missing MCP Docker images before starting containers
for server in &docker_mcp {
if let Some(ref image) = server.docker_image {
if !docker::image_exists(image).await.unwrap_or(false) {
emit_progress(
&app_handle,
&project_id,
&format!("Pulling MCP image for '{}'...", server.name),
);
let image_clone = image.clone();
let app_clone = app_handle.clone();
let pid_clone = project_id.clone();
let sname = server.name.clone();
docker::pull_image(&image_clone, move |msg| {
emit_progress(&app_clone, &pid_clone, &format!("[{}] {}", sname, msg));
}).await.map_err(|e| {
format!("Failed to pull MCP image '{}' for '{}': {}", image, server.name, e)
})?;
}
}
}
emit_progress(&app_handle, &project_id, "Setting up MCP network...");
let net = docker::ensure_project_network(&project.id).await?;
emit_progress(&app_handle, &project_id, "Starting MCP containers...");
@@ -386,6 +408,46 @@ pub async fn rebuild_project_container(
start_project_container(project_id, app_handle, state).await
}
/// Reconcile project statuses against actual Docker container state.
/// Called by the frontend after Docker is confirmed available. Projects
/// marked as Running whose containers are no longer running get reset
/// to Stopped.
#[tauri::command]
pub async fn reconcile_project_statuses(
state: State<'_, AppState>,
) -> Result<Vec<Project>, String> {
let projects = state.projects_store.list();
for project in &projects {
if project.status != ProjectStatus::Running && project.status != ProjectStatus::Error {
continue;
}
let is_running = if let Some(ref container_id) = project.container_id {
docker::is_container_running(container_id).await.unwrap_or(false)
} else {
false
};
if is_running {
log::info!(
"Project '{}' ({}) container is still running — keeping Running status",
project.name,
project.id
);
} else {
log::info!(
"Project '{}' ({}) container is not running — setting to Stopped",
project.name,
project.id
);
let _ = state.projects_store.update_status(&project.id, ProjectStatus::Stopped);
}
}
Ok(state.projects_store.list())
}
fn default_docker_socket() -> String {
if cfg!(target_os = "windows") {
"//./pipe/docker_engine".to_string()

View File

@@ -47,8 +47,8 @@ The `/workspace/mission-control/` directory contains **Flight Control** — an A
### How It Works
- **Mission Control is a tool, not a project.** It provides skills and methodology for managing other projects.
- All Flight Control skills live in `/workspace/mission-control/.claude/skills/`
- The projects registry at `/workspace/mission-control/projects.md` lists all active projects
- All Flight Control skills are installed as personal skills in `~/.claude/skills/` and are automatically available as `/slash-commands`
- The methodology docs and project registry live in `/workspace/mission-control/`
### When to Use
@@ -1031,6 +1031,16 @@ pub async fn get_container_info(project: &Project) -> Result<Option<ContainerInf
}
}
/// Check whether a Docker container is currently running.
/// Returns false if the container doesn't exist or Docker is unavailable.
pub async fn is_container_running(container_id: &str) -> Result<bool, String> {
let docker = get_docker()?;
match docker.inspect_container(container_id, None).await {
Ok(info) => Ok(info.state.and_then(|s| s.running).unwrap_or(false)),
Err(_) => Ok(false),
}
}
pub async fn list_sibling_containers() -> Result<Vec<ContainerSummary>, String> {
let docker = get_docker()?;

View File

@@ -88,6 +88,7 @@ pub fn run() {
commands::project_commands::start_project_container,
commands::project_commands::stop_project_container,
commands::project_commands::rebuild_project_container,
commands::project_commands::reconcile_project_statuses,
// Settings
commands::settings_commands::get_settings,
commands::settings_commands::update_settings,

View File

@@ -72,6 +72,8 @@ impl ProjectsStore {
// Reconcile stale transient statuses: on a cold app start no Docker
// operations can be in flight, so Starting/Stopping are always stale.
// Running/Error are left as-is and reconciled against Docker later
// via the reconcile_project_statuses command.
let mut projects = projects;
let mut needs_save = needs_save;
for p in projects.iter_mut() {

View File

@@ -10,6 +10,7 @@ import { useProjects } from "./hooks/useProjects";
import { useMcpServers } from "./hooks/useMcpServers";
import { useUpdates } from "./hooks/useUpdates";
import { useAppState } from "./store/appState";
import { reconcileProjectStatuses } from "./lib/tauri-commands";
export default function App() {
const { checkDocker, checkImage, startDockerPolling } = useDocker();
@@ -17,8 +18,8 @@ export default function App() {
const { refresh } = useProjects();
const { refresh: refreshMcp } = useMcpServers();
const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates();
const { sessions, activeSessionId } = useAppState(
useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId }))
const { sessions, activeSessionId, setProjects } = useAppState(
useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, setProjects: s.setProjects }))
);
// Initialize on mount
@@ -28,6 +29,14 @@ export default function App() {
checkDocker().then((available) => {
if (available) {
checkImage();
// Reconcile project statuses against actual Docker container state,
// then refresh the project list so the UI reflects reality.
reconcileProjectStatuses().then((projects) => {
setProjects(projects);
}).catch(() => {
// If reconciliation fails (e.g. Docker hiccup), just load from store
refresh();
});
} else {
stopPolling = startDockerPolling();
}

View File

@@ -147,7 +147,7 @@ export default function McpServerCard({ server, onUpdate, onRemove }: Props) {
className={inputCls}
/>
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-60">
Set a Docker image to run this MCP server as a container. Leave empty for manual mode.
Set a Docker image to run this MCP server in its own container. Leave empty to run commands inside the project container. Images are pulled automatically if not present.
</p>
</div>
@@ -171,6 +171,14 @@ export default function McpServerCard({ server, onUpdate, onRemove }: Props) {
</div>
</div>
{/* Mode description */}
<p className="text-xs text-[var(--text-secondary)] opacity-60">
{transportType === "stdio" && isDocker && "Runs via docker exec in a separate MCP container."}
{transportType === "stdio" && !isDocker && "Runs inside the project container (e.g. npx commands)."}
{transportType === "http" && isDocker && "Runs in a separate container, reached by hostname on the project network."}
{transportType === "http" && !isDocker && "Connects to an MCP server at the URL you specify."}
</p>
{/* Container Port (HTTP+Docker only) */}
{transportType === "http" && isDocker && (
<div>
@@ -183,7 +191,7 @@ export default function McpServerCard({ server, onUpdate, onRemove }: Props) {
className={inputCls}
/>
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-60">
Port inside the MCP container (default: 3000)
Port the MCP server listens on inside its container. The URL is auto-generated as http://&lt;container&gt;:&lt;port&gt;/mcp on the project network.
</p>
</div>
)}

View File

@@ -24,6 +24,8 @@ export const stopProjectContainer = (projectId: string) =>
invoke<void>("stop_project_container", { projectId });
export const rebuildProjectContainer = (projectId: string) =>
invoke<Project>("rebuild_project_container", { projectId });
export const reconcileProjectStatuses = () =>
invoke<Project[]>("reconcile_project_statuses");
// Settings
export const getSettings = () => invoke<AppSettings>("get_settings");

View File

@@ -131,6 +131,15 @@ if [ "$MISSION_CONTROL_ENABLED" = "1" ]; then
# Symlink into workspace so Claude sees it at /workspace/mission-control
ln -sfn "$MC_HOME" "$MC_LINK"
chown -h claude:claude "$MC_LINK"
# Install skills to ~/.claude/skills/ so Claude Code discovers them automatically
if [ -d "$MC_HOME/.claude/skills" ]; then
mkdir -p /home/claude/.claude/skills
cp -r "$MC_HOME/.claude/skills/"* /home/claude/.claude/skills/ 2>/dev/null
chown -R claude:claude /home/claude/.claude/skills
echo "entrypoint: mission-control skills installed to ~/.claude/skills/"
fi
unset MISSION_CONTROL_ENABLED
fi