Compare commits
5 Commits
v0.2.5-win
...
v0.2.10
| Author | SHA1 | Date | |
|---|---|---|---|
| ecaa42fa77 | |||
| 280358166a | |||
| 4732feb33e | |||
| 5977024953 | |||
| 27007b90e3 |
@@ -4,6 +4,25 @@ Triple-C (Claude-Code-Container) is a desktop application that runs Claude Code
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [First Launch](#first-launch)
|
||||||
|
- [The Interface](#the-interface)
|
||||||
|
- [Project Management](#project-management)
|
||||||
|
- [Project Configuration](#project-configuration)
|
||||||
|
- [MCP Servers (Beta)](#mcp-servers-beta)
|
||||||
|
- [AWS Bedrock Configuration](#aws-bedrock-configuration)
|
||||||
|
- [Ollama Configuration](#ollama-configuration)
|
||||||
|
- [LiteLLM Configuration](#litellm-configuration)
|
||||||
|
- [Settings](#settings)
|
||||||
|
- [Terminal Features](#terminal-features)
|
||||||
|
- [Scheduled Tasks (Inside the Container)](#scheduled-tasks-inside-the-container)
|
||||||
|
- [What's Inside the Container](#whats-inside-the-container)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
@@ -94,8 +113,9 @@ Claude Code launches automatically with `--dangerously-skip-permissions` inside
|
|||||||
|
|
||||||
1. Stop the container first (settings can only be changed while stopped).
|
1. Stop the container first (settings can only be changed while stopped).
|
||||||
2. In the project card, switch the backend to **Ollama**.
|
2. In the project card, switch the 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). Optionally set a model ID.
|
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).
|
||||||
4. Start the container again.
|
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.
|
||||||
|
|
||||||
**LiteLLM:**
|
**LiteLLM:**
|
||||||
|
|
||||||
@@ -395,7 +415,7 @@ To use Claude Code with a local or remote Ollama server, switch the backend to *
|
|||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
- **Base URL** — The URL of your Ollama server. Defaults to `http://host.docker.internal:11434`, which reaches a locally running Ollama instance from inside the container. For a remote server, use its IP or hostname (e.g., `http://192.168.1.100:11434`).
|
- **Base URL** — The URL of your Ollama server. Defaults to `http://host.docker.internal:11434`, which reaches a locally running Ollama instance from inside the container. For a remote server, use its IP or hostname (e.g., `http://192.168.1.100:11434`).
|
||||||
- **Model ID** — Optional. Override the model to use (e.g., `qwen3.5:27b`).
|
- **Model ID** — **Required.** The model to use (e.g., `qwen3.5:27b`). The model must be pulled in Ollama before use — run `ollama pull <model>` or use it via Ollama cloud so it is available when the container starts.
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
@@ -403,6 +423,8 @@ Triple-C sets `ANTHROPIC_BASE_URL` to point Claude Code at your Ollama server in
|
|||||||
|
|
||||||
> **Note:** Ollama support is best-effort. Claude Code is designed for Anthropic models, so some features (tool use, extended thinking, prompt caching, etc.) may not work as expected with non-Anthropic models.
|
> **Note:** Ollama support is best-effort. Claude Code is designed for Anthropic models, so some features (tool use, extended thinking, prompt caching, etc.) may not work as expected with non-Anthropic models.
|
||||||
|
|
||||||
|
> **Important:** The model must already be available in Ollama before starting the container. If using a local Ollama instance, pull the model first with `ollama pull <model-name>`. If using Ollama's cloud service, ensure the model has been used at least once so it is cached.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## LiteLLM Configuration
|
## LiteLLM Configuration
|
||||||
@@ -622,3 +644,13 @@ You can install additional tools at runtime with `sudo apt install`, `pip instal
|
|||||||
- Ensure the Docker image for the MCP server exists (pull it first if needed).
|
- Ensure the Docker image for the MCP server exists (pull it first if needed).
|
||||||
- Check that Docker socket access is available (stdio + Docker MCP servers auto-enable this).
|
- Check that Docker socket access is available (stdio + Docker MCP servers auto-enable this).
|
||||||
- Try resetting the project container to force a clean recreation.
|
- Try resetting the project container to force a clean recreation.
|
||||||
|
|
||||||
|
### "Failed to install Anthropic marketplace" Error
|
||||||
|
|
||||||
|
If Claude Code shows **"Failed to install Anthropic marketplace - Will retry on next startup"** repeatedly, the marketplace metadata in `~/.claude.json` may be corrupted. To fix this, open a **Shell** session in the project and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp ~/.claude.json ~/.claude.json.bak && jq 'with_entries(select(.key | startswith("officialMarketplace") | not))' ~/.claude.json.bak > ~/.claude.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This backs up your config and removes the corrupted marketplace entries. Claude Code will re-download them cleanly on the next startup.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ 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): User runs `claude login` inside the terminal on first use. Token persisted in the config volume across restarts and resets.
|
||||||
- **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token). 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`). Optional model override.
|
- **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.
|
||||||
- **LiteLLM**: Connect through a LiteLLM proxy gateway via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` to access 100+ model providers. API key stored securely in OS keychain.
|
- **LiteLLM**: Connect through a LiteLLM proxy gateway via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` to access 100+ model providers. API key stored securely in OS keychain.
|
||||||
|
|
||||||
> **Note:** Ollama and LiteLLM support is best-effort. Claude Code is designed for Anthropic models, so some features (tool use, extended thinking, prompt caching, etc.) may not work as expected with non-Anthropic models behind these backends.
|
> **Note:** Ollama and LiteLLM support is best-effort. Claude Code is designed for Anthropic models, so some features (tool use, extended thinking, prompt caching, etc.) may not work as expected with non-Anthropic models behind these backends.
|
||||||
|
|||||||
60
app/src-tauri/src/commands/help_commands.rs
Normal file
60
app/src-tauri/src/commands/help_commands.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use std::sync::OnceLock;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
const HELP_URL: &str =
|
||||||
|
"https://repo.anhonesthost.net/cybercovellc/triple-c/raw/branch/main/HOW-TO-USE.md";
|
||||||
|
|
||||||
|
const EMBEDDED_HELP: &str = include_str!("../../../../HOW-TO-USE.md");
|
||||||
|
|
||||||
|
/// Cached help content fetched from the remote repo (or `None` if not yet fetched).
|
||||||
|
static CACHED_HELP: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Return the help markdown content.
|
||||||
|
///
|
||||||
|
/// On the first call, tries to fetch the latest version from the gitea repo.
|
||||||
|
/// If that fails (network error, timeout, etc.), falls back to the version
|
||||||
|
/// embedded at compile time. The result is cached for the rest of the session.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_help_content() -> Result<String, String> {
|
||||||
|
let mutex = CACHED_HELP.get_or_init(|| Mutex::new(None));
|
||||||
|
let mut guard = mutex.lock().await;
|
||||||
|
|
||||||
|
if let Some(ref cached) = *guard {
|
||||||
|
return Ok(cached.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = match fetch_remote_help().await {
|
||||||
|
Ok(md) => {
|
||||||
|
log::info!("Loaded help content from remote repo");
|
||||||
|
md
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::info!("Using embedded help content (remote fetch failed: {})", e);
|
||||||
|
EMBEDDED_HELP.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*guard = Some(content.clone());
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_remote_help() -> Result<String, String> {
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.get(HELP_URL)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to fetch help content: {}", e))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("Remote returned status {}", resp.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read response body: {}", e))
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod aws_commands;
|
pub mod aws_commands;
|
||||||
pub mod docker_commands;
|
pub mod docker_commands;
|
||||||
pub mod file_commands;
|
pub mod file_commands;
|
||||||
|
pub mod help_commands;
|
||||||
pub mod mcp_commands;
|
pub mod mcp_commands;
|
||||||
pub mod project_commands;
|
pub mod project_commands;
|
||||||
pub mod settings_commands;
|
pub mod settings_commands;
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ pub fn run() {
|
|||||||
commands::update_commands::get_app_version,
|
commands::update_commands::get_app_version,
|
||||||
commands::update_commands::check_for_updates,
|
commands::update_commands::check_for_updates,
|
||||||
commands::update_commands::check_image_update,
|
commands::update_commands::check_image_update,
|
||||||
|
// Help
|
||||||
|
commands::help_commands::get_help_content,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -1,619 +1,20 @@
|
|||||||
import { useEffect, useRef, useCallback } from "react";
|
import { useEffect, useRef, useCallback, useState } from "react";
|
||||||
|
import { getHelpContent } from "../../lib/tauri-commands";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HELP_MARKDOWN = `# How to Use Triple-C
|
/** Convert header text to a URL-friendly slug for anchor links. */
|
||||||
|
function slugify(text: string): string {
|
||||||
Triple-C (Claude-Code-Container) is a desktop application that runs Claude Code inside isolated Docker containers. Each project gets its own sandboxed environment with bind-mounted directories, so Claude only has access to the files you explicitly provide.
|
return text
|
||||||
|
.toLowerCase()
|
||||||
---
|
.replace(/<[^>]+>/g, "") // strip HTML tags (e.g. from inline code)
|
||||||
|
.replace(/[^\w\s-]/g, "") // remove non-word chars except spaces/dashes
|
||||||
## Prerequisites
|
.replace(/\s+/g, "-") // spaces to dashes
|
||||||
|
.replace(/-+/g, "-") // collapse consecutive dashes
|
||||||
### Docker
|
.replace(/^-|-$/g, ""); // trim leading/trailing dashes
|
||||||
|
}
|
||||||
Triple-C requires a running Docker daemon. Install one of the following:
|
|
||||||
|
|
||||||
| Platform | Option | Link |
|
|
||||||
|----------|--------|------|
|
|
||||||
| **Windows** | Docker Desktop | https://docs.docker.com/desktop/install/windows-install/ |
|
|
||||||
| **macOS** | Docker Desktop | https://docs.docker.com/desktop/install/mac-install/ |
|
|
||||||
| **Linux** | Docker Engine | https://docs.docker.com/engine/install/ |
|
|
||||||
| **Linux** | Docker Desktop (alternative) | https://docs.docker.com/desktop/install/linux/ |
|
|
||||||
|
|
||||||
After installation, verify Docker is running:
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
docker info
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
> **Windows note:** Docker Desktop must be running before launching Triple-C. The app communicates with Docker through the named pipe at \`//./pipe/docker_engine\`.
|
|
||||||
|
|
||||||
> **Linux note:** Your user must have permission to access the Docker socket (\`/var/run/docker.sock\`). Either add your user to the \`docker\` group (\`sudo usermod -aG docker $USER\`, then log out and back in) or run Docker in rootless mode.
|
|
||||||
|
|
||||||
### Claude Code Account
|
|
||||||
|
|
||||||
You need access to Claude Code through one of:
|
|
||||||
|
|
||||||
- **Anthropic account** — Sign up at https://claude.ai and use \`claude login\` (OAuth) inside the terminal
|
|
||||||
- **AWS Bedrock** — An AWS account with Bedrock access and Claude models enabled
|
|
||||||
- **Ollama** — A local or remote Ollama server running an Anthropic-compatible model (best-effort support)
|
|
||||||
- **LiteLLM** — A LiteLLM proxy gateway providing access to 100+ model providers (best-effort support)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## First Launch
|
|
||||||
|
|
||||||
### 1. Get the Container Image
|
|
||||||
|
|
||||||
When you first open Triple-C, go to the **Settings** tab in the sidebar. Under **Docker**, you'll see:
|
|
||||||
|
|
||||||
- **Docker Status** — Should show "Connected" (green). If it shows "Not Available", make sure Docker is running.
|
|
||||||
- **Image Status** — Will show "Not Found" on first launch.
|
|
||||||
|
|
||||||
Choose an **Image Source**:
|
|
||||||
|
|
||||||
| Source | Description | When to Use |
|
|
||||||
|--------|-------------|-------------|
|
|
||||||
| **Registry** | Pulls the pre-built image from \`repo.anhonesthost.net\` | Fastest setup — recommended for most users |
|
|
||||||
| **Local Build** | Builds the image locally from the embedded Dockerfile | If you can't reach the registry, or want a custom build |
|
|
||||||
| **Custom** | Use any Docker image you specify | Advanced — bring your own sandbox image |
|
|
||||||
|
|
||||||
Click **Pull Image** (for Registry/Custom) or **Build Image** (for Local Build). A progress log will stream below the button. When complete, the status changes to "Ready" (green).
|
|
||||||
|
|
||||||
### 2. Create Your First Project
|
|
||||||
|
|
||||||
Switch to the **Projects** tab in the sidebar and click the **+** button.
|
|
||||||
|
|
||||||
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.
|
|
||||||
3. Click **Add Project**.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
Claude Code launches automatically with \`--dangerously-skip-permissions\` inside the sandboxed container.
|
|
||||||
|
|
||||||
### 5. Authenticate
|
|
||||||
|
|
||||||
**Anthropic (OAuth) — default:**
|
|
||||||
|
|
||||||
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.
|
|
||||||
3. Complete the login in your browser. The token is saved and persists across container stops and resets.
|
|
||||||
|
|
||||||
**AWS Bedrock:**
|
|
||||||
|
|
||||||
1. Stop the container first (settings can only be changed while stopped).
|
|
||||||
2. In the project card, switch the backend to **Bedrock**.
|
|
||||||
3. Expand the **Config** panel and fill in your AWS credentials (see AWS Bedrock Configuration below).
|
|
||||||
4. Start the container again.
|
|
||||||
|
|
||||||
**Ollama:**
|
|
||||||
|
|
||||||
1. Stop the container first (settings can only be changed while stopped).
|
|
||||||
2. In the project card, switch the 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). Optionally set a model ID.
|
|
||||||
4. Start the container again.
|
|
||||||
|
|
||||||
**LiteLLM:**
|
|
||||||
|
|
||||||
1. Stop the container first (settings can only be changed while stopped).
|
|
||||||
2. In the project card, switch the backend to **LiteLLM**.
|
|
||||||
3. Expand the **Config** panel and set the base URL of your LiteLLM proxy (defaults to \`http://host.docker.internal:4000\`). Optionally set an API key and model ID.
|
|
||||||
4. Start the container again.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The Interface
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ TopBar [ Terminal Tabs ] Docker ● Image ●│
|
|
||||||
├────────────┬────────────────────────────────────────┤
|
|
||||||
│ Sidebar │ │
|
|
||||||
│ │ Terminal View │
|
|
||||||
│ Projects │ (xterm.js) │
|
|
||||||
│ MCP │ │
|
|
||||||
│ Settings │ │
|
|
||||||
├────────────┴────────────────────────────────────────┤
|
|
||||||
│ StatusBar X projects · X running · X terminals │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
- **TopBar** — Terminal tabs for switching between sessions. Bash shell tabs show a "(bash)" suffix. Status dots on the right show Docker connection (green = connected) and image availability (green = ready).
|
|
||||||
- **Sidebar** — Toggle between the **Projects** list, **MCP** server configuration, and **Settings** panel.
|
|
||||||
- **Terminal View** — Interactive terminal powered by xterm.js with WebGL rendering. Includes a **Jump to Current** button that appears when you scroll up, so you can quickly return to the latest output.
|
|
||||||
- **StatusBar** — Counts of total projects, running containers, and open terminal sessions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Management
|
|
||||||
|
|
||||||
### Project Status
|
|
||||||
|
|
||||||
Each project shows a colored status dot:
|
|
||||||
|
|
||||||
| Color | Status | Meaning |
|
|
||||||
|-------|--------|---------|
|
|
||||||
| Gray | Stopped | Container is not running |
|
|
||||||
| Orange | Starting / Stopping | Container is transitioning |
|
|
||||||
| Green | Running | Container is active, ready for terminals |
|
|
||||||
| Red | Error | Something went wrong (check error message) |
|
|
||||||
|
|
||||||
### Project Actions
|
|
||||||
|
|
||||||
Select a project in the sidebar to see its action buttons:
|
|
||||||
|
|
||||||
| Button | When Available | What It Does |
|
|
||||||
|--------|---------------|--------------|
|
|
||||||
| **Start** | Stopped | Creates (if needed) and starts the container |
|
|
||||||
| **Stop** | Running | Stops the container but preserves its state |
|
|
||||||
| **Terminal** | Running | Opens a new Claude Code terminal session |
|
|
||||||
| **Shell** | Running | Opens a bash login shell in the container (no Claude Code) |
|
|
||||||
| **Files** | Running | Opens the file manager to browse, download, and upload files |
|
|
||||||
| **Reset** | Stopped | Destroys and recreates the container from scratch |
|
|
||||||
| **Config** | Always | Toggles the configuration panel |
|
|
||||||
| **Remove** | Stopped | Deletes the project and its container (with confirmation) |
|
|
||||||
|
|
||||||
### Renaming a Project
|
|
||||||
|
|
||||||
Double-click the project name in the sidebar to rename it inline. Press **Enter** to confirm or **Escape** to cancel.
|
|
||||||
|
|
||||||
### Container Lifecycle
|
|
||||||
|
|
||||||
Containers use a **stop/start** model. When you stop a container, everything inside it is preserved — installed packages, modified files, downloaded tools. Starting it again resumes where you left off.
|
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
Only **Remove** deletes everything, including the config volume and any stored credentials.
|
|
||||||
|
|
||||||
### Container Progress Feedback
|
|
||||||
|
|
||||||
When starting, stopping, or resetting a container, a progress modal shows real-time status messages (e.g., "Setting up MCP network...", "Starting MCP containers...", "Creating container..."). If an error occurs, the modal displays the error with a **Close** button. A **Force Stop** option is available if the operation stalls. The modal auto-closes on success.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Configuration
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
### Mounted Folders
|
|
||||||
|
|
||||||
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
|
|
||||||
- Edit the mount name to control where it appears inside \`/workspace/\`
|
|
||||||
- Click **+** to add more folders, or **x** to remove one
|
|
||||||
- Mount names must be unique and use only letters, numbers, dashes, underscores, and dots
|
|
||||||
|
|
||||||
### SSH Keys
|
|
||||||
|
|
||||||
Specify the path to your SSH key directory (typically \`~/.ssh\`). Keys are mounted read-only and copied into the container with correct permissions. This enables \`git clone\` via SSH inside the container.
|
|
||||||
|
|
||||||
### Git Configuration
|
|
||||||
|
|
||||||
- **Git Name / Email** — Sets \`git config user.name\` and \`user.email\` inside the container.
|
|
||||||
- **Git HTTPS Token** — A personal access token (e.g., from GitHub) for HTTPS git operations. Stored securely in your OS keychain — never written to disk in plaintext.
|
|
||||||
|
|
||||||
### Allow Container Spawning
|
|
||||||
|
|
||||||
When enabled, the host Docker socket is mounted into the container so Claude Code can create sibling containers (e.g., for running databases, test environments). This is **off by default** for security.
|
|
||||||
|
|
||||||
> Toggling this requires stopping and restarting the container to take effect.
|
|
||||||
|
|
||||||
### Mission Control
|
|
||||||
|
|
||||||
Toggle **Mission Control** to integrate Flight Control — an AI-first development methodology — into the project. When enabled:
|
|
||||||
|
|
||||||
- The Flight Control repository is automatically cloned into the container
|
|
||||||
- Flight Control skills are installed to Claude Code's skill directory (\`~/.claude/skills/\`)
|
|
||||||
- Project instructions are appended with Flight Control workflow guidance
|
|
||||||
- The repository is symlinked at \`/workspace/mission-control\`
|
|
||||||
|
|
||||||
Available skills include \`/mission\`, \`/flight\`, \`/leg\`, \`/agentic-workflow\`, \`/flight-debrief\`, \`/mission-debrief\`, \`/daily-briefing\`, and \`/init-project\`.
|
|
||||||
|
|
||||||
> This setting can only be changed when the container is stopped. Toggling it triggers a container recreation on the next start.
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Click **Edit** to open the environment variables modal. Add key-value pairs that will be injected into the container. Per-project variables override global variables with the same key.
|
|
||||||
|
|
||||||
> Reserved prefixes (\`ANTHROPIC_\`, \`AWS_\`, \`GIT_\`, \`HOST_\`, \`CLAUDE_\`, \`TRIPLE_C_\`) are filtered out to prevent conflicts with internal variables.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
Each mapping specifies:
|
|
||||||
- **Host Port** — The port on your machine (1-65535)
|
|
||||||
- **Container Port** — The port inside the container (1-65535)
|
|
||||||
- **Protocol** — TCP (default) or UDP
|
|
||||||
|
|
||||||
### Claude Instructions
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MCP Servers (Beta)
|
|
||||||
|
|
||||||
Triple-C supports Model Context Protocol (MCP) servers, which extend Claude Code with access to external tools and data sources. MCP servers are configured in a **global library** and **enabled per-project**.
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
There are two dimensions to MCP server configuration:
|
|
||||||
|
|
||||||
| | **Manual** (no Docker image) | **Docker** (Docker image specified) |
|
|
||||||
|---|---|---|
|
|
||||||
| **Stdio** | Command runs inside the project container | Command runs in a separate MCP container via \`docker exec\` |
|
|
||||||
| **HTTP** | Connects to a URL you provide | Runs in a separate container, reached by hostname on a shared Docker network |
|
|
||||||
|
|
||||||
**Docker images are pulled automatically** if not already present when the project starts.
|
|
||||||
|
|
||||||
### Accessing MCP Configuration
|
|
||||||
|
|
||||||
Click the **MCP** tab in the sidebar to open the MCP server library. This is where you define all available MCP servers.
|
|
||||||
|
|
||||||
### Adding an MCP Server
|
|
||||||
|
|
||||||
1. Type a name in the input field and click **Add**.
|
|
||||||
2. Expand the server card and configure it.
|
|
||||||
|
|
||||||
The key decision is whether to set a **Docker Image**:
|
|
||||||
- **With Docker image** — The MCP server runs in its own isolated container. Best for servers that need specific dependencies or system-level packages.
|
|
||||||
- **Without Docker image** (manual) — The command runs directly inside your project container. Best for lightweight npx-based servers that just need Node.js.
|
|
||||||
|
|
||||||
Then choose the **Transport Type**:
|
|
||||||
- **Stdio** — The MCP server communicates over stdin/stdout. This is the most common type.
|
|
||||||
- **HTTP** — The MCP server exposes an HTTP endpoint (streamable HTTP transport).
|
|
||||||
|
|
||||||
### Configuration Examples
|
|
||||||
|
|
||||||
#### Example 1: Filesystem Server (Stdio, Manual)
|
|
||||||
|
|
||||||
A simple npx-based server that runs inside the project container. No Docker image needed since Node.js is already installed.
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Docker Image** | *(empty)* |
|
|
||||||
| **Transport** | Stdio |
|
|
||||||
| **Command** | \`npx\` |
|
|
||||||
| **Arguments** | \`-y @modelcontextprotocol/server-filesystem /workspace\` |
|
|
||||||
|
|
||||||
#### Example 2: GitHub Server (Stdio, Manual)
|
|
||||||
|
|
||||||
Another npx-based server, with an environment variable for authentication.
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Docker Image** | *(empty)* |
|
|
||||||
| **Transport** | Stdio |
|
|
||||||
| **Command** | \`npx\` |
|
|
||||||
| **Arguments** | \`-y @modelcontextprotocol/server-github\` |
|
|
||||||
| **Environment Variables** | \`GITHUB_PERSONAL_ACCESS_TOKEN\` = \`ghp_your_token\` |
|
|
||||||
|
|
||||||
#### Example 3: Custom MCP Server (HTTP, Docker)
|
|
||||||
|
|
||||||
An MCP server packaged as a Docker image that exposes an HTTP endpoint.
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Docker Image** | \`myregistry/my-mcp-server:latest\` |
|
|
||||||
| **Transport** | HTTP |
|
|
||||||
| **Container Port** | \`8080\` |
|
|
||||||
| **Environment Variables** | \`API_KEY\` = \`your_key\` |
|
|
||||||
|
|
||||||
#### Example 4: Database Server (Stdio, Docker)
|
|
||||||
|
|
||||||
An MCP server that needs its own runtime environment, communicating over stdio.
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Docker Image** | \`mcp/postgres-server:latest\` |
|
|
||||||
| **Transport** | Stdio |
|
|
||||||
| **Command** | \`node\` |
|
|
||||||
| **Arguments** | \`dist/index.js\` |
|
|
||||||
| **Environment Variables** | \`DATABASE_URL\` = \`postgresql://user:pass@host:5432/db\` |
|
|
||||||
|
|
||||||
### Enabling MCP Servers Per-Project
|
|
||||||
|
|
||||||
In a project's configuration panel (click **Config**), the **MCP Servers** section shows checkboxes for all globally defined servers. Toggle each server on or off for that project. Changes take effect on the next container start.
|
|
||||||
|
|
||||||
### How Docker-Based MCP Works
|
|
||||||
|
|
||||||
When a project with Docker-based MCP servers starts:
|
|
||||||
|
|
||||||
1. Missing Docker images are **automatically pulled** (progress shown in the progress modal)
|
|
||||||
2. A dedicated **bridge network** is created for the project (\`triple-c-net-{projectId}\`)
|
|
||||||
3. Each enabled Docker MCP server gets its own container on that network
|
|
||||||
4. The main project container is connected to the same network
|
|
||||||
5. MCP server configuration is written to \`~/.claude.json\` inside the container
|
|
||||||
|
|
||||||
**Networking**: Docker-based MCP containers are reached by their container name as a hostname (e.g., \`triple-c-mcp-{serverId}\`), not by \`localhost\`. Docker DNS resolves these names automatically on the shared bridge network.
|
|
||||||
|
|
||||||
**Stdio + Docker**: The project container uses \`docker exec\` to communicate with the MCP container over stdin/stdout. This automatically enables Docker socket access on the project container.
|
|
||||||
|
|
||||||
**HTTP + Docker**: The project container connects to the MCP container's HTTP endpoint using the container hostname and port (e.g., \`http://triple-c-mcp-{serverId}:3000/mcp\`).
|
|
||||||
|
|
||||||
**Manual (no Docker image)**: Stdio commands run directly inside the project container. HTTP URLs connect to wherever you point them (could be an external service or something running on the host).
|
|
||||||
|
|
||||||
### Configuration Change Detection
|
|
||||||
|
|
||||||
MCP server configuration is tracked via SHA-256 fingerprints stored as Docker labels. If you add, remove, or modify MCP servers for a project, the container is automatically recreated on the next start to apply the new configuration. The container filesystem is snapshotted first, so installed packages are preserved.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AWS Bedrock Configuration
|
|
||||||
|
|
||||||
To use Claude via AWS Bedrock instead of Anthropic's API, switch the backend to **Bedrock** on the project card.
|
|
||||||
|
|
||||||
### Authentication Methods
|
|
||||||
|
|
||||||
| Method | Fields | Use Case |
|
|
||||||
|--------|--------|----------|
|
|
||||||
| **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 |
|
|
||||||
| **Token** | Bearer Token | Temporary bearer token authentication |
|
|
||||||
|
|
||||||
### Additional Bedrock Settings
|
|
||||||
|
|
||||||
- **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\`).
|
|
||||||
|
|
||||||
### Global AWS Defaults
|
|
||||||
|
|
||||||
In **Settings > AWS Configuration**, you can set defaults that apply to all Bedrock projects:
|
|
||||||
|
|
||||||
- **AWS Config Path** — Path to your \`~/.aws\` directory. Click **Detect** to auto-find it.
|
|
||||||
- **Default Profile** — Select from profiles found in your AWS config.
|
|
||||||
- **Default Region** — Fallback region for projects that don't specify one.
|
|
||||||
|
|
||||||
Per-project settings always override these global defaults.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ollama Configuration
|
|
||||||
|
|
||||||
To use Claude Code with a local or remote Ollama server, switch the backend to **Ollama** on the project card.
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
- **Base URL** — The URL of your Ollama server. Defaults to \`http://host.docker.internal:11434\`, which reaches a locally running Ollama instance from inside the container. For a remote server, use its IP or hostname (e.g., \`http://192.168.1.100:11434\`).
|
|
||||||
- **Model ID** — Optional. Override the model to use (e.g., \`qwen3.5:27b\`).
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
Triple-C sets \`ANTHROPIC_BASE_URL\` to point Claude Code at your Ollama server instead of Anthropic's API. The \`ANTHROPIC_AUTH_TOKEN\` is set to \`ollama\` (required by Claude Code but not used for actual authentication).
|
|
||||||
|
|
||||||
> **Note:** Ollama support is best-effort. Claude Code is designed for Anthropic models, so some features (tool use, extended thinking, prompt caching, etc.) may not work as expected with non-Anthropic models.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LiteLLM Configuration
|
|
||||||
|
|
||||||
To use Claude Code through a LiteLLM proxy gateway, switch the backend to **LiteLLM** on the project card. LiteLLM supports 100+ model providers (OpenAI, Gemini, Anthropic, and more) through a single proxy.
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
- **Base URL** — The URL of your LiteLLM proxy. Defaults to \`http://host.docker.internal:4000\` for a locally running proxy.
|
|
||||||
- **API Key** — Optional. The API key for your LiteLLM proxy, if authentication is required. Stored securely in your OS keychain.
|
|
||||||
- **Model ID** — Optional. Override the model to use.
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
Triple-C sets \`ANTHROPIC_BASE_URL\` to point Claude Code at your LiteLLM proxy. If an API key is provided, it is set as \`ANTHROPIC_AUTH_TOKEN\`.
|
|
||||||
|
|
||||||
> **Note:** LiteLLM support is best-effort. Claude Code is designed for Anthropic models, so some features (tool use, extended thinking, prompt caching, etc.) may not work as expected when routing to non-Anthropic models through the proxy.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Settings
|
|
||||||
|
|
||||||
Access global settings via the **Settings** tab in the sidebar.
|
|
||||||
|
|
||||||
### Docker Settings
|
|
||||||
|
|
||||||
- **Docker Status** — Connection status to the Docker daemon.
|
|
||||||
- **Image Source** — Where to get the sandbox container image (Registry, Local Build, or Custom).
|
|
||||||
- **Pull / Build Image** — Download or build the image. Progress streams in real time.
|
|
||||||
- **Refresh** — Re-check Docker and image status.
|
|
||||||
|
|
||||||
### Container Timezone
|
|
||||||
|
|
||||||
Set the timezone for all containers (IANA format, e.g., \`America/New_York\`, \`Europe/London\`, \`UTC\`). Auto-detected from your host on first launch. This affects scheduled task timing inside containers.
|
|
||||||
|
|
||||||
### Global Claude Instructions
|
|
||||||
|
|
||||||
Instructions applied to **all** projects. Written to \`~/.claude/CLAUDE.md\` in every container, before any per-project instructions.
|
|
||||||
|
|
||||||
### Global Environment Variables
|
|
||||||
|
|
||||||
Environment variables applied to **all** project containers. Per-project variables with the same key take precedence.
|
|
||||||
|
|
||||||
### Updates
|
|
||||||
|
|
||||||
- **Current Version** — The installed version of Triple-C.
|
|
||||||
- **Auto-check** — Toggle automatic update checks (every 24 hours).
|
|
||||||
- **Check now** — Manually check for updates.
|
|
||||||
|
|
||||||
When an update is available, a pulsing **Update** button appears in the top bar. Click it to see release notes and download links.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Terminal Features
|
|
||||||
|
|
||||||
### 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, with a "(bash)" suffix for shell sessions.
|
|
||||||
|
|
||||||
### Bash Shell Sessions
|
|
||||||
|
|
||||||
In addition to Claude Code terminals, you can open a plain **bash login shell** in any running container by clicking the **Shell** button. This is useful for manual inspection, package installation, debugging, or running commands that don't need Claude Code.
|
|
||||||
|
|
||||||
### URL Detection
|
|
||||||
|
|
||||||
When Claude Code prints a long URL (e.g., during \`claude login\`), Triple-C detects it and shows a toast notification at the top of the terminal with an **Open** button. Clicking it opens the URL in your default browser. The toast auto-dismisses after 30 seconds.
|
|
||||||
|
|
||||||
Shorter URLs in terminal output are also clickable directly.
|
|
||||||
|
|
||||||
### Clipboard Support (OSC 52)
|
|
||||||
|
|
||||||
Programs inside the container can copy text to your host clipboard. When a container program uses \`xclip\`, \`xsel\`, or \`pbcopy\`, the text is transparently forwarded to your host clipboard via OSC 52 escape sequences. No additional configuration is required — this works out of the box.
|
|
||||||
|
|
||||||
### Image Paste
|
|
||||||
|
|
||||||
You can paste images from your clipboard into the terminal (Ctrl+V / Cmd+V). The image is uploaded to the container as \`/tmp/clipboard_<timestamp>.png\` and the file path is injected into the terminal input so Claude Code can reference it. A toast notification confirms the upload.
|
|
||||||
|
|
||||||
### Jump to Current
|
|
||||||
|
|
||||||
When you scroll up in the terminal to review previous output, a **Jump to Current** button appears in the bottom-right corner. Click it to scroll back to the latest output.
|
|
||||||
|
|
||||||
### File Manager
|
|
||||||
|
|
||||||
Click the **Files** button on a running project to open the file manager modal. You can:
|
|
||||||
|
|
||||||
- **Browse** the container filesystem starting from \`/workspace\`, with breadcrumb navigation
|
|
||||||
- **Download** any file to your host machine via the download button on each file entry
|
|
||||||
- **Upload** files from your host into the current container directory
|
|
||||||
- **Refresh** the directory listing at any time
|
|
||||||
|
|
||||||
The file manager shows file names, sizes, and modification dates.
|
|
||||||
|
|
||||||
### Terminal Rendering
|
|
||||||
|
|
||||||
The terminal uses WebGL for hardware-accelerated rendering of the active tab. Inactive tabs fall back to canvas rendering to conserve GPU resources. The terminal automatically resizes when you resize the window.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Scheduled Tasks (Inside the Container)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Create a Recurring Task
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
triple-c-scheduler add --name "daily-review" --schedule "0 9 * * *" --prompt "Review open issues and summarize"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Create a One-Time Task
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
triple-c-scheduler add --name "migrate-db" --at "2026-03-05 14:00" --prompt "Run database migrations"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
One-time tasks automatically remove themselves after execution.
|
|
||||||
|
|
||||||
### Manage Tasks
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
triple-c-scheduler list # List all tasks
|
|
||||||
triple-c-scheduler enable --id abc123 # Enable a task
|
|
||||||
triple-c-scheduler disable --id abc123 # Disable a task
|
|
||||||
triple-c-scheduler remove --id abc123 # Delete a task
|
|
||||||
triple-c-scheduler run --id abc123 # Trigger a task immediately
|
|
||||||
triple-c-scheduler logs --id abc123 # View logs for a task
|
|
||||||
triple-c-scheduler logs --tail 20 # View last 20 log entries (all tasks)
|
|
||||||
triple-c-scheduler notifications # View completion notifications
|
|
||||||
triple-c-scheduler notifications --clear # Clear notifications
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Cron Schedule Format
|
|
||||||
|
|
||||||
Standard 5-field cron: \`minute hour day-of-month month day-of-week\`
|
|
||||||
|
|
||||||
| Example | Meaning |
|
|
||||||
|---------|---------|
|
|
||||||
| \`*/30 * * * *\` | Every 30 minutes |
|
|
||||||
| \`0 9 * * 1-5\` | 9:00 AM on weekdays |
|
|
||||||
| \`0 */2 * * *\` | Every 2 hours |
|
|
||||||
| \`0 0 1 * *\` | Midnight on the 1st of each month |
|
|
||||||
|
|
||||||
### Working Directory
|
|
||||||
|
|
||||||
By default, tasks run in \`/workspace\`. Use \`--working-dir\` to specify a different directory:
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
triple-c-scheduler add --name "test" --schedule "0 */6 * * *" --prompt "Run tests" --working-dir /workspace/my-project
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What's Inside the Container
|
|
||||||
|
|
||||||
The sandbox container (Ubuntu 24.04) comes pre-installed with:
|
|
||||||
|
|
||||||
| Tool | Version | Purpose |
|
|
||||||
|------|---------|---------|
|
|
||||||
| Claude Code | Latest | AI coding assistant (the tool being sandboxed) |
|
|
||||||
| Node.js | 22 LTS | JavaScript/TypeScript development |
|
|
||||||
| pnpm | Latest | Fast Node.js package manager |
|
|
||||||
| Python | 3.12 | Python development |
|
|
||||||
| uv | Latest | Fast Python package manager |
|
|
||||||
| ruff | Latest | Python linter/formatter |
|
|
||||||
| Rust | Stable | Rust development (via rustup) |
|
|
||||||
| Docker CLI | Latest | Container management (when spawning is enabled) |
|
|
||||||
| git | Latest | Version control |
|
|
||||||
| GitHub CLI (gh) | Latest | GitHub integration |
|
|
||||||
| AWS CLI | v2 | AWS services and Bedrock |
|
|
||||||
| ripgrep | Latest | Fast code search |
|
|
||||||
| build-essential | — | C/C++ compiler toolchain |
|
|
||||||
| openssh-client | — | SSH for git and remote access |
|
|
||||||
|
|
||||||
The container also includes **clipboard shims** (\`xclip\`, \`xsel\`, \`pbcopy\`) that forward copy operations to the host via OSC 52, and an **audio shim** (\`rec\`, \`arecord\`) for future voice mode support.
|
|
||||||
|
|
||||||
You can install additional tools at runtime with \`sudo apt install\`, \`pip install\`, \`npm install -g\`, etc. Installed packages persist across container stops (but not across resets).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Docker is "Not Available"
|
|
||||||
|
|
||||||
- **Is Docker running?** Start Docker Desktop or the Docker daemon (\`sudo systemctl start docker\`).
|
|
||||||
- **Permissions?** On Linux, ensure your user is in the \`docker\` group or the socket is accessible.
|
|
||||||
- **Custom socket path?** If your Docker socket is not at the default location, set it in Settings. The app expects \`/var/run/docker.sock\` on Linux/macOS or \`//./pipe/docker_engine\` on Windows.
|
|
||||||
|
|
||||||
### Image is "Not Found"
|
|
||||||
|
|
||||||
- Click **Pull Image** or **Build Image** in Settings > Docker.
|
|
||||||
- If pulling fails, check your network connection and whether you can reach the registry.
|
|
||||||
- Try switching to **Local Build** as an alternative.
|
|
||||||
|
|
||||||
### Container Won't Start
|
|
||||||
|
|
||||||
- Check that the Docker image is "Ready" in Settings.
|
|
||||||
- Verify that the mounted folder paths exist on your host.
|
|
||||||
- Look at the error message displayed in the progress modal.
|
|
||||||
|
|
||||||
### OAuth Login URL Not Opening
|
|
||||||
|
|
||||||
- Triple-C detects long URLs printed by \`claude login\` and shows a toast with an **Open** button.
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
- If you see permission errors, try resetting the container (stop, then click **Reset**).
|
|
||||||
|
|
||||||
### Settings Won't Save
|
|
||||||
|
|
||||||
- Most project settings can only be changed when the container is **stopped**. Stop the container first, make your changes, then start it again.
|
|
||||||
- Some changes (like toggling Docker access, Mission Control, or changing mounted folders) trigger an automatic container recreation on the next start.
|
|
||||||
|
|
||||||
### MCP Containers Not Starting
|
|
||||||
|
|
||||||
- Ensure the Docker image for the MCP server exists (pull it first if needed).
|
|
||||||
- Check that Docker socket access is available (stdio + Docker MCP servers auto-enable this).
|
|
||||||
- Try resetting the project container to force a clean recreation.`;
|
|
||||||
|
|
||||||
/** Simple markdown-to-HTML converter for the help content. */
|
/** Simple markdown-to-HTML converter for the help content. */
|
||||||
function renderMarkdown(md: string): string {
|
function renderMarkdown(md: string): string {
|
||||||
@@ -666,11 +67,11 @@ function renderMarkdown(md: string): string {
|
|||||||
// Horizontal rules
|
// Horizontal rules
|
||||||
html = html.replace(/\n---\n/g, '<hr class="help-hr"/>');
|
html = html.replace(/\n---\n/g, '<hr class="help-hr"/>');
|
||||||
|
|
||||||
// Headers (process from h4 down to h1)
|
// Headers with id attributes for anchor navigation (process from h4 down to h1)
|
||||||
html = html.replace(/^#### (.+)$/gm, '<h4 class="help-h4">$1</h4>');
|
html = html.replace(/^#### (.+)$/gm, (_m, title) => `<h4 class="help-h4" id="${slugify(title)}">${title}</h4>`);
|
||||||
html = html.replace(/^### (.+)$/gm, '<h3 class="help-h3">$1</h3>');
|
html = html.replace(/^### (.+)$/gm, (_m, title) => `<h3 class="help-h3" id="${slugify(title)}">${title}</h3>`);
|
||||||
html = html.replace(/^## (.+)$/gm, '<h2 class="help-h2">$1</h2>');
|
html = html.replace(/^## (.+)$/gm, (_m, title) => `<h2 class="help-h2" id="${slugify(title)}">${title}</h2>`);
|
||||||
html = html.replace(/^# (.+)$/gm, '<h1 class="help-h1">$1</h1>');
|
html = html.replace(/^# (.+)$/gm, (_m, title) => `<h1 class="help-h1" id="${slugify(title)}">${title}</h1>`);
|
||||||
|
|
||||||
// Bold (**...**)
|
// Bold (**...**)
|
||||||
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||||
@@ -678,6 +79,18 @@ function renderMarkdown(md: string): string {
|
|||||||
// Italic (*...*)
|
// Italic (*...*)
|
||||||
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||||
|
|
||||||
|
// Markdown-style anchor links [text](#anchor)
|
||||||
|
html = html.replace(
|
||||||
|
/\[([^\]]+)\]\(#([^)]+)\)/g,
|
||||||
|
'<a class="help-link" href="#$2">$1</a>',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Markdown-style external links [text](url)
|
||||||
|
html = html.replace(
|
||||||
|
/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,
|
||||||
|
'<a class="help-link" href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||||
|
);
|
||||||
|
|
||||||
// Unordered list items (- ...)
|
// Unordered list items (- ...)
|
||||||
// Group consecutive list items
|
// Group consecutive list items
|
||||||
html = html.replace(/((?:^|\n)- .+(?:\n- .+)*)/g, (block) => {
|
html = html.replace(/((?:^|\n)- .+(?:\n- .+)*)/g, (block) => {
|
||||||
@@ -699,7 +112,7 @@ function renderMarkdown(md: string): string {
|
|||||||
return `<ol class="help-ol">${items}</ol>`;
|
return `<ol class="help-ol">${items}</ol>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Links - convert URLs to clickable links
|
// Links - convert bare URLs to clickable links (skip already-wrapped URLs)
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
/(?<!="|'>)(https?:\/\/[^\s<)]+)/g,
|
/(?<!="|'>)(https?:\/\/[^\s<)]+)/g,
|
||||||
'<a class="help-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
|
'<a class="help-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||||
@@ -728,6 +141,9 @@ function renderMarkdown(md: string): string {
|
|||||||
|
|
||||||
export default function HelpDialog({ onClose }: Props) {
|
export default function HelpDialog({ onClose }: Props) {
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [markdown, setMarkdown] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -737,6 +153,12 @@ export default function HelpDialog({ onClose }: Props) {
|
|||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getHelpContent()
|
||||||
|
.then(setMarkdown)
|
||||||
|
.catch((e) => setError(String(e)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleOverlayClick = useCallback(
|
const handleOverlayClick = useCallback(
|
||||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (e.target === overlayRef.current) onClose();
|
if (e.target === overlayRef.current) onClose();
|
||||||
@@ -744,7 +166,17 @@ export default function HelpDialog({ onClose }: Props) {
|
|||||||
[onClose],
|
[onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderedHtml = renderMarkdown(HELP_MARKDOWN);
|
// Handle anchor link clicks to scroll within the dialog
|
||||||
|
const handleContentClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const anchor = target.closest("a");
|
||||||
|
if (!anchor) return;
|
||||||
|
const href = anchor.getAttribute("href");
|
||||||
|
if (!href || !href.startsWith("#")) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const el = contentRef.current?.querySelector(href);
|
||||||
|
if (el) el.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -766,9 +198,20 @@ export default function HelpDialog({ onClose }: Props) {
|
|||||||
|
|
||||||
{/* Scrollable content */}
|
{/* Scrollable content */}
|
||||||
<div
|
<div
|
||||||
|
ref={contentRef}
|
||||||
|
onClick={handleContentClick}
|
||||||
className="flex-1 overflow-y-auto px-6 py-4 help-content"
|
className="flex-1 overflow-y-auto px-6 py-4 help-content"
|
||||||
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
>
|
||||||
/>
|
{error && (
|
||||||
|
<p className="text-[var(--error)] text-sm">Failed to load help content: {error}</p>
|
||||||
|
)}
|
||||||
|
{!markdown && !error && (
|
||||||
|
<p className="text-[var(--text-secondary)] text-sm">Loading...</p>
|
||||||
|
)}
|
||||||
|
{markdown && (
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: renderMarkdown(markdown) }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useShallow } from "zustand/react/shallow";
|
|||||||
import { useAppState } from "../../store/appState";
|
import { useAppState } from "../../store/appState";
|
||||||
|
|
||||||
export default function StatusBar() {
|
export default function StatusBar() {
|
||||||
const { projects, sessions } = useAppState(
|
const { projects, sessions, terminalHasSelection } = useAppState(
|
||||||
useShallow(s => ({ projects: s.projects, sessions: s.sessions }))
|
useShallow(s => ({ projects: s.projects, sessions: s.sessions, terminalHasSelection: s.terminalHasSelection }))
|
||||||
);
|
);
|
||||||
const running = projects.filter((p) => p.status === "running").length;
|
const running = projects.filter((p) => p.status === "running").length;
|
||||||
|
|
||||||
@@ -20,6 +20,12 @@ export default function StatusBar() {
|
|||||||
<span>
|
<span>
|
||||||
{sessions.length} terminal{sessions.length !== 1 ? "s" : ""}
|
{sessions.length} terminal{sessions.length !== 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
|
{terminalHasSelection && (
|
||||||
|
<>
|
||||||
|
<span className="mx-2">|</span>
|
||||||
|
<span className="text-[var(--accent)]">Ctrl+Shift+C to copy</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -942,7 +942,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (optional)<Tooltip text="Ollama model name to use (e.g. qwen3.5:27b). Leave blank for the server default." /></label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (required)<Tooltip text="Ollama model name to use (e.g. qwen3.5:27b). The model must be pulled in Ollama before starting the container." /></label>
|
||||||
<input
|
<input
|
||||||
value={ollamaModelId}
|
value={ollamaModelId}
|
||||||
onChange={(e) => setOllamaModelId(e.target.value)}
|
onChange={(e) => setOllamaModelId(e.target.value)}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
const webglRef = useRef<WebglAddon | null>(null);
|
const webglRef = useRef<WebglAddon | null>(null);
|
||||||
const detectorRef = useRef<UrlDetector | null>(null);
|
const detectorRef = useRef<UrlDetector | null>(null);
|
||||||
const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal();
|
const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal();
|
||||||
|
const setTerminalHasSelection = useAppState(s => s.setTerminalHasSelection);
|
||||||
|
|
||||||
const ssoBufferRef = useRef("");
|
const ssoBufferRef = useRef("");
|
||||||
const ssoTriggeredRef = useRef(false);
|
const ssoTriggeredRef = useRef(false);
|
||||||
@@ -80,6 +81,22 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
|
|
||||||
term.open(containerRef.current);
|
term.open(containerRef.current);
|
||||||
|
|
||||||
|
// Ctrl+Shift+C copies selected terminal text to clipboard.
|
||||||
|
// This prevents the keystroke from reaching the container (where
|
||||||
|
// Ctrl+C would send SIGINT and cancel running work).
|
||||||
|
term.attachCustomKeyEventHandler((event) => {
|
||||||
|
if (event.type === "keydown" && event.ctrlKey && event.shiftKey && event.key === "C") {
|
||||||
|
const sel = term.getSelection();
|
||||||
|
if (sel) {
|
||||||
|
navigator.clipboard.writeText(sel).catch((e) =>
|
||||||
|
console.error("Ctrl+Shift+C clipboard write failed:", e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false; // prevent xterm from processing this key
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// WebGL addon is loaded/disposed dynamically in the active effect
|
// WebGL addon is loaded/disposed dynamically in the active effect
|
||||||
// to avoid exhausting the browser's limited WebGL context pool.
|
// to avoid exhausting the browser's limited WebGL context pool.
|
||||||
|
|
||||||
@@ -120,6 +137,11 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
setIsAtBottom(buf.viewportY >= buf.baseY);
|
setIsAtBottom(buf.viewportY >= buf.baseY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track text selection to show copy hint in status bar
|
||||||
|
const selectionDisposable = term.onSelectionChange(() => {
|
||||||
|
setTerminalHasSelection(term.hasSelection());
|
||||||
|
});
|
||||||
|
|
||||||
// Handle image paste: intercept paste events with image data,
|
// Handle image paste: intercept paste events with image data,
|
||||||
// upload to the container, and inject the file path into terminal input.
|
// upload to the container, and inject the file path into terminal input.
|
||||||
const handlePaste = (e: ClipboardEvent) => {
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
@@ -222,6 +244,8 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
osc52Disposable.dispose();
|
osc52Disposable.dispose();
|
||||||
inputDisposable.dispose();
|
inputDisposable.dispose();
|
||||||
scrollDisposable.dispose();
|
scrollDisposable.dispose();
|
||||||
|
selectionDisposable.dispose();
|
||||||
|
setTerminalHasSelection(false);
|
||||||
containerRef.current?.removeEventListener("paste", handlePaste, { capture: true });
|
containerRef.current?.removeEventListener("paste", handlePaste, { capture: true });
|
||||||
outputPromise.then((fn) => fn?.());
|
outputPromise.then((fn) => fn?.());
|
||||||
exitPromise.then((fn) => fn?.());
|
exitPromise.then((fn) => fn?.());
|
||||||
|
|||||||
@@ -85,3 +85,6 @@ export const checkForUpdates = () =>
|
|||||||
invoke<UpdateInfo | null>("check_for_updates");
|
invoke<UpdateInfo | null>("check_for_updates");
|
||||||
export const checkImageUpdate = () =>
|
export const checkImageUpdate = () =>
|
||||||
invoke<ImageUpdateInfo | null>("check_image_update");
|
invoke<ImageUpdateInfo | null>("check_image_update");
|
||||||
|
|
||||||
|
// Help
|
||||||
|
export const getHelpContent = () => invoke<string>("get_help_content");
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ interface AppState {
|
|||||||
removeMcpServerFromList: (id: string) => void;
|
removeMcpServerFromList: (id: string) => void;
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
|
terminalHasSelection: boolean;
|
||||||
|
setTerminalHasSelection: (has: boolean) => void;
|
||||||
sidebarView: "projects" | "mcp" | "settings";
|
sidebarView: "projects" | "mcp" | "settings";
|
||||||
setSidebarView: (view: "projects" | "mcp" | "settings") => void;
|
setSidebarView: (view: "projects" | "mcp" | "settings") => void;
|
||||||
dockerAvailable: boolean | null;
|
dockerAvailable: boolean | null;
|
||||||
@@ -100,6 +102,8 @@ export const useAppState = create<AppState>((set) => ({
|
|||||||
})),
|
})),
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
|
terminalHasSelection: false,
|
||||||
|
setTerminalHasSelection: (has) => set({ terminalHasSelection: has }),
|
||||||
sidebarView: "projects",
|
sidebarView: "projects",
|
||||||
setSidebarView: (view) => set({ sidebarView: view }),
|
setSidebarView: (view) => set({ sidebarView: view }),
|
||||||
dockerAvailable: null,
|
dockerAvailable: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user