Confirm before Reset, and rewrite the docs for the new UI

Reset is destructive in a way its name does not advertise:
rebuild_project_container deletes both project volumes, so it wipes the
claude login, anything installed in the container, and every saved
session transcript. It was a single unconfirmed click in the overflow
menu, while the comparably destructive Remove already confirmed. Adds
ConfirmResetModal, which names each loss and says explicitly that the
host-side mounted folders are untouched.

Docs: the user guides still described the pre-Project-Home UI. Sixteen
factually wrong statements corrected, including "expand the Config
panel" (six sites), the actions table (Reset and Remove are in an
overflow menu, Files is a tab), a progress modal that no longer exists,
a double-click-to-rename gesture ProjectRow never had, the Full
Permissions boolean, an incomplete reserved-env list, and the claim in
TECHNICAL.md that OAuth tokens survive a Reset. Both layout diagrams and
the project tree were rebuilt from the filesystem.

New sections cover permission modes with the exact CLI mapping, Project
Home, Sessions, capability tiles, Automation, shared authentication, the
Auth Bridge and its security posture, and keyboard shortcuts.

Known gap recorded rather than papered over: the Automation tab manages
existing scheduled tasks but cannot create them — no add command is
registered — so task creation remains `triple-c-scheduler add` in the
terminal.

87 frontend tests, 34 Rust tests, both builds clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 12:02:35 -07:00
co-authored by Claude Opus 5
parent d95ba54a69
commit cf3b021c72
6 changed files with 1018 additions and 161 deletions
+468 -86
View File
@@ -9,16 +9,22 @@ Triple-C (Claude-Code-Container) is a desktop application that runs Claude Code
- [Prerequisites](#prerequisites) - [Prerequisites](#prerequisites)
- [First Launch](#first-launch) - [First Launch](#first-launch)
- [The Interface](#the-interface) - [The Interface](#the-interface)
- [Project Home](#project-home)
- [Project Management](#project-management) - [Project Management](#project-management)
- [Permission Modes](#permission-modes)
- [Project Configuration](#project-configuration) - [Project Configuration](#project-configuration)
- [Shared Claude Authentication](#shared-claude-authentication)
- [Browser Logins Inside the Container (Auth Bridge)](#browser-logins-inside-the-container-auth-bridge)
- [AWS Bedrock Configuration](#aws-bedrock-configuration) - [AWS Bedrock Configuration](#aws-bedrock-configuration)
- [Ollama Configuration](#ollama-configuration) - [Ollama Configuration](#ollama-configuration)
- [OpenAI Compatible Configuration](#openai-compatible-configuration) - [OpenAI Compatible Configuration](#openai-compatible-configuration)
- [Settings](#settings) - [Settings](#settings)
- [Web Terminal (Remote Access)](#web-terminal-remote-access) - [Web Terminal (Remote Access)](#web-terminal-remote-access)
- [Terminal Features](#terminal-features) - [Terminal Features](#terminal-features)
- [Scheduled Tasks (Inside the Container)](#scheduled-tasks-inside-the-container) - [Automation & Scheduled Tasks](#automation--scheduled-tasks)
- [Keyboard Shortcuts](#keyboard-shortcuts)
- [What's Inside the Container](#whats-inside-the-container) - [What's Inside the Container](#whats-inside-the-container)
- [Claude Code Tips](#claude-code-tips)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
--- ---
@@ -78,7 +84,7 @@ Click **Pull Image** (for Registry/Custom) or **Build Image** (for Local Build).
### 2. Create Your First Project ### 2. Create Your First Project
Switch to the **Projects** tab in the sidebar and click the **+** button. Switch to the **Projects** tab in the sidebar and click **+ Add**.
1. **Project Name** — Give it a meaningful name (e.g., "my-web-app"). 1. **Project Name** — Give it a meaningful name (e.g., "my-web-app").
2. **Folders** — Click **Browse** to select a directory on your host machine. This directory will be mounted into the container at `/workspace/<folder-name>`. You can add multiple folders with the **+** button at the bottom of the folder list. 2. **Folders** — Click **Browse** to select a directory on your host machine. This directory will be mounted into the container at `/workspace/<folder-name>`. You can add multiple folders with the **+** button at the bottom of the folder list.
@@ -86,42 +92,61 @@ Switch to the **Projects** tab in the sidebar and click the **+** button.
### 3. Start the Container ### 3. Start the Container
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. Click the project in the sidebar. Its **Project Home** opens as a tab in the main area. Click
**Start** in the Project Home header (or use the play control that appears when you hover the
sidebar row).
Progress is reported inline — the sidebar row and the Project Home header show messages like
"Creating container…" and "Starting container…" while the status moves from Stopped (`○`) to
Starting (`◐`) to Running (`●`). Nothing blocks the rest of the app; if something fails you get a
toast with the full detail behind a **Details** disclosure.
### 4. Open a Terminal ### 4. Open a Terminal
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. Click **Open Claude Terminal** in the Project Home header, or press **Ctrl+T**. A new tab appears
in the main tab strip and an xterm.js terminal loads.
Claude Code launches automatically. By default, it runs in standard permission mode and will ask for your approval before executing commands or editing files. To enable auto-approval of all actions within the sandbox, enable **Full Permissions** in the project configuration. Claude Code launches automatically. The project's **permission mode** decides how much it asks
before acting — the default is to prompt before each tool call. See
[Permission Modes](#permission-modes).
### 5. Authenticate ### 5. Authenticate
**Anthropic (OAuth) — default:** **Anthropic — shared token (recommended):**
Run `claude setup-token` once from **Settings → Claude Authentication** in the sidebar, and every
Anthropic-backend project uses that token without its own login. See
[Shared Claude Authentication](#shared-claude-authentication).
**Anthropic — per-container OAuth:**
1. Type `claude login` or `/login` in the terminal. 1. Type `claude login` or `/login` in the terminal.
2. Claude prints an OAuth URL. Triple-C detects long URLs and shows a clickable toast at the top of the terminal — click **Open** to open it in your browser. 2. Claude prints an OAuth URL. Triple-C detects long URLs and shows a clickable toast at the top of the terminal — click **Open** to open it in your browser.
3. Complete the login in your browser. The token is saved and persists across container stops and resets. 3. Complete the login in your browser. The token is saved and persists across container stops, starts and recreations. A **Reset** deletes it — see below.
> If the login hangs after the browser step, the callback could not reach the container. Enable the
> [Auth Bridge](#browser-logins-inside-the-container-auth-bridge) for that project.
**AWS Bedrock:** **AWS Bedrock:**
1. Stop the container first (settings can only be changed while stopped). 1. Stop the container first (most settings can only be changed while stopped).
2. In the project card, switch the backend to **Bedrock**. 2. Open the project's **Config** tab and, under **Model**, set **Backend** to **Bedrock**.
3. Expand the **Config** panel and fill in your AWS credentials (see [AWS Bedrock Configuration](#aws-bedrock-configuration) below). 3. Fill in your AWS credentials in the same section (see [AWS Bedrock Configuration](#aws-bedrock-configuration) below).
4. Start the container again. 4. Start the container again.
**Ollama:** **Ollama:**
1. Stop the container first (settings can only be changed while stopped). 1. Stop the container first (most settings can only be changed while stopped).
2. In the project card, switch the backend to **Ollama**. 2. Open the project's **Config** tab and, under **Model**, set **Backend** to **Ollama**.
3. Expand the **Config** panel and set the base URL of your Ollama server (defaults to `http://host.docker.internal:11434` for a local instance). Set the **Model ID** to the model you want to use (required). 3. Set the base URL of your Ollama server (defaults to `http://host.docker.internal:11434` for a local instance). Set the **Model** to the model you want to use (required).
4. Make sure the model has been pulled in Ollama (e.g., `ollama pull qwen3.5:27b`) or used via Ollama cloud before starting. 4. Make sure the model has been pulled in Ollama (e.g., `ollama pull qwen3.5:27b`) or used via Ollama cloud before starting.
5. Start the container again. 5. Start the container again.
**OpenAI Compatible:** **OpenAI Compatible:**
1. Stop the container first (settings can only be changed while stopped). 1. Stop the container first (most settings can only be changed while stopped).
2. In the project card, switch the backend to **OpenAI Compatible**. 2. Open the project's **Config** tab and, under **Model**, set **Backend** to **OpenAI Compatible**.
3. Expand the **Config** panel and set the base URL of your OpenAI-compatible endpoint (defaults to `http://host.docker.internal:4000` as an example). Optionally set an API key and model ID. 3. Set the base URL of your OpenAI-compatible endpoint (defaults to `http://host.docker.internal:4000` as an example). Optionally set an API key and model.
4. Start the container again. 4. Start the container again.
--- ---
@@ -129,22 +154,97 @@ Claude Code launches automatically. By default, it runs in standard permission m
## The Interface ## The Interface
``` ```
┌─────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────
TopBar [ Terminal Tabs ] Docker ● Image ●│ [⌂ my-app] [▣ my-app ask] [▣ my-app (bash)] Docker ● Image ● ?
├────────────────────────────────────────────────────┤ ├─────────────┬────────────────────────────────────────────────────────┤
│ Sidebar │ │ Sidebar ┌──────────────────────────────────────────────────┐
│ │ Terminal View │ my-app ● Running · up 2h 5m
│ Projects │ (xterm.js) │ Projects [Open Claude Terminal] [Shell] [Files]
│ Settings │ Settings │ │ [Stop] [⋯]
├────────────┴────────────────────────────────────────┤ │ │ ├──────────────────────────────────────────────────┤
StatusBar X projects · X running · X terminals ● my-app │ │ Overview · Sessions · Automation · Config · Files│
─────────────────────────────────────────────────────┘ │ ○ other │ ├──────────────────────────────────────────────────┤ │
│ │ │ │ │
│ │ │ (Project Home, or a terminal view) │ │
│ │ │ │ │
│ │ └──────────────────────────────────────────────────┘ │
├─────────────┴────────────────────────────────────────────────────────┤
│ 2 project(s) · 1 running · 2 terminal(s) Jump to Current ↓ │
└──────────────────────────────────────────────────────────────────────┘
``` ```
- **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). - **Tab strip (top)** — One strip holds every open tab, in the order you opened them. There are two
- **Sidebar** — Toggle between the **Projects** list and the **Settings** panel. kinds: **Project Home** tabs (`⌂` glyph, project name, status glyph) and **terminal** tabs (`▣`
- **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. glyph, plus a small badge showing the permission mode the terminal was launched with —
- **StatusBar** — Counts of total projects, running containers, and open terminal sessions. `plan`, `ask`, `edits` or `bypass`). Bash shell tabs show a "(bash)" suffix. Right-click a
terminal tab to rename it, jump to its project home, or close it; double-click to rename inline.
There is no separate terminal tab bar and no "+" button — tabs appear when you open a project or
a terminal.
- **Status indicators (top right)** — Docker connection and container image availability. Each pairs
a coloured dot with a word, so status is never conveyed by colour alone. The **?** button opens
the built-in help.
- **Sidebar** — Toggle between the **Projects** list and the **Settings** panel. It collapses to a
narrow icon rail with the chevron button, and remembers that choice.
- **Main area** — Shows the active tab: a Project Home view or an xterm.js terminal. With no tabs
open you get a welcome screen with Docker/image/project readiness checks.
- **StatusBar** — Counts of total projects, running containers and open terminal sessions; the
**Jump to Current ↓** button when a terminal is scrolled up; and the microphone button when
speech-to-text is enabled.
---
## Project Home
Clicking a project in the sidebar opens **Project Home** in the main area. The sidebar row is only
for selecting a project and for two quick controls that appear on hover — start/stop, and open a
Claude terminal. Everything else about a project lives in Project Home.
The header shows the project name, its status, how long the container has been up, and the action
buttons. Below that are five tabs:
| Tab | What it's for |
|---|---|
| **Overview** | The permission mode control, a summary of the backend and sandbox settings, capability tiles, recent sessions and scheduled tasks |
| **Sessions** | Past Claude Code conversations stored on this project's config volume, each with a **Resume** button |
| **Automation** | The scheduled tasks running inside this container — see [Automation & Scheduled Tasks](#automation--scheduled-tasks) |
| **Config** | All per-project configuration — see [Project Configuration](#project-configuration) |
| **Files** | Browse, download and upload files inside the container |
### Sessions
Claude Code records each conversation on the project's config volume. The **Sessions** tab lists
them with a name or summary, the session id, its working directory, its age, size and message
count. **Refresh** re-reads the list.
**Resume** opens a new shell tab and runs `claude --resume <session-id>` for you, with the
project's current permission-mode flags applied. The Overview tab shows the four most recent
sessions with the same Resume action.
Sessions can only be read while the container is running, and they are stored on the config volume
— so a **Reset** deletes them.
### Capability Tiles
The Overview tab shows read-only counts of what Claude Code has available **inside this container**:
| Tile | What is counted |
|---|---|
| **Skills** | Directories under `.claude/skills/` that contain a `SKILL.md` |
| **Agents** | `.md` files under `.claude/agents/` |
| **Commands** | `.md` files under `.claude/commands/` |
| **Hooks** | Hook handlers configured in `.claude/settings.json` / `settings.local.json` |
| **Plugins** | Installed and enabled Claude Code plugins |
| **MCP servers** | Servers in `~/.claude.json` and in any `.mcp.json` under `/workspace` |
Both user scope (`/home/claude/.claude`) and project scope (`/workspace/<folder>/.claude`) are
included, and each tile opens a list of what it found.
> **Triple-C does not edit any of this.** Claude Code owns skills, agents, commands, hooks, plugins
> and MCP servers, and it has good built-in tooling for them. The tiles are a window, not an editor:
> **Manage in terminal** opens a terminal in the container so you can use `/agents`, `/hooks`,
> `/plugins`, `/mcp` and friends directly.
The counts are only available while the container is running.
--- ---
@@ -152,59 +252,135 @@ Claude Code launches automatically. By default, it runs in standard permission m
### Project Status ### Project Status
Each project shows a colored status dot: Each project shows a status glyph paired with a word, so it is readable without relying on colour:
| Color | Status | Meaning | | Glyph | Status | Meaning |
|-------|--------|---------| |-------|--------|---------|
| Gray | Stopped | Container is not running | | `○` | Stopped | Container is not running |
| Orange | Starting / Stopping | Container is transitioning | | `◐` | Starting / Stopping | Container is transitioning (the glyph pulses) |
| Green | Running | Container is active, ready for terminals | | `●` | Running | Container is active, ready for terminals |
| Red | Error | Something went wrong (check error message) | | `▲` | Error | Something went wrong (check the toast for detail) |
While a container is starting or stopping, the status line is replaced by the live progress message.
### Project Actions ### Project Actions
Select a project in the sidebar to see its action buttons: Most actions live in the **Project Home header**; two live behind the **⋯** overflow menu next to
it. The sidebar row carries only the two hover controls.
| Button | When Available | What It Does | | Action | Where | When Available | What It Does |
|--------|---------------|--------------| |--------|-------|---------------|--------------|
| **Start** | Stopped | Creates (if needed) and starts the container | | **Start** | Project Home header; sidebar hover control | Stopped | Creates (if needed) and starts the container |
| **Stop** | Running | Stops the container but preserves its state | | **Stop** | Project Home header; sidebar hover control | Running | Stops the container but preserves its state |
| **Terminal** | Running | Opens a new Claude Code terminal session | | **Force stop** | Project Home header | Starting / Stopping | Interrupts a transition that is stuck |
| **Shell** | Running | Opens a bash login shell in the container (no Claude Code) | | **Open Claude Terminal** | Project Home header; sidebar hover control; `Ctrl+T` | Running | Opens a new Claude Code terminal tab |
| **Files** | Running | Opens the file manager to browse, download, and upload files | | **Shell** | Project Home header | Running | Opens a bash login shell tab in the container (no Claude Code) |
| **Reset** | Stopped | Destroys and recreates the container from scratch | | **Files** | Project Home header, and the **Files** tab | Running | Switches to the Files tab to browse, download and upload files |
| **Config** | Always | Toggles the configuration panel | | **Config** | The **Config** tab | Always | Per-project configuration (most fields need the container stopped) |
| **Remove** | Stopped | Deletes the project and its container (with confirmation) | | **Back up container** | **⋯** overflow menu | A container exists | Saves a `.tar.gz` archive of the container to a location you choose |
| **Reset container…** | **⋯** overflow menu | Stopped or Error | Destroys the container, snapshot image and both volumes, then recreates from the base image (wipes `~/.claude`) — asks first |
| **Remove project…** | **⋯** overflow menu | Always | Deletes the project, its container, its volumes and its stored credentials — asks first |
> Both destructive actions confirm before acting, and the Reset dialog spells out what you
> lose: your `claude login`, anything installed inside the container, and every saved
> session transcript. Your mounted project folders live on the host and are not touched.
> The backup archive includes the Claude config volume, which may contain API keys. Keep it private.
### Renaming a Project ### Renaming a Project
Double-click the project name in the sidebar to rename it inline. Press **Enter** to confirm or **Escape** to cancel. Rename a project in its **Config** tab, under **Workspace → Project name**. Press **Enter** to save
and leave the field, or **Escape** to revert. (Double-clicking a *terminal tab* renames that tab —
that is a different thing.)
### Container Lifecycle ### 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. 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.
**Reset** removes the container and creates a fresh one. However, your Claude Code configuration (including OAuth tokens from `claude login`) is stored in a separate Docker volume and survives resets. **Reset container** removes the container, its snapshot image **and both of its named volumes**
(`triple-c-home-<project-id>` and `triple-c-claude-config-<project-id>`), then creates a fresh one
from the clean base image. This is destructive: `~/.claude` and `~/.claude.json` go with the
volumes, so your per-container OAuth login, any skills or agents you installed, your session
transcripts and your scheduled tasks are all lost.
Only **Remove** deletes everything, including the config volume and any stored credentials. What Reset keeps: your host folders (they are bind mounts and are never touched), the project's
configuration in Triple-C, and anything stored in your OS keychain — including the shared Claude
authentication token. If the project uses that shared token, it re-authenticates by itself after a
Reset; if it relies on `claude login`, you will need to log in again.
Apart from **Remove project…**, Reset is the only action that deletes the volumes. Stopping and
starting preserves them, and so does the automatic container recreation that happens when you
change a setting that affects the container — in both cases your Claude Code configuration
survives.
**Remove project…** deletes everything Reset does, plus the project record itself and its stored
credentials.
### Container Progress Feedback ### Container Progress Feedback
When starting, stopping, or resetting a container, a progress modal shows real-time status messages (e.g., "Creating container...", "Starting 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. When starting, stopping, or resetting a container, progress is shown inline on the project row and in the Project Home header (e.g., "Creating container...", "Starting container..."), so the rest of the app stays usable. If an error occurs it is raised as a toast with the full detail behind a **Details** disclosure. There is no blocking progress modal.
---
## Permission Modes
Every project has a **permission mode** that decides how much Claude Code does without asking. It
is a segmented control on the **Overview** tab (and again under **Config → Runtime**), and it
replaces the old Full Permissions on/off switch.
| Mode | What Claude does | What Triple-C passes to `claude` |
|------|------------------|----------------------------------|
| **Plan** | Proposes a plan and makes no changes | `--permission-mode plan` |
| **Default** | Asks before each tool call | *(nothing — Claude Code's own default)* |
| **Accept Edits** | Auto-approves file edits; other tools still prompt | `--permission-mode acceptEdits` |
| **Bypass** | Auto-approves every tool call | `--dangerously-skip-permissions` |
New projects start in **Default**. Projects created before permission modes existed keep behaving
the way they did: one that had Full Permissions on becomes **Bypass**, one that had it off becomes
**Default**.
> **CAUTION:** In **Bypass**, Claude can execute any command inside the container without asking.
> The container sandbox limits the blast radius, but think carefully — especially if the container
> has Docker socket access or reaches services on your network. The Overview tab tells you whether
> the in-container sandbox is also on.
### When a change takes effect
- **Terminals** — the mode is applied when a terminal is opened, so it affects terminals you open
from then on. A Claude session that is already running keeps the permissions it started with;
close the tab and open a new terminal to change it. The badge on each terminal tab shows the mode
that terminal was launched with (`plan`, `ask`, `edits`, `bypass`).
- **Resumed sessions** — a session resumed from the **Sessions** tab uses the project's current
mode.
- **Scheduled tasks** — these now honour the permission mode too (they previously always ran with
`--dangerously-skip-permissions`). The mode reaches them through the container's environment,
which can only change when the container is recreated, so **stop and start the project** for a
mode change to reach the scheduler.
> Scheduled tasks run headless (`claude -p`) and cannot answer a permission prompt. In any mode
> other than **Bypass**, a task may simply stop early when Claude Code asks for approval. Its run
> log records which mode it used.
--- ---
## Project Configuration ## Project Configuration
Click **Config** on a selected project to expand the configuration panel. Settings can only be changed when the container is **stopped** (an orange warning box appears if the container is running). Open a project's **Config** tab in Project Home. Configuration is grouped into four sections —
**Workspace**, **Model**, **Access** and **Runtime** — plus **Claude instructions** and **Claude
Code settings**.
Changes save automatically when a field loses focus, and a Saved / Saving… / Failed indicator in
the corner tells you what happened. Most settings can only be changed when the container is
**stopped**; a warning chip appears at the top of the tab if it is running. (The project name and
the permission mode can be changed at any time.)
### Mounted Folders ### Mounted Folders
Each project mounts one or more host directories into the container. The mount appears at `/workspace/<mount-name>` inside the container. Each project mounts one or more host directories into the container. The mount appears at `/workspace/<mount-name>` inside the container.
- Click **Browse** ("...") to change the host path - Click **Browse** to change the host path
- Edit the mount name to control where it appears inside `/workspace/` - Edit the mount name to control where it appears inside `/workspace/`
- Click **+** to add more folders, or **x** to remove one - Click **+ Add folder** to add more, or **Remove** to drop one (the last remaining folder cannot be removed)
- Mount names must be unique and use only letters, numbers, dashes, underscores, and dots - Mount names must be unique and use only letters, numbers, dashes, underscores, and dots
### SSH Keys ### SSH Keys
@@ -235,27 +411,31 @@ Available skills include `/mission`, `/flight`, `/leg`, `/agentic-workflow`, `/f
> This setting can only be changed when the container is stopped. Toggling it triggers a container recreation on the next start. > This setting can only be changed when the container is stopped. Toggling it triggers a container recreation on the next start.
### Full Permissions ### Permission Mode
Toggle **Full Permissions** to allow Claude Code to run with `--dangerously-skip-permissions` inside the container. This is **off by default**. The **Runtime** section repeats the permission mode control from the Overview tab — see
[Permission Modes](#permission-modes) for what each mode does and when a change takes effect.
When **enabled**, Claude auto-approves all tool calls (file edits, shell commands, etc.) without prompting you. This is the fastest workflow since you won't be interrupted for approvals, and the Docker container provides isolation. ### Sandbox Mode
When **disabled** (default), Claude prompts you for approval before executing each action, giving you fine-grained control over what it does. Toggles Claude Code's in-container bubblewrap isolation. The Overview tab shows the current state
next to the permission mode, because the two together decide how contained a Bypass-mode session
> **CAUTION:** Enabling full permissions means Claude can execute any command inside the container without asking. While the container sandbox limits the blast radius, make sure you understand the implications — especially if the container has Docker socket access or network connectivity. really is.
> This setting can only be changed when the container is stopped. It takes effect the next time you open a terminal session.
### Environment Variables ### 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. Add key-value pairs under **Access → Environment variables**; they are injected into the container.
Per-project variables override global variables with the same key.
> Reserved prefixes (`ANTHROPIC_`, `AWS_`, `GIT_`, `HOST_`, `TRIPLE_C_`) and specific internal variables (`CLAUDE_INSTRUCTIONS`, `CLAUDE_CODE_SETTINGS_JSON`, etc.) are filtered out to prevent conflicts. `CLAUDE_CODE_*` variables are now allowed, so you can set Claude Code feature flags directly (e.g., `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`). > Reserved prefixes (`ANTHROPIC_`, `AWS_`, `GIT_`, `HOST_`, `TRIPLE_C_`) are filtered out to prevent
> conflicts, along with the exact names Triple-C manages itself: `CLAUDE_INSTRUCTIONS`,
> `CLAUDE_CODE_SETTINGS_JSON`, `CLAUDE_CODE_OAUTH_TOKEN`, `MISSION_CONTROL_ENABLED`,
> `TRIPLE_C_PERMISSION_MODE` and `MCP_SERVERS_JSON`. Other `CLAUDE_CODE_*` variables are allowed, so
> you can set Claude Code feature flags directly (e.g., `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`).
### Port Mappings ### Port Mappings
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. Under **Access → Port mappings**, 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: Each mapping specifies:
- **Host Port** — The port on your machine (1-65535) - **Host Port** — The port on your machine (1-65535)
@@ -264,11 +444,11 @@ Each mapping specifies:
### Claude Instructions ### Claude Instructions
Click **Edit** to write per-project instructions for Claude Code. These are written to `~/.claude/CLAUDE.md` inside the container and provide project-specific context. If you also have global instructions (in Settings), the global instructions come first, followed by the per-project instructions. The **Claude instructions** editor at the bottom of the Config tab holds per-project instructions for Claude Code. These are written to `~/.claude/CLAUDE.md` inside the container and provide project-specific context. If you also have global instructions (in Settings), the global instructions come first, followed by the per-project instructions.
### Claude Code Settings ### Claude Code Settings
Click **Edit** next to "Claude Code Settings" to configure Claude Code CLI behavior for this project. These settings control how Claude Code operates inside the container: The **Claude Code settings** editor, also at the bottom of the Config tab, configures Claude Code CLI behavior for this project. These settings control how Claude Code operates inside the container:
| Setting | What It Does | | Setting | What It Does |
|---------|-------------| |---------|-------------|
@@ -299,22 +479,113 @@ Your MCP configuration persists across container stop/start because `~/.claude.j
--- ---
## Shared Claude Authentication
Instead of running `claude login` separately in every container, you can authenticate once and
share the result across projects. Claude Code's `claude setup-token` mints a long-lived token
(roughly a year) that Triple-C stores in your **OS keychain** and injects into containers as
`CLAUDE_CODE_OAUTH_TOKEN`.
This lives in the sidebar under **Settings → Claude Authentication**.
### Signing in
1. Start at least one project whose container is running — the flow borrows that container as a
place to run the CLI. The token it produces is global, not tied to that project.
2. Start the sign-in. Triple-C runs `claude setup-token` inside the container and streams its
output.
3. Claude Code prints an authorization URL. Open it, sign in, and Anthropic's page gives you a
code to copy — this flow finishes on an Anthropic-hosted page, not a local callback.
4. Paste the code back into Triple-C. The token is captured and written straight to the keychain.
Only one sign-in can run at a time, and the whole flow times out after 15 minutes. A long-lived
token requires a Claude subscription; without one, `setup-token` finishes without printing a token
and nothing is stored.
### How the token is used
- It is injected only into projects whose backend is **Anthropic** — it means nothing to Bedrock,
Ollama or an OpenAI-compatible endpoint.
- Each project can opt out under **Config → Model** ("Use the shared Claude token"). Projects are
opted **in** by default, so a single sign-in covers your whole fleet; opt a project out if you
want it pinned to its own `claude login` identity.
- `CLAUDE_CODE_OAUTH_TOKEN` is reserved — you cannot set it yourself as a custom environment
variable, because a hand-set value would silently outrank the stored token.
- A container picks the token up when it is **next started**: acquiring, re-acquiring, revoking or
opting out changes an internal marker that triggers a container recreation on the next start.
Restart your Anthropic-backend containers after signing in.
### Revoking
Revoking deletes the token from your keychain. Containers keep the value they were given until each
is next started, at which point the same recreation clears the variable.
> The token is never shown in the app, never written to a log, and never sent to the frontend.
> While `setup-token` is running, its output is filtered so anything resembling an `sk-ant-`
> secret is masked before it reaches the screen — including a secret split across two chunks of
> output.
---
## Browser Logins Inside the Container (Auth Bridge)
Some CLIs log you in by opening a browser and waiting for the browser to call back to a temporary
web server they started on `localhost`. `claude login`, `aws sso login` and Concourse's
`fly login` all work this way. When the CLI runs inside a container, that `localhost` is the
*container's* — the browser on your host calls back into nothing and the login hangs forever.
The **Auth Bridge** fixes this. It is **opt-in per project** and **off by default**.
### What it does
- Every couple of seconds it looks inside the container for programs listening on the container's
loopback address, and binds **the same port number** on your host. That is the whole trick: the
redirect URL the login provider was handed resolves correctly on both sides.
- Connections are carried into the container over the Docker API, which keeps working on Docker
Desktop where container IP addresses are not reachable from the host.
- It follows whichever address family the container program actually used. This matters in
practice: Node resolves `localhost` to IPv6 first on Linux, so `claude login` frequently listens
on `::1` and nothing else.
- Ports you have already configured as port mappings are left alone. If a host port is already
taken, the bridge reports a conflict and leaves it alone rather than fighting for it — it will
retry on a later pass.
- The bridge is entirely host-side, so turning it on or off never recreates the container. It stops
by itself when the container stops.
### Security
The host side binds **loopback only**`127.0.0.1` and `[::1]`, never a wildcard address. Nothing
on your network can reach a bridged port. Within your own machine, though, a bridged port is
reachable by any local process for as long as the in-container listener exists, and the services
behind it are unauthenticated: they bound loopback precisely because they expected to be reachable
from nowhere else. Only container programs that bound loopback are bridged; anything listening on
all interfaces is deliberately ignored (publishing those is what port mappings are for).
Leave it off unless you need it, and it will not be running.
---
## AWS Bedrock Configuration ## AWS Bedrock Configuration
To use Claude via AWS Bedrock instead of Anthropic's API, switch the backend to **Bedrock** on the project card. To use Claude via AWS Bedrock instead of Anthropic's API, set **Backend** to **Bedrock** under
**Config → Model**.
### Authentication Methods ### Authentication Methods
| Method | Fields | Use Case | | Method | Fields | Use Case |
|--------|--------|----------| |--------|--------|----------|
| **Keys** | Access Key ID, Secret Access Key, Session Token (optional) | Direct credentials — simplest setup | | **Static keys** | Access Key ID, Secret Access Key, Session Token (optional) | Direct credentials — simplest setup |
| **Profile** | AWS Profile name | Uses `~/.aws/config` and `~/.aws/credentials` on the host | | **Named profile** | AWS Profile name | Uses `~/.aws/config` and `~/.aws/credentials` on the host |
| **Token** | Bearer Token | Temporary bearer token authentication | | **Bearer token** | Bearer Token | Temporary bearer token authentication |
With **Named profile**, the SSO session is validated before Claude Code launches, so an expired
session is caught at the start of a terminal rather than mid-task.
### Additional Bedrock Settings ### Additional Bedrock Settings
- **AWS Region** — Required. The region where your Bedrock models are deployed (e.g., `us-east-1`). - **AWS Region** — Required. The region where your Bedrock models are deployed (e.g., `us-east-1`).
- **Model ID** — Optional. Override the default Claude model (e.g., `anthropic.claude-sonnet-4-20250514-v1:0`). - **Model ID** — Optional. Override the default Claude model (e.g., `anthropic.claude-sonnet-4-20250514-v1:0`).
- **Service tier** — Optional. Selects a Bedrock service tier.
### Global AWS Defaults ### Global AWS Defaults
@@ -330,7 +601,7 @@ Per-project settings always override these global defaults.
## Ollama Configuration ## Ollama Configuration
To use Claude Code with a local or remote Ollama server, switch the backend to **Ollama** on the project card. To use Claude Code with a local or remote Ollama server, set **Backend** to **Ollama** under **Config → Model**.
### Settings ### Settings
@@ -349,7 +620,7 @@ Triple-C sets `ANTHROPIC_BASE_URL` to point Claude Code at your Ollama server in
## OpenAI Compatible Configuration ## OpenAI Compatible Configuration
To use Claude Code through any OpenAI API-compatible endpoint, switch the backend to **OpenAI Compatible** on the project card. This works with any server that exposes an OpenAI-compatible API, including LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, and others. To use Claude Code through any OpenAI API-compatible endpoint, set **Backend** to **OpenAI Compatible** under **Config → Model**. This works with any server that exposes an OpenAI-compatible API, including LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, and others.
### Settings ### Settings
@@ -367,7 +638,14 @@ Triple-C sets `ANTHROPIC_BASE_URL` to point Claude Code at your OpenAI-compatibl
## Settings ## Settings
Access global settings via the **Settings** tab in the sidebar. Access global settings via the **Settings** tab in the sidebar. The panel is a set of collapsible
sections: **General**, **Claude Authentication**, **Backends**, **Container**, **Git / SSH**,
**Tools** and **Updates**.
### Claude Authentication
Acquire or revoke the shared Claude authentication token — see
[Shared Claude Authentication](#shared-claude-authentication).
### Docker Settings ### Docker Settings
@@ -462,7 +740,14 @@ The web terminal UI mirrors the desktop app's terminal experience:
### Multiple Sessions ### 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. Tabs show the project name (or custom session name if provided), with a "(bash)" suffix for shell sessions. You can open multiple terminal sessions (even for the same project). Each session gets its own tab
in the main tab strip, alongside any open Project Home tabs. Click a tab to switch, or click the
**×** on a tab to close it. Tabs show the project name (or a custom session name if you set one),
with a "(bash)" suffix for shell sessions and a badge for the permission mode the session was
launched with.
Right-click a terminal tab for **Rename tab**, **Reset name**, **Open project home** and
**Close tab**; double-click it to rename inline.
### Bash Shell Sessions ### Bash Shell Sessions
@@ -490,16 +775,16 @@ You can paste images from your clipboard into the terminal (Ctrl+V / Cmd+V). The
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. 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 ### Files
Click the **Files** button on a running project to open the file manager modal. You can: The **Files** tab of Project Home browses inside a running container. You can:
- **Browse** the container filesystem starting from `/workspace`, with breadcrumb navigation - **Browse** the container filesystem, starting at `/workspace`, with breadcrumb navigation
- **Download** any file to your host machine via the download button on each file entry - **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 - **Upload file** from your host into the current container directory
- **Refresh** the directory listing at any time - **Refresh** the directory listing at any time
The file manager shows file names, sizes, and modification dates. The listing shows file names, sizes, and modification dates.
### Terminal Rendering ### Terminal Rendering
@@ -507,9 +792,48 @@ The terminal uses WebGL for hardware-accelerated rendering of the active tab. In
--- ---
## Scheduled Tasks (Inside the Container) ## Automation & Scheduled Tasks
Once inside a running container terminal, you can set up recurring or one-time tasks using `triple-c-scheduler`. Tasks run as separate Claude Code sessions. Each container can run Claude Code on a schedule — recurring or one-time — through a small
scheduler called `triple-c-scheduler` that lives inside the image. Tasks run as separate,
headless Claude Code invocations (`claude -p "<your prompt>"`) driven by cron.
The **Automation** tab in Project Home is the place to watch and control them; tasks are created
from inside the container with the `triple-c-scheduler` CLI.
### The Automation Tab
With the container running, the Automation tab lists every task the scheduler knows about. For each
one you get its name, whether it is recurring or one-time, its cron expression or scheduled time,
and when it last ran. For each task you can:
| Control | What it does |
|---------|--------------|
| **Toggle** | Enable or disable the task without deleting it |
| **Run now** | Trigger the task immediately, outside its schedule |
| **Log** | Show the tail of that task's run log (last 200 lines) |
| **Remove** | Delete the task, after a confirmation |
**Refresh** re-reads everything from the container.
When tasks finish they leave **notifications**. If any are waiting, a panel appears at the top of
the tab listing each one with its task name, whether it succeeded or failed, how long ago it ran
and a summary. **Clear all** dismisses them. The Overview tab also shows a notification count and
the next few scheduled tasks.
### Permission mode
Scheduled runs use the project's [permission mode](#permission-modes) — they no longer always run
with `--dangerously-skip-permissions`. Because the mode travels into the container as an
environment variable, **stop and start the project** after changing it for the scheduler to see the
change. Remember that a headless run cannot answer a permission prompt, so in any mode other than
**Bypass** a task may stop early when Claude Code asks for approval; the run log records the mode
that was used.
### Creating Tasks (In the Container)
There is no "add task" form in the app. Create tasks from a terminal in the container — either type
the commands yourself in a **Shell** session, or just ask Claude to do it.
### Create a Recurring Task ### Create a Recurring Task
@@ -525,7 +849,9 @@ triple-c-scheduler add --name "migrate-db" --at "2026-03-05 14:00" --prompt "Run
One-time tasks automatically remove themselves after execution. One-time tasks automatically remove themselves after execution.
### Manage Tasks ### Manage Tasks From the CLI
The CLI still works, and does the same things the Automation tab does:
```bash ```bash
triple-c-scheduler list # List all tasks triple-c-scheduler list # List all tasks
@@ -558,6 +884,39 @@ By default, tasks run in `/workspace`. Use `--working-dir` to specify a differen
triple-c-scheduler add --name "test" --schedule "0 */6 * * *" --prompt "Run tests" --working-dir /workspace/my-project triple-c-scheduler add --name "test" --schedule "0 */6 * * *" --prompt "Run tests" --working-dir /workspace/my-project
``` ```
> Scheduled tasks live on the project's config volume, so a **Reset** deletes them along with
> everything else on that volume.
---
## Keyboard Shortcuts
### Application
| Shortcut | Action |
|----------|--------|
| **Ctrl+T** | Open a new Claude terminal for the current project (nothing happens unless its container is running) |
| **Ctrl+Shift+W** | Close the active tab |
| **Ctrl+Tab** | Switch to the next tab |
| **Ctrl+Shift+Tab** | Switch to the previous tab |
| **Ctrl+1****Ctrl+9** | Jump to the first through ninth tab |
> **Why Ctrl+Shift+W and not Ctrl+W?** `Ctrl+W` is readline's `kill-word` — it deletes the word
> before the cursor, and it is used constantly in the terminal this app is built around. Binding it
> to "close tab" would make the shell unusable, so Triple-C deliberately leaves `Ctrl+W` alone.
### In the Terminal
| Shortcut | Action |
|----------|--------|
| **Ctrl+Shift+C** | Copy the selection, with trailing whitespace trimmed |
| **Ctrl+Shift+Alt+C** | Copy the selection exactly as-is |
| **Ctrl+Shift+V** | Paste |
| **Ctrl+V** | Paste an image from the clipboard into the container |
| **Ctrl+Shift+M** | Toggle speech-to-text recording (when enabled) |
Everything else goes straight through to the program running in the container.
--- ---
## What's Inside the Container ## What's Inside the Container
@@ -623,7 +982,7 @@ These features are built into Claude Code and work inside Triple-C containers wi
- Check that the Docker image is "Ready" in Settings. - Check that the Docker image is "Ready" in Settings.
- Verify that the mounted folder paths exist on your host. - Verify that the mounted folder paths exist on your host.
- Look at the error message displayed in the progress modal. - Read the error toast — the full message is behind its **Details** disclosure.
### OAuth Login URL Not Opening ### OAuth Login URL Not Opening
@@ -631,10 +990,33 @@ These features are built into Claude Code and work inside Triple-C containers wi
- If the toast doesn't appear, try scrolling up in the terminal — the URL may have already been printed. - If the toast doesn't appear, try scrolling up in the terminal — the URL may have already been printed.
- You can also manually copy the URL from the terminal output and paste it into your browser. - You can also manually copy the URL from the terminal output and paste it into your browser.
### A Browser Login Never Completes
You opened the URL, signed in successfully, and the CLI in the terminal is still waiting. The
callback from your browser is landing on your host's `localhost` while the CLI is listening on the
*container's*. Enable the
[Auth Bridge](#browser-logins-inside-the-container-auth-bridge) for that project and try again.
For Claude specifically, the simpler answer is usually
[Shared Claude Authentication](#shared-claude-authentication), which finishes on an Anthropic-hosted
page and needs no callback at all.
### A Scheduled Task Stopped Part-Way Through
Scheduled tasks run headless and cannot answer a permission prompt. If the project is not in
**Bypass** mode, a task will stop when Claude Code asks for approval. Check the task's **Log** in
the Automation tab — it records the permission mode the run used.
### A Permission Mode Change Didn't Apply
- **In a terminal:** the mode is set when the terminal opens. Close the tab and open a new one.
- **For scheduled tasks:** the mode reaches the scheduler through the container's environment. Stop
the project and start it again.
### File Permission Issues ### File Permission Issues
- Triple-C automatically remaps the container user's UID/GID to match your host user, so files created inside the container should have the correct ownership on your host. - Triple-C automatically remaps the container user's UID/GID to match your host user, so files created inside the container should have the correct ownership on your host.
- If you see permission errors, try resetting the container (stop, then click **Reset**). - If you see permission errors, try resetting the container: stop it, then choose **Reset container** from the **⋯** menu in the Project Home header. Note that this wipes `~/.claude`.
### Settings Won't Save ### Settings Won't Save
+172 -31
View File
@@ -1,6 +1,6 @@
# Triple-C (Claude-Code-Container) # Triple-C (Claude-Code-Container)
Triple-C is a cross-platform desktop application that sandboxes Claude Code inside Docker containers. Each project can optionally enable full permissions mode (`--dangerously-skip-permissions`), giving Claude unrestricted access within the sandbox. Triple-C is a cross-platform desktop application that sandboxes Claude Code inside Docker containers. Each project chooses its own **permission mode** — from Plan (read-only) through to Bypass (`--dangerously-skip-permissions`), which gives Claude unrestricted access within the sandbox.
## Architecture ## Architecture
@@ -13,41 +13,160 @@ Triple-C is a cross-platform desktop application that sandboxes Claude Code insi
``` ```
┌─────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ TopBar (terminal tabs + Docker/Image status) │ TopBar (MainTabs strip + Docker/Image status + ?)
├────────────┬────────────────────────────────────────┤ ├────────────┬────────────────────────────────────────┤
│ Sidebar │ Main Content (terminal views) │ Sidebar │ Main Content
│ (25% w, │ │ (25% w, │ · Project Home views, or
│ responsive│ │ responsive│ · terminal views (xterm.js)
│ min/max) │ │ │ min/max) │ │
├────────────┴────────────────────────────────────────┤ ├────────────┴────────────────────────────────────────┤
│ StatusBar (project/terminal counts) │ StatusBar (project/terminal counts, STT, scroll)
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
``` ```
The main area is driven by **one ordered tab strip** (`components/layout/MainTabs.tsx`) holding
two tab kinds: `home:<projectId>` (Project Home) and `term:<sessionId>` (a terminal). There is no
separate terminal tab bar. `activeSessionId` is derived from the active tab key, so exactly one
thing is current at a time.
### Keyboard Shortcuts
Implemented in `hooks/useKeyboardShortcuts.ts` (document-level, capture phase):
| Shortcut | Action |
|---|---|
| `Ctrl+T` | New Claude terminal for the current project (no-op unless it is running) |
| `Ctrl+Shift+W` | Close the active tab |
| `Ctrl+Tab` / `Ctrl+Shift+Tab` | Cycle tabs forward / backward |
| `Ctrl+1``Ctrl+9` | Jump to the nth tab |
`Ctrl+W` is deliberately **not** bound: it is readline's `kill-word`, used constantly in the
terminal this app is built around. Terminal-scoped keys (`Ctrl+Shift+C`, `Ctrl+Shift+Alt+C`,
`Ctrl+Shift+M`) are handled in `TerminalView.tsx`.
### Project Home
Clicking a project row in the sidebar opens **Project Home** in the main area — the per-project
view, with tabs **Overview · Sessions · Automation · Config · Files**. The sidebar row itself is
select-only (plus hover controls for start/stop and opening a terminal); it holds no configuration.
Per-project configuration lives in the Config tab rather than in modals.
| Tab | Contents |
|---|---|
| **Overview** | Permission mode control, sandbox/backend/Docker-access summary, capability tiles, recent sessions, scheduled tasks |
| **Sessions** | Past Claude Code conversations read from the config volume, with **Resume** |
| **Automation** | The container's `triple-c-scheduler` tasks — enable/disable, run now, read logs, remove, and completion notifications |
| **Config** | Workspace (name, folders), Model (backend), Access (SSH, git, env vars, port mappings), Runtime (permission mode, sandbox, Docker access, Mission Control, instructions, Claude Code settings) |
| **Files** | Browse, download and upload files inside the container |
Container start/stop progress is reported inline (on the sidebar row and in the Project Home
header) via the `container-progress` event, and failures surface as toasts. There is no blocking
progress modal.
### Permission Modes
`PermissionMode` in `models/project.rs` replaces the old `full_permissions` boolean. Four states,
mapped to CLI flags by `PermissionMode::cli_args()`:
| Mode | Serialized | CLI args passed to `claude` |
|---|---|---|
| **Plan** | `plan` | `--permission-mode plan` |
| **Default** | `default` | *(none)* |
| **Accept Edits** | `acceptEdits` | `--permission-mode acceptEdits` |
| **Bypass** | `bypass` | `--dangerously-skip-permissions` |
`Project.permission_mode` is `Option<PermissionMode>`; `effective_permission_mode()` falls back to
the legacy `full_permissions` flag (`true` → Bypass) for records written before the change. Changing
the mode affects terminals opened **from then on** — a running `claude` process keeps the argv it
was launched with.
Scheduled tasks honour it too. The mode is injected as `TRIPLE_C_PERMISSION_MODE` (via
`as_env_value()`) and written as the `triple-c.permission-mode` container label; the entrypoint
snapshots it into `~/.claude/scheduler/.env`, and `container/triple-c-task-runner` translates it
back into flags for its headless `claude -p` run. Because it travels as container env, a mode change
only reaches the scheduler after the container is recreated on its next start (the label mismatch
forces that).
### Container Introspection (Capability Tiles)
`list_container_capabilities` (`commands/inspect_commands.rs`) runs a read-only `find`/`jq` script
inside a running container and returns counts plus item lists for **skills, agents, commands, hooks,
plugins and MCP servers**, at user scope (`/home/claude/.claude`) and project scope
(`/workspace/*/.claude`, `/workspace/*/.mcp.json`). Overview renders these as tiles.
Triple-C does not create or edit any of them — Claude Code owns that configuration, and the tiles
link out to a terminal where `/agents`, `/hooks`, `/plugins` and `/mcp` do the real work.
### Auth Bridge
Browser-based logins run *inside* a container (`claude login`, `aws sso login`, Concourse
`fly login`) start an ephemeral HTTP listener on the container's loopback and expect the host
browser's redirect to reach it. `auth_bridge/` closes that gap:
- Listeners are discovered by parsing `/proc/net/tcp{,6}` every 2 seconds — the image ships no
`ss`, `netstat` or `lsof`. Only `TCP_LISTEN` rows bound to loopback are considered; wildcard
binds are deliberately ignored (that is the port-mappings feature's job).
- Each discovered port is bound on the host at **the same port number**, on `127.0.0.1` (required)
and `[::1]` (best effort) — never a wildcard address. Node resolves `localhost` to IPv6 first, so
`claude login` often binds `::1` alone; the bridge follows the family it actually finds.
- Traffic is carried in over the Docker API by an attached exec running `socat`, because container
IPs are not routable from the host on Docker Desktop.
- Ports already covered by the project's port mappings are skipped, and a host port that is already
in use is reported as a conflict rather than fought over.
Opt-in per project (`auth_bridge_enabled`, default `false`), purely host-side, so toggling it never
recreates the container. The poller stops on its own when the container stops.
**Security posture:** the host side binds loopback only. Everything reachable through it is an
unauthenticated service inside the container, so widening those addresses would publish container
internals to the LAN. Nothing else on the network can reach a bridged port.
### Shared Claude Authentication Token
Rather than running `claude login` in every container, `claude setup-token` can be run once
(`commands/auth_token_commands.rs`). The flow borrows a running container, runs the CLI on a PTY,
and the long-lived token it prints is stored in the OS keychain — it is never returned to the
frontend and never logged. Streamed output passes through a chunk-boundary-safe redactor that masks
anything resembling an `sk-ant-` secret.
The token is injected as `CLAUDE_CODE_OAUTH_TOKEN` into every project where the backend is
Anthropic, the project has not opted out (`use_shared_auth_token`, default `true`), and a token is
actually stored. It is a reserved env key, so it cannot be hand-set as a custom variable.
Rotation is tracked with a random id (not a hash of the token) mirrored into the
`triple-c.claude-token-version` label — a hash in a `docker inspect`-readable label would be an
offline verification oracle. Acquiring, rotating, revoking or opting out changes that label, which
forces a container recreation on the next start; that is when a container picks the token up or has
it cleared.
### Container Lifecycle ### Container Lifecycle
1. **Create**: New container created with bind mounts, env vars, and labels 1. **Create**: New container created with bind mounts, named volumes, env vars, and labels
2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group, injects Claude Code settings 2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group, injects Claude Code settings, rebuilds the scheduler crontab
3. **Terminal**: `docker exec` launches Claude Code (or bash shell) with a PTY 3. **Terminal**: `docker exec` launches Claude Code (with the project's permission-mode flags) or a bash login shell, with a PTY
4. **Stop**: Container halted (filesystem persists in named volume) 4. **Stop**: Container halted (its filesystem layer and both named volumes persist)
5. **Restart**: Existing container restarted; recreated if settings changed (detected via SHA-256 fingerprint) 5. **Restart**: Existing container restarted; if any `triple-c.*` label no longer matches the project's settings, the container is committed to a snapshot image, removed, and recreated from that snapshot — so installed packages survive
6. **Reset**: Container removed and recreated from scratch (named volume preserved) 6. **Reset**: Container, snapshot image **and both named volumes** all removed, then recreated from the clean base image. `remove_project_volumes` deletes `triple-c-home-{projectId}` and `triple-c-claude-config-{projectId}`, so `~/.claude`, `~/.claude.json`, the OAuth login, installed skills, session transcripts and the scheduler's tasks are all lost.
### Mounts ### Mounts
| Target in Container | Source | Type | Notes | | Target in Container | Source | Type | Notes |
|---|---|---|---| |---|---|---|---|
| `/workspace` | Project directory | Bind | Read-write | | `/workspace/<mount-name>` | Each configured project folder | Bind | Read-write; one per folder |
| `/home/claude/.claude` | `triple-c-claude-config-{projectId}` | Named Volume | Persists across container recreation | | `/home/claude` | `triple-c-home-{projectId}` | Named Volume | Home directory; survives stop/start and recreation |
| `/home/claude/.claude` | `triple-c-claude-config-{projectId}` | Named Volume | Nested inside the home volume; Docker gives the more specific mount precedence |
| `/tmp/.host-ssh` | SSH key directory | Bind | Read-only; entrypoint copies to `~/.ssh` | | `/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 | | `/home/claude/.aws` | AWS config directory | Bind | Read-only; for Bedrock auth |
| `/var/run/docker.sock` | Host Docker socket | Bind | If "Allow container spawning" is ON | | `/var/run/docker.sock` | Host Docker socket | Bind | If "Allow container spawning" is ON |
These two named volumes are the only ones a project owns. Both are removed by Reset and by project
removal, and by nothing else.
### Authentication Modes ### Authentication Modes
Each project can independently use one of: 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. - **Anthropic** (OAuth or shared token): either the shared `claude setup-token` token injected as `CLAUDE_CODE_OAUTH_TOKEN` (see below), or a per-container `claude login`. An interactive login's token lives in the config volume and survives container stop/start and recreation — but **not** a Reset, which deletes the volumes.
- **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token). SSO sessions are validated before launching Claude for Profile auth. - **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token). SSO sessions are validated before launching Claude for Profile auth.
- **Ollama**: Connect to a local or remote Ollama server via `ANTHROPIC_BASE_URL` (e.g., `http://host.docker.internal:11434`). Requires a model ID, and the model must be pulled (or used via Ollama cloud) before starting the container. - **Ollama**: Connect to a local or remote Ollama server via `ANTHROPIC_BASE_URL` (e.g., `http://host.docker.internal:11434`). Requires a model ID, and the model must be pulled (or used via Ollama cloud) before starting the container.
- **OpenAI Compatible**: Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.) via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`. API key stored securely in OS keychain. - **OpenAI Compatible**: Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.) via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`. API key stored securely in OS keychain.
@@ -78,7 +197,7 @@ The web terminal shares the existing `ExecSessionManager` via `Arc`-wrapped stor
### Speech-to-Text (Voice Mode) ### Speech-to-Text (Voice Mode)
Triple-C includes optional speech-to-text powered by [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) running in a separate Docker container. When enabled, a microphone button appears in the bottom-left corner of each terminal view. Triple-C includes optional speech-to-text powered by [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) running in a separate Docker container. When enabled, a microphone button appears in the StatusBar whenever a terminal session is active.
- **Hotkey**: `Ctrl+Shift+M` to toggle recording - **Hotkey**: `Ctrl+Shift+M` to toggle recording
- **Models**: `tiny`, `small`, or `medium` (configurable in Settings) - **Models**: `tiny`, `small`, or `medium` (configurable in Settings)
@@ -101,31 +220,49 @@ Users can override this in Settings via the global `docker_socket_path` option.
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `app/src/App.tsx` | Root layout (TopBar + Sidebar + Main + StatusBar) | | `app/src/App.tsx` | Root layout (TopBar + Sidebar + Main + StatusBar + ToastHost) |
| `app/src/index.css` | Global CSS variables, dark theme, `color-scheme: dark` | | `app/src/index.css` | Global CSS variables, dark theme, `color-scheme: dark`, `:focus-visible` ring |
| `app/src/components/layout/TopBar.tsx` | Terminal tabs + Docker/Image status indicators | | `app/src/components/layout/TopBar.tsx` | Hosts MainTabs + Docker/Image status indicators + Help |
| `app/src/components/layout/Sidebar.tsx` | Responsive sidebar (25% width, min 224px, max 320px) | | `app/src/components/layout/MainTabs.tsx` | The single main-area tab strip (Project Home + terminal tabs) |
| `app/src/components/layout/StatusBar.tsx` | Running project/terminal counts | | `app/src/components/layout/Sidebar.tsx` | Responsive sidebar (25% width, min 224px, max 320px), collapsible to an icon rail |
| `app/src/components/projects/ProjectCard.tsx` | Project config, backend selector, action buttons | | `app/src/components/layout/StatusBar.tsx` | Project/terminal counts, Jump to Current, STT mic |
| `app/src/components/projects/ClaudeCodeSettingsModal.tsx` | Claude Code CLI settings modal (TUI mode, effort, focus, caching) | | `app/src/components/projects/ProjectRow.tsx` | Select-only sidebar row; opens Project Home, with hover start/stop and terminal controls |
| `app/src/components/projects/ProjectList.tsx` | Project list in sidebar | | `app/src/components/projects/ProjectList.tsx` | Project list in sidebar |
| `app/src/components/projects/FileManagerModal.tsx` | File browser modal (browse, download, upload) | | `app/src/components/projects/PermissionModeControl.tsx` | Plan / Default / Accept Edits / Bypass segmented control |
| `app/src/components/projects/ContainerProgressModal.tsx` | Real-time container operation progress | | `app/src/components/projects/home/ProjectHome.tsx` | Project Home shell: header actions, overflow menu, tab strip |
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, web terminal, and global settings | | `app/src/components/projects/home/OverviewTab.tsx` | Permission mode, summary, capability tiles, recent sessions and tasks |
| `app/src/components/projects/home/SessionsTab.tsx` | Past Claude sessions with Resume |
| `app/src/components/projects/home/AutomationTab.tsx` | Scheduler tasks: toggle, run now, logs, remove, notifications |
| `app/src/components/projects/home/ConfigTab.tsx` | Config sections (Workspace, Model, Access, Runtime) |
| `app/src/components/projects/home/FilesTab.tsx` | File browser (browse, download, upload) |
| `app/src/components/projects/home/CapabilityTiles.tsx` | Read-only skills/agents/commands/hooks/plugins/MCP counts |
| `app/src/components/projects/ClaudeCodeSettingsEditor.tsx` | Claude Code CLI settings (TUI mode, effort, focus, caching) |
| `app/src/components/ui/` | Shared primitives: `Modal`, `Button`, `Toggle`, `Field`, `SegmentedControl`, `StatusIndicator`, `SaveIndicator`, `OverflowMenu`, `ToastHost`, `Tooltip` |
| `app/src/hooks/useKeyboardShortcuts.ts` | `Ctrl+T`, `Ctrl+Shift+W`, `Ctrl+Tab`, `Ctrl+1..9` |
| `app/src/hooks/useContainerProgress.ts` | `container-progress` event → inline progress lines |
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, web terminal, shared auth, and global settings |
| `app/src/components/settings/SharedAuthSettings.tsx` | Acquire / revoke the shared Claude authentication token |
| `app/src/components/settings/WebTerminalSettings.tsx` | Web terminal toggle, URL, token management | | `app/src/components/settings/WebTerminalSettings.tsx` | Web terminal toggle, URL, token management |
| `app/src/components/settings/SttSettings.tsx` | STT settings panel (model, port, language, container controls) | | `app/src/components/settings/SttSettings.tsx` | STT settings panel (model, port, language, container controls) |
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste | | `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste |
| `app/src/components/terminal/SttButton.tsx` | Mic button overlay with on-demand container start | | `app/src/components/terminal/SttButton.tsx` | Mic button with on-demand STT container start |
| `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/useTerminal.ts` | Terminal session management (claude and bash modes) |
| `app/src/hooks/useProjectActions.ts` | Start/stop/reset/backup and terminal-opening helpers |
| `app/src/hooks/useFileManager.ts` | File manager operations (list, download, upload) | | `app/src/hooks/useFileManager.ts` | File manager operations (list, download, upload) |
| `app/src/hooks/useClaudeAuth.ts` | Shared-token status and acquisition |
| `app/src/hooks/useSTT.ts` | Speech-to-text recording, transcription, and container management | | `app/src/hooks/useSTT.ts` | Speech-to-text recording, transcription, and container management |
| `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, fingerprinting | | `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, labels, recreation checks, `remove_project_volumes` |
| `app/src-tauri/src/docker/exec.rs` | PTY exec sessions, file upload/download via tar | | `app/src-tauri/src/docker/exec.rs` | `create_attached_exec()` — the single attached-exec path; file upload/download via tar |
| `app/src-tauri/src/docker/image.rs` | Image building/pulling | | `app/src-tauri/src/docker/image.rs` | Image building/pulling |
| `app/src-tauri/src/docker/stt.rs` | Speech-to-text container lifecycle |
| `app/src-tauri/src/docker/legacy_cleanup.rs` | One-release migration shim removing leftovers from the deleted MCP feature |
| `app/src-tauri/src/auth_bridge/` | Loopback callback bridge (`mod.rs`, `proc_net.rs`, `tunnel.rs`) |
| `app/src-tauri/src/commands/project_commands.rs` | Start/stop/rebuild Tauri command handlers | | `app/src-tauri/src/commands/project_commands.rs` | Start/stop/rebuild Tauri command handlers |
| `app/src-tauri/src/commands/inspect_commands.rs` | Read-only container views: sessions, capabilities, scheduler tasks |
| `app/src-tauri/src/commands/auth_token_commands.rs` | `claude setup-token` flow, redaction, keychain storage |
| `app/src-tauri/src/commands/auth_bridge_commands.rs` | Auth bridge enable/status commands |
| `app/src-tauri/src/commands/file_commands.rs` | File manager Tauri commands (list, download, upload) | | `app/src-tauri/src/commands/file_commands.rs` | File manager Tauri commands (list, download, upload) |
| `app/src-tauri/src/models/project.rs` | Project struct (backend, Docker access, Claude Code settings, Mission Control) | | `app/src-tauri/src/models/project.rs` | Project struct (backend, `PermissionMode`, Docker access, Claude Code settings, Mission Control, auth bridge, shared-token opt-out) |
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, Claude Code settings, web terminal, STT) | | `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, Claude Code settings, web terminal, STT) |
| `app/src-tauri/src/web_terminal/server.rs` | Axum HTTP+WS server for remote terminal access | | `app/src-tauri/src/web_terminal/server.rs` | Axum HTTP+WS server for remote terminal access |
| `app/src-tauri/src/web_terminal/ws_handler.rs` | WebSocket connection handler and session management | | `app/src-tauri/src/web_terminal/ws_handler.rs` | WebSocket connection handler and session management |
@@ -140,6 +277,10 @@ Users can override this in Settings via the global `docker_socket_path` option.
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, Claude Code settings injection, Mission Control setup | | `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, Claude Code settings injection, Mission Control setup |
| `container/osc52-clipboard` | Clipboard shim (xclip/xsel/pbcopy via OSC 52) | | `container/osc52-clipboard` | Clipboard shim (xclip/xsel/pbcopy via OSC 52) |
| `container/audio-shim` | Audio capture shim (rec/arecord via FIFO) for voice mode | | `container/audio-shim` | Audio capture shim (rec/arecord via FIFO) for voice mode |
| `container/triple-c-scheduler` | Bash CLI managing scheduled task JSON and the crontab |
| `container/triple-c-task-runner` | Cron entry point; maps `TRIPLE_C_PERMISSION_MODE` to flags and runs `claude -p` |
| `container/triple-c-sso-refresh` | AWS SSO session refresh helper |
| `app/src-tauri/src/storage/secure.rs` | OS keychain access (per-project secrets, shared token, rotation id) |
## CSS / Styling Notes ## CSS / Styling Notes
+240 -42
View File
@@ -2,7 +2,7 @@
## Overview ## Overview
Triple-C (Claude-Code-Container) sandboxes Claude Code inside Docker containers so that when running with `--dangerously-skip-permissions`, Claude only has access to files and projects you explicitly provide. The project consists of two components: a **Docker container image** pre-loaded with development tools, and a **cross-platform desktop application** for managing project containers, terminal sessions, and authentication. Triple-C (Claude-Code-Container) sandboxes Claude Code inside Docker containers so that even in its most permissive mode — `--dangerously-skip-permissions` Claude only has access to files and projects you explicitly provide. The project consists of two components: a **Docker container image** pre-loaded with development tools, and a **cross-platform desktop application** for managing project containers, terminal sessions, and authentication.
--- ---
@@ -123,7 +123,8 @@ Implementation gotchas for the terminal view and its global controls (merged in
┌──────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────┐
│ Docker Container (per project) │ │ Docker Container (per project) │
│ │ │ │
│ /workspace ←─ bind mount ─► Host project directory │ /workspace/<name> ←─ bind mount ─► Host project folder
│ /home/claude ←── named volume (home dir) │
│ /home/claude/.claude ←── named volume (persists config) │ │ /home/claude/.claude ←── named volume (persists config) │
│ /tmp/.host-ssh ←── read-only bind mount (SSH keys) │ │ /tmp/.host-ssh ←── read-only bind mount (SSH keys) │
│ /var/run/docker.sock ←── optional (sibling containers) │ │ /var/run/docker.sock ←── optional (sibling containers) │
@@ -160,22 +161,143 @@ Terminal resize follows the same pattern: `ResizeObserver` detects container siz
Containers follow a **stop/start** model, not create/destroy: Containers follow a **stop/start** model, not create/destroy:
1. **First start**: A new container is created with bind mounts, environment variables, and labels. The entrypoint remaps UID/GID, configures SSH and git, then runs `sleep infinity` to keep the container alive. 1. **First start**: A new container is created with bind mounts, named volumes, environment variables, and labels. The entrypoint remaps UID/GID, configures SSH and git, rebuilds the scheduler crontab, then runs `sleep infinity` to keep the container alive.
2. **Terminal open**: `docker exec` launches `claude --dangerously-skip-permissions` with a PTY in the running container. 2. **Terminal open**: `docker exec` launches `claude` with a PTY in the running container, with the permission-mode flags from `PermissionMode::cli_args()` (or `bash -l` for a shell session).
3. **Stop**: `docker stop` halts the container but preserves its filesystem. Any packages Claude installed via `apt`, `pip`, `cargo`, etc. survive. 3. **Stop**: `docker stop` halts the container but preserves its filesystem. Any packages Claude installed via `apt`, `pip`, `cargo`, etc. survive.
4. **Restart**: `docker start` resumes the existing container. All installed tools and configuration persist. 4. **Restart**: `docker start` resumes the existing container — unless `container_needs_recreation()` finds a `triple-c.*` label that no longer matches the project's settings, in which case the container is committed to a snapshot image (`triple-c-snapshot-{projectId}:latest`), removed, and recreated from that snapshot. Installed tools survive; the named volumes are untouched.
5. **Reset**: The container is removed and recreated from the image. This is a clean slate — the nuclear option when the container state is corrupted. 5. **Reset**: `rebuild_project_container` closes live exec sessions, removes the container, removes the snapshot image, calls `remove_project_volumes` to delete **both** named volumes, then starts fresh from the clean base image.
The `.claude` configuration directory uses a **named Docker volume** (`triple-c-claude-config-{projectId}`) so OAuth tokens from `claude login` persist even across container resets. Two named volumes exist per project and they are the only ones it owns:
| Volume | Mount point | Purpose |
|---|---|---|
| `triple-c-home-{projectId}` | `/home/claude` | Home directory — `~/.claude.json`, `~/.local`, `~/.ssh`, `~/.aws` |
| `triple-c-claude-config-{projectId}` | `/home/claude/.claude` | Claude Code config: OAuth credential, settings, skills/agents/commands, session transcripts, scheduler state. Nested inside the home volume; Docker gives the more specific mount precedence. |
`remove_project_volumes` names those two volumes explicitly (no prefix sweep) and is called from
exactly two places: `remove_project` and `rebuild_project_container`. Ordinary container removal
passes `v: false`, so stop/start and recreation never touch the volumes — **only Reset and project
removal delete them.** A Reset therefore destroys the `claude login` credential, installed skills,
session transcripts and scheduled tasks; it does not touch host bind mounts, the project record, or
host keychain secrets.
### Permission Modes
`PermissionMode` (`models/project.rs`) is a four-state enum replacing the earlier `full_permissions`
boolean. It reaches Claude Code by two different routes:
| Mode | `cli_args()` — interactive terminals | `as_env_value()` — scheduler |
|---|---|---|
| `Plan` | `--permission-mode plan` | `plan` |
| `Default` | *(no flag)* | `default` |
| `AcceptEdits` | `--permission-mode acceptEdits` | `acceptEdits` |
| `Bypass` | `--dangerously-skip-permissions` | `bypass` |
`Project.permission_mode` is `Option<PermissionMode>`, and `effective_permission_mode()` resolves
`None` from the legacy `full_permissions` flag, so records written before the change keep behaving
the same way.
**Interactive path.** `build_terminal_cmd()` evaluates `cli_args()` when a session is created, so
the flags are fixed for the life of that `claude` process. Changing the mode affects terminals
opened afterwards, not running ones. The same applies to `resume_session_command`, which builds
`claude <flags> --resume <id>` server-side.
**Scheduler path.** Cron jobs run with a minimal environment, so the mode travels as
`TRIPLE_C_PERMISSION_MODE` in the container's env; the entrypoint snapshots the allowlisted
variables into `~/.claude/scheduler/.env`, and `triple-c-task-runner` sources that file and maps the
value back to flags for its `claude -p` run. Container env can only change at create time, so
`container_needs_recreation()` compares a `triple-c.permission-mode` label and forces a recreation
on the next start. A mode change therefore reaches new terminals immediately but the scheduler only
after a stop/start. `TRIPLE_C_PERMISSION_MODE` is a reserved env key so it cannot be hand-set.
### Authentication Modes ### Authentication Modes
Each project independently chooses one of two authentication methods: Each project independently chooses one backend:
| Mode | How It Works | When to Use | | Backend | How It Works | When to Use |
|------|-------------|-------------| |------|-------------|-------------|
| **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 | | **Anthropic** | Either the shared `CLAUDE_CODE_OAUTH_TOKEN` injected from the OS keychain, or a per-container `claude login` whose credential persists in the `.claude` config volume. The OAuth URL opens in the host browser via URL detection. | 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 | | **AWS Bedrock** | Per-project AWS credentials (static keys, named profile, or bearer token) injected as env vars. `~/.aws` config optionally bind-mounted read-only; SSO sessions are validated before launching Claude for profile auth. | Enterprise environments using Bedrock |
| **Ollama** | `ANTHROPIC_BASE_URL` points at an Ollama server; `ANTHROPIC_AUTH_TOKEN` is set to a placeholder. | Local models (best-effort) |
| **OpenAI Compatible** | `ANTHROPIC_BASE_URL` plus `ANTHROPIC_AUTH_TOKEN` point at any OpenAI-compatible endpoint (LiteLLM, OpenRouter, vLLM, …). | Gateways and proxies (best-effort) |
### Shared Claude Authentication Token
`commands/auth_token_commands.rs` runs `claude setup-token` on a PTY inside a running container.
Contrary to the loopback pattern most CLI logins use, `setup-token` redirects to an Anthropic-hosted
page and then blocks on a stdin paste prompt, so the flow needs a way to feed the pasted code back
in — hence `submit_claude_token_code`. The flow is single-flight (the token is global, so two
concurrent logins would race to overwrite each other's keychain entry) and times out after 15
minutes.
- **Storage** — the OS keychain, under a dedicated service name; the token is never returned to the
frontend, never written to a log, and no command accepts or returns it.
- **Redaction** — streamed output is stripped of ANSI sequences and passed through a stateful
redactor that masks anything matching `sk-ant-` with a plausible body, withholding any tail that
could still grow into a secret across a chunk boundary.
- **Injection** — `CLAUDE_CODE_OAUTH_TOKEN` is set only when the backend is Anthropic, the project
has not opted out (`use_shared_auth_token`, default `true`), and a non-blank token is stored. When
those conditions do not hold, the variable is explicitly set to empty rather than omitted, so a
value baked into a snapshot image by `docker commit` is actively cleared.
- **Rotation** — a random UUID minted on each store is mirrored into the
`triple-c.claude-token-version` label. It is deliberately *not* a hash of the token: labels are
readable by anything that can run `docker inspect`, and a hash would be an offline verification
oracle. A label mismatch forces container recreation on the next start, which is when a container
picks up or loses the token.
### Auth Bridge
CLIs that log in through a browser (`claude login`, `aws sso login`, `fly login`) start an ephemeral
HTTP listener on an unpredictable loopback port and hand the provider a `http://localhost:<port>/…`
redirect. Run inside a container, that listener is unreachable from the host browser and nothing can
be pre-published at container-creation time. `auth_bridge/` bridges it at runtime:
- **Discovery** (`proc_net.rs`) — a `docker exec` reads `/proc/net/tcp` and `/proc/net/tcp6` every
two seconds. The image ships no `ss`, `netstat` or `lsof`. Only rows in state `0A` (`TCP_LISTEN`)
bound to loopback are kept; wildcard binds are ignored on purpose, since publishing those is the
port-mappings feature's job.
- **Family handling** — a `::1`-only listener genuinely cannot be reached over `127.0.0.1`, and Node
resolves `localhost` to IPv6 first on Linux, so `claude login` frequently binds `::1` alone. The
socat target follows the family actually observed; IPv4-mapped rows in `/proc/net/tcp6` are
treated as IPv4.
- **Host bind** (`tunnel.rs`) — the same port number is bound on the host: `127.0.0.1` is required,
`[::1]` is best-effort. **The host side binds loopback only, never a wildcard address**
everything behind it is an unauthenticated in-container service that bound loopback precisely
because it expected to be unreachable.
- **Transport** — each accepted connection is proxied by an attached exec running
`socat - TCP:127.0.0.1:<port>`, because container IPs are not routable from the host under Docker
Desktop. It goes through the same `create_attached_exec()` helper as terminal sessions, with
`tty: false` so socat's stderr is demultiplexed away from the proxied byte stream.
- **Policy** — ports appearing in the project's port mappings are skipped, and a host bind failure
is recorded as a conflict and retried later rather than fought over.
- **Lifecycle** — opt-in per project (`auth_bridge_enabled`, default `false`). It is purely
host-side, so it deliberately has no container-recreation label. The poller stops itself when the
project is gone, the flag is cleared, or the container is no longer running, and `stop()` awaits
it so host ports are provably released.
### Container Introspection
`list_container_capabilities` (`commands/inspect_commands.rs`) executes a read-only shell script in
a running container and returns counts and item lists for skills, agents, commands, hooks, plugins
and MCP servers, across user scope (`/home/claude/.claude`) and project scope
(`/workspace/*/.claude`, `/workspace/*/.mcp.json`). Everything is computed in-container with
`find`/`awk`/`jq`; only the JSON summary crosses the wire, and a stopped container yields zeros
rather than an error.
The script writes nothing. Claude Code owns this configuration and has its own tooling for it
(`/agents`, `/hooks`, `/plugins`, `/mcp`); Triple-C surfaces counts and opens a terminal rather than
rebuilding those editors as forms. `list_claude_sessions` and the scheduler commands
(`list_scheduled_tasks`, `get_scheduled_task_log`, `set_scheduled_task_enabled`,
`run_scheduled_task_now`, `remove_scheduled_task`, `clear_scheduler_notifications`) live in the same
module; the mutating ones shell out to `triple-c-scheduler` rather than editing its state files.
### Main-Area Tab Model
The frontend keeps a single ordered `tabOrder` array in the Zustand store holding two tab kinds,
`home:<projectId>` and `term:<sessionId>`, rendered by `components/layout/MainTabs.tsx`.
`activeSessionId` is *derived* from `activeTabKey`, so exactly one thing is current and a Project
Home tab and a terminal cannot both claim focus. Project configuration is a main-area view
(`components/projects/home/`), not a modal; the sidebar row is select-only.
### UID/GID Remapping ### UID/GID Remapping
@@ -205,10 +327,12 @@ This avoids the common Docker problem where bind-mount permissions can't be chan
| Data | Storage | Location | | Data | Storage | Location |
|------|---------|----------| |------|---------|----------|
| Project configurations | JSON file (atomic writes) | `~/.local/share/triple-c/projects.json` | | Project configurations | JSON file (atomic writes) | `~/.local/share/triple-c/projects.json` |
| API keys | OS keychain | macOS Keychain / Windows Credential Manager / Linux Secret Service | | API keys and per-project secrets | OS keychain | macOS Keychain / Windows Credential Manager / Linux Secret Service |
| Shared Claude token + rotation id | OS keychain | Separate service entries; never on disk, never in a label |
| App settings | Tauri plugin-store | App data directory | | App settings | Tauri plugin-store | App data directory |
| Claude config/tokens | Named Docker volume | `triple-c-claude-config-{projectId}` | | Claude config, sessions, scheduler state | Named Docker volume | `triple-c-claude-config-{projectId}` |
| Container filesystem | Docker container layer | Preserved across stop/start, cleared on reset | | Container home directory | Named Docker volume | `triple-c-home-{projectId}` |
| Container filesystem | Docker container layer, preserved into `triple-c-snapshot-{projectId}:latest` on recreation | Survives stop/start and recreation; destroyed by Reset |
The projects store uses **atomic writes** (write to `.json.tmp`, then `rename()`) to prevent data corruption if the app crashes mid-write. Corrupted files are backed up to `.json.bak` before being replaced. The projects store uses **atomic writes** (write to `.json.tmp`, then `rename()`) to prevent data corruption if the app crashes mid-write. Corrupted files are backed up to `.json.bak` before being replaced.
@@ -230,92 +354,159 @@ The `TerminalView` component works around this with a **URL accumulator**:
triple-c/ triple-c/
├── README.md # Architecture overview ├── README.md # Architecture overview
├── TECHNICAL.md # This document ├── TECHNICAL.md # This document
├── HOW-TO-USE.md # User guide ├── HOW-TO-USE.md # User guide (also served by the in-app Help dialog)
├── BUILDING.md # Build instructions ├── BUILDING.md # Build instructions
├── CLAUDE.md # Claude Code instructions ├── CLAUDE.md # Claude Code instructions
├── DESIGN-REVIEW.md # UI/UX review notes
├── ROADMAP.md # Planned work
├── container/ ├── container/ # Sandbox image
│ ├── Dockerfile # Ubuntu 24.04 + all dev tools + Claude Code │ ├── Dockerfile # Ubuntu 24.04 + all dev tools + Claude Code
│ ├── entrypoint.sh # UID/GID remap, SSH setup, git config, settings injection │ ├── entrypoint.sh # UID/GID remap, SSH setup, git config, settings injection,
│ │ # scheduler env snapshot + crontab rebuild
│ ├── osc52-clipboard # Clipboard shim (xclip/xsel/pbcopy via OSC 52) │ ├── osc52-clipboard # Clipboard shim (xclip/xsel/pbcopy via OSC 52)
│ ├── audio-shim # Audio capture shim (rec/arecord via FIFO) │ ├── audio-shim # Audio capture shim (rec/arecord via FIFO)
│ ├── triple-c-scheduler # Bash-based cron task system │ ├── triple-c-scheduler # Bash-based cron task system
── triple-c-task-runner # Task execution runner for scheduler ── triple-c-task-runner # Cron entry point; permission mode → flags → `claude -p`
│ ├── triple-c-sso-refresh # AWS SSO session refresh helper
│ └── mission-control/ # Bundled Flight Control methodology (skills, docs, templates)
├── stt-container/ # Speech-to-text image
│ ├── Dockerfile # Faster Whisper (Python 3.11 + FastAPI)
│ └── server.py # POST /transcribe endpoint
├── .gitea/ ├── .gitea/
│ └── workflows/ │ └── workflows/
│ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows) │ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows)
│ ├── build-app-preview.yml # Preview builds
│ ├── build.yml # Build container image (multi-arch) │ ├── build.yml # Build container image (multi-arch)
│ ├── build-stt.yml # Build the STT image
│ ├── sync-release.yml # Mirror releases to GitHub │ ├── sync-release.yml # Mirror releases to GitHub
── backfill-releases.yml # Bulk copy releases to GitHub ── backfill-releases.yml # Bulk copy releases to GitHub
│ └── cleanup-releases.yml # Prune old releases
└── app/ # Tauri v2 desktop application └── app/ # Tauri v2 desktop application
├── package.json # React, xterm.js, zustand, tailwindcss ├── package.json # React, xterm.js, zustand, tailwindcss
├── vite.config.ts # Vite bundler config ├── vite.config.ts # Vite bundler config
├── vitest.config.ts # Vitest (jsdom) config
├── index.html # HTML entry point ├── index.html # HTML entry point
├── src/ # React frontend ├── src/ # React frontend
│ ├── main.tsx # React DOM root │ ├── main.tsx # React DOM root
│ ├── App.tsx # Top-level layout │ ├── App.tsx # Top-level layout + welcome screen
│ ├── index.css # CSS variables, dark theme, scrollbars │ ├── index.css # CSS variables, dark theme, focus ring, scrollbars
│ ├── store/ │ ├── store/
│ │ └── appState.ts # Zustand store (projects, sessions, UI) │ │ └── appState.ts # Zustand store (projects, sessions, tab strip, toasts)
│ ├── hooks/ │ ├── hooks/
│ │ ├── useClaudeAuth.ts # Shared token status + acquisition
│ │ ├── useContainerProgress.ts # container-progress events → inline progress
│ │ ├── useDocker.ts # Docker status, image build/pull │ │ ├── useDocker.ts # Docker status, image build/pull
│ │ ├── useFileManager.ts # File manager operations │ │ ├── useFileManager.ts # File browser operations
│ │ ├── useInstallHelper.ts # Guided Docker installation
│ │ ├── useKeyboardShortcuts.ts # Ctrl+T / Ctrl+Shift+W / Ctrl+Tab / Ctrl+1..9
│ │ ├── useProjectActions.ts # Start/stop/reset/backup, open terminals
│ │ ├── useProjects.ts # Project CRUD operations │ │ ├── useProjects.ts # Project CRUD operations
│ │ ├── useSaveState.ts # Saved / Saving / Failed indicator state
│ │ ├── useSettings.ts # App settings │ │ ├── useSettings.ts # App settings
│ │ ├── useSTT.ts # Speech-to-text recording and container control
│ │ ├── useTerminal.ts # Terminal I/O, resize, session events │ │ ├── useTerminal.ts # Terminal I/O, resize, session events
│ │ ├── useUpdates.ts # App update checking │ │ ├── useUpdates.ts # App update checking
│ │ └── useVoice.ts # Voice mode audio capture │ │ └── useVoice.ts # Voice mode audio capture
│ ├── lib/ │ ├── lib/
│ │ ├── types.ts # TypeScript interfaces matching Rust models │ │ ├── types.ts # TypeScript interfaces matching Rust models
│ │ ├── tauri-commands.ts # Typed invoke() wrappers │ │ ├── tauri-commands.ts # Typed invoke() wrappers
│ │ ├── urlDetector.ts # Long-URL reassembly for OAuth flows
│ │ ├── wav.ts # WAV encoding for STT
│ │ └── constants.ts # App-wide constants │ │ └── constants.ts # App-wide constants
│ └── components/ │ └── components/
│ ├── layout/ # Sidebar, TopBar, StatusBar │ ├── DockerInstallDialog.tsx # First-run Docker setup
│ ├── projects/ # ProjectCard, ProjectList, AddProjectDialog, │ ├── layout/ # TopBar, MainTabs (the unified tab strip),
│ │ # FileManagerModal, ContainerProgressModal, modals │ │ # Sidebar, StatusBar, HelpDialog
│ ├── projects/
│ │ ├── home/ # Project Home — the main-area project view
│ │ │ ├── ProjectHome.tsx # Header, actions, overflow menu, tab strip
│ │ │ ├── OverviewTab.tsx # Permission mode, summary, recent activity
│ │ │ ├── SessionsTab.tsx # Past Claude sessions + Resume
│ │ │ ├── AutomationTab.tsx # Scheduler tasks + notifications
│ │ │ ├── ConfigTab.tsx # Config section host
│ │ │ ├── FilesTab.tsx # In-container file browser
│ │ │ ├── CapabilityTiles.tsx # Read-only capability counts
│ │ │ ├── format.ts # Age / size / uptime formatting
│ │ │ └── config/ # WorkspaceSection, ModelSection,
│ │ │ # AccessSection, RuntimeSection
│ │ ├── ProjectRow.tsx # Select-only sidebar row
│ │ ├── ProjectList.tsx # Sidebar project list
│ │ ├── AddProjectDialog.tsx # New-project dialog
│ │ ├── PermissionModeControl.tsx # Plan/Default/Accept Edits/Bypass
│ │ ├── ConfirmRemoveModal.tsx # Project removal confirmation
│ │ └── *Editor.tsx / *Modal.tsx # EnvVars, PortMappings,
│ │ # ClaudeInstructions, ClaudeCodeSettings —
│ │ # editors reused by Project Home
│ ├── settings/ # SettingsPanel, DockerSettings, AwsSettings, │ ├── settings/ # SettingsPanel, DockerSettings, AwsSettings,
│ │ # WebTerminalSettings, UpdateDialog │ │ # OllamaSettings, OpenAiCompatibleSettings,
└── terminal/ # TerminalView (xterm.js), TerminalTabs, UrlToast │ # SharedAuthSettings, ClaudeAuthModal,
│ │ # WebTerminalSettings, SttSettings,
│ │ # MicrophoneSettings, UpdateDialog, ImageUpdateDialog
│ ├── terminal/ # TerminalView (xterm.js), TerminalContextMenu,
│ │ # SttButton, UrlToast, trimSelection
│ └── ui/ # Shared primitives: Modal, Button, Toggle, Field,
│ # SegmentedControl, StatusIndicator, SaveIndicator,
│ # OverflowMenu, ToastHost, Tooltip, AccordionSection
└── src-tauri/ # Rust backend └── src-tauri/ # Rust backend
├── Cargo.toml # Rust dependencies ├── Cargo.toml # Rust dependencies
├── tauri.conf.json # Tauri app configuration ├── tauri.conf.json # Tauri app configuration
├── build.rs # Tauri build script
├── capabilities/ ├── capabilities/
│ └── default.json # Tauri v2 permission grants │ └── default.json # Tauri v2 plugin permission grants
└── src/ └── src/
├── lib.rs # App builder, plugin + command registration ├── lib.rs # App builder, plugin + command registration
├── main.rs # Entry point ├── main.rs # Entry point
├── logging.rs # Log configuration ├── logging.rs # Log configuration
├── commands/ # Tauri command handlers ├── commands/ # Tauri command handlers
│ ├── docker_commands.rs # Docker status, image ops │ ├── auth_bridge_commands.rs # Enable/status for the loopback bridge
│ ├── file_commands.rs # File manager (list/download/upload) │ ├── auth_token_commands.rs # claude setup-token flow, redaction, keychain
│ ├── project_commands.rs # Start/stop/rebuild containers │ ├── aws_commands.rs # AWS profile/region discovery
│ ├── settings_commands.rs # Settings CRUD │ ├── docker_commands.rs # Docker status, image ops
│ ├── terminal_commands.rs # Terminal I/O, resize │ ├── file_commands.rs # File browser (list/download/upload)
│ ├── update_commands.rs # App update checking │ ├── help_commands.rs # Serves HOW-TO-USE.md to the Help dialog
│ ├── inspect_commands.rs # Sessions, capabilities, scheduler tasks
│ ├── install_helper_commands.rs # Guided Docker installation
│ ├── project_commands.rs # Start/stop/rebuild/backup containers
│ ├── settings_commands.rs # Settings CRUD
│ ├── stt_commands.rs # STT start/stop/transcribe
│ ├── terminal_commands.rs # Terminal I/O, resize
│ ├── update_commands.rs # App update checking
│ └── web_terminal_commands.rs # Web terminal start/stop/status │ └── web_terminal_commands.rs # Web terminal start/stop/status
├── web_terminal/ # Remote terminal access ├── auth_bridge/ # Host-side loopback callback bridge
│ ├── mod.rs # Per-project poller, status, lifecycle
│ ├── proc_net.rs # /proc/net/tcp{,6} parsing, loopback filtering
│ └── tunnel.rs # Host loopback bind + socat tunnel over the Docker API
├── web_terminal/ # Remote terminal access
│ ├── mod.rs # Module root │ ├── mod.rs # Module root
│ ├── server.rs # Axum HTTP+WS server lifecycle │ ├── server.rs # Axum HTTP+WS server lifecycle
│ ├── ws_handler.rs # WebSocket connection handler │ ├── ws_handler.rs # WebSocket connection handler
│ └── terminal.html # Embedded xterm.js web UI │ └── terminal.html # Embedded xterm.js web UI
├── install_helper/ # Docker installation assistance
│ ├── mod.rs # Install orchestration
│ └── platform.rs # Per-OS install strategies
├── docker/ # Docker API layer ├── docker/ # Docker API layer
│ ├── client.rs # bollard singleton connection │ ├── client.rs # bollard singleton connection
│ ├── container.rs # Create, start, stop, remove, fingerprinting │ ├── container.rs # Create/start/stop/remove, labels, recreation checks,
├── exec.rs # PTY exec sessions with bidirectional streaming │ # remove_project_volumes, snapshot commit
── image.rs # Build from Dockerfile, pull from registry ── exec.rs # create_attached_exec() — the single attached-exec path
│ ├── image.rs # Build from Dockerfile, pull from registry
│ ├── stt.rs # Speech-to-text container lifecycle
│ └── legacy_cleanup.rs # Migration shim for the removed MCP feature
├── models/ # Data structures ├── models/ # Data structures
│ ├── project.rs # Project, Backend, BedrockConfig │ ├── project.rs # Project, Backend, PermissionMode, BedrockConfig, …
│ ├── app_settings.rs # Global settings (image source, AWS, etc.) │ ├── app_settings.rs # Global settings (image source, AWS, STT, web terminal)
│ ├── container_config.rs # Image name resolution │ ├── container_config.rs # Image name resolution
│ └── update_info.rs # Update metadata │ └── update_info.rs # Update metadata
└── storage/ # Persistence └── storage/ # Persistence
├── projects_store.rs # JSON file with atomic writes ├── projects_store.rs # JSON file with atomic writes
├── settings_store.rs # App settings (Tauri plugin-store) ├── settings_store.rs # App settings (Tauri plugin-store)
└── secure.rs # OS keychain via keyring └── secure.rs # OS keychain via keyring (secrets, shared token)
``` ```
--- ---
@@ -339,6 +530,11 @@ triple-c/
| `tar` | 0.4 | In-memory tar archives for Docker build context | | `tar` | 0.4 | In-memory tar archives for Docker build context |
| `dirs` | 6.x | Cross-platform app data directory paths | | `dirs` | 6.x | Cross-platform app data directory paths |
| `serde` / `serde_json` | 1.x | Serialization for IPC and persistence | | `serde` / `serde_json` | 1.x | Serialization for IPC and persistence |
| `log` / `fern` | 0.4 / 0.7 | Date-based file logging |
| `include_dir` | 0.7 | Embeds the container build context in the binary |
| `reqwest` | 0.12 | HTTPS (rustls) for update checks, help content, STT uploads |
| `iana-time-zone` | 0.1 | Host timezone detection for container `TZ` |
| `sha2` | 0.10 | Settings fingerprints |
| `axum` | 0.8 | HTTP+WebSocket server for web terminal | | `axum` | 0.8 | HTTP+WebSocket server for web terminal |
| `tower-http` | 0.6 | CORS middleware for web terminal | | `tower-http` | 0.6 | CORS middleware for web terminal |
| `base64` | 0.22 | Terminal data encoding over WebSocket | | `base64` | 0.22 | Terminal data encoding over WebSocket |
@@ -361,6 +557,8 @@ triple-c/
| `zustand` | 5.x | Lightweight state management | | `zustand` | 5.x | Lightweight state management |
| `tailwindcss` | 4.x | Utility-first CSS framework | | `tailwindcss` | 4.x | Utility-first CSS framework |
| `vite` | 6.x | Frontend build tool and dev server | | `vite` | 6.x | Frontend build tool and dev server |
| `vitest` | 4.x | Test runner (jsdom environment) |
| `@testing-library/react` | 16.x | Component tests |
### Container Image ### Container Image
@@ -0,0 +1,57 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, act } from "@testing-library/react";
import ConfirmResetModal from "./ConfirmResetModal";
/** Modal focuses via rAF so the panel is laid out first; jsdom needs a flush. */
async function flushFocus() {
await act(async () => {
vi.advanceTimersByTime(20);
});
}
describe("ConfirmResetModal", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["requestAnimationFrame", "setTimeout"] });
});
afterEach(() => {
vi.useRealTimers();
});
async function renderModal() {
const onConfirm = vi.fn();
const onCancel = vi.fn();
render(
<ConfirmResetModal
projectName="api-server"
onConfirm={onConfirm}
onCancel={onCancel}
/>,
);
await flushFocus();
return { onConfirm, onCancel };
}
it("names what will be lost rather than just asking to confirm", async () => {
await renderModal();
// The whole point of the gate: Reset deletes the volumes, and the two
// losses users do not expect are the login and the session transcripts.
expect(screen.getByText(/sign in again/i)).toBeInTheDocument();
expect(screen.getByText(/session transcript/i)).toBeInTheDocument();
// And it must say what is safe, or the warning reads as "you lose everything".
expect(screen.getByText(/mounted project folders/i)).toBeInTheDocument();
});
it("does not reset until confirmed", async () => {
const { onConfirm, onCancel } = await renderModal();
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(1);
expect(onConfirm).not.toHaveBeenCalled();
});
it("resets on confirm", async () => {
const { onConfirm } = await renderModal();
fireEvent.click(screen.getByRole("button", { name: "Reset container" }));
expect(onConfirm).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,66 @@
import Modal from "../ui/Modal";
import Button from "../ui/Button";
interface Props {
projectName: string;
onConfirm: () => void;
onCancel: () => void;
}
/**
* Reset is destructive in a way its name does not advertise.
*
* `rebuild_project_container` calls `remove_project_volumes`, which deletes
* both `triple-c-home-{id}` and `triple-c-claude-config-{id}` — so the OAuth
* login, any skills or agents installed in the container, and every session
* transcript go with them. That is intentional (Reset exists to get back to a
* clean base image), but it is not recoverable, so it gets the same
* confirmation gate as Remove.
*/
export default function ConfirmResetModal({ projectName, onConfirm, onCancel }: Props) {
return (
<Modal
title="Reset container"
onClose={onCancel}
widthClassName="w-[28rem]"
footer={
<>
<Button size="md" variant="ghost" onClick={onCancel}>
Cancel
</Button>
<Button
size="md"
onClick={onConfirm}
className="bg-[var(--error-emphasis)] text-white border border-transparent hover:opacity-90"
>
Reset container
</Button>
</>
}
>
<div className="space-y-2.5 text-[13px] text-[var(--text-secondary)]">
<p>
Rebuild{" "}
<strong className="text-[var(--text-primary)]">{projectName}</strong>&rsquo;s
container from the clean base image.
</p>
<p>
This deletes the container&rsquo;s volumes, which means you will lose:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
your <code className="font-mono">claude login</code> &mdash; you will need to
sign in again
</li>
<li>any skills, agents or plugins installed inside the container</li>
<li>every saved session transcript, so past sessions cannot be resumed</li>
<li>anything installed with <code className="font-mono">apt</code>, <code className="font-mono">pip</code> or <code className="font-mono">npm</code></li>
</ul>
<p>
Your mounted project folders are on the host and are{" "}
<strong className="text-[var(--text-primary)]">not</strong> affected.
</p>
</div>
</Modal>
);
}
@@ -8,6 +8,7 @@ import { ProjectStatusIndicator } from "../../ui/StatusIndicator";
import Button from "../../ui/Button"; import Button from "../../ui/Button";
import OverflowMenu from "../../ui/OverflowMenu"; import OverflowMenu from "../../ui/OverflowMenu";
import ConfirmRemoveModal from "../ConfirmRemoveModal"; import ConfirmRemoveModal from "../ConfirmRemoveModal";
import ConfirmResetModal from "../ConfirmResetModal";
import OverviewTab from "./OverviewTab"; import OverviewTab from "./OverviewTab";
import SessionsTab from "./SessionsTab"; import SessionsTab from "./SessionsTab";
import AutomationTab from "./AutomationTab"; import AutomationTab from "./AutomationTab";
@@ -39,6 +40,7 @@ export default function ProjectHome({ projectId, active }: Props) {
const project = projects.find((p) => p.id === projectId); const project = projects.find((p) => p.id === projectId);
const [tab, setTab] = useState<ProjectHomeTabId>("overview"); const [tab, setTab] = useState<ProjectHomeTabId>("overview");
const [confirmRemove, setConfirmRemove] = useState(false); const [confirmRemove, setConfirmRemove] = useState(false);
const [confirmReset, setConfirmReset] = useState(false);
const { runningSince, progress } = useAppState( const { runningSince, progress } = useAppState(
useShallow((s) => ({ useShallow((s) => ({
runningSince: s.runningSince[projectId], runningSince: s.runningSince[projectId],
@@ -144,9 +146,10 @@ export default function ProjectHome({ projectId, active }: Props) {
disabled: actions.backingUp || !project.container_id, disabled: actions.backingUp || !project.container_id,
}, },
{ {
label: "Reset container", label: "Reset container",
onSelect: actions.handleReset, onSelect: () => setConfirmReset(true),
disabled: !isStopped || actions.busy, disabled: !isStopped || actions.busy,
danger: true,
}, },
{ {
label: "Remove project…", label: "Remove project…",
@@ -205,6 +208,16 @@ export default function ProjectHome({ projectId, active }: Props) {
{tab === "files" && <FilesTab project={project} />} {tab === "files" && <FilesTab project={project} />}
</div> </div>
{confirmReset && (
<ConfirmResetModal
projectName={project.name}
onCancel={() => setConfirmReset(false)}
onConfirm={() => {
setConfirmReset(false);
actions.handleReset();
}}
/>
)}
{confirmRemove && ( {confirmRemove && (
<ConfirmRemoveModal <ConfirmRemoveModal
projectName={project.name} projectName={project.name}