Compare commits
2 Commits
v0.2.2-mac
...
v0.2.4
| Author | SHA1 | Date | |
|---|---|---|---|
| d2c1c2108a | |||
| cc163e6650 |
@@ -34,30 +34,37 @@ pub async fn check_for_updates() -> Result<Option<UpdateInfo>, String> {
|
|||||||
.map_err(|e| format!("Failed to parse releases: {}", e))?;
|
.map_err(|e| format!("Failed to parse releases: {}", e))?;
|
||||||
|
|
||||||
let current_version = env!("CARGO_PKG_VERSION");
|
let current_version = env!("CARGO_PKG_VERSION");
|
||||||
let is_windows = cfg!(target_os = "windows");
|
let current_semver = parse_semver(current_version).unwrap_or((0, 0, 0));
|
||||||
|
|
||||||
|
// Determine platform suffix for tag filtering
|
||||||
|
let platform_suffix: &str = if cfg!(target_os = "windows") {
|
||||||
|
"-win"
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
"-mac"
|
||||||
|
} else {
|
||||||
|
"" // Linux uses bare tags (no suffix)
|
||||||
|
};
|
||||||
|
|
||||||
// Filter releases by platform tag suffix
|
// Filter releases by platform tag suffix
|
||||||
let platform_releases: Vec<&GiteaRelease> = releases
|
let platform_releases: Vec<&GiteaRelease> = releases
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|r| {
|
.filter(|r| {
|
||||||
if is_windows {
|
if platform_suffix.is_empty() {
|
||||||
r.tag_name.ends_with("-win")
|
// Linux: bare tag only (no -win, no -mac)
|
||||||
|
!r.tag_name.ends_with("-win") && !r.tag_name.ends_with("-mac")
|
||||||
} else {
|
} else {
|
||||||
!r.tag_name.ends_with("-win")
|
r.tag_name.ends_with(platform_suffix)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Find the latest release with a higher patch version
|
// Find the latest release with a higher semver version
|
||||||
// Version format: 0.1.X or v0.1.X (tag may have prefix/suffix)
|
let mut best: Option<(&GiteaRelease, (u32, u32, u32))> = None;
|
||||||
let current_patch = parse_patch_version(current_version).unwrap_or(0);
|
|
||||||
|
|
||||||
let mut best: Option<(&GiteaRelease, u32)> = None;
|
|
||||||
for release in &platform_releases {
|
for release in &platform_releases {
|
||||||
if let Some(patch) = parse_patch_from_tag(&release.tag_name) {
|
if let Some(ver) = parse_semver_from_tag(&release.tag_name) {
|
||||||
if patch > current_patch {
|
if ver > current_semver {
|
||||||
if best.is_none() || patch > best.unwrap().1 {
|
if best.is_none() || ver > best.unwrap().1 {
|
||||||
best = Some((release, patch));
|
best = Some((release, ver));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,36 +99,34 @@ pub async fn check_for_updates() -> Result<Option<UpdateInfo>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse patch version from a semver string like "0.1.5" -> 5
|
/// Parse a semver string like "0.2.5" -> (0, 2, 5)
|
||||||
fn parse_patch_version(version: &str) -> Option<u32> {
|
fn parse_semver(version: &str) -> Option<(u32, u32, u32)> {
|
||||||
let clean = version.trim_start_matches('v');
|
let clean = version.trim_start_matches('v');
|
||||||
let parts: Vec<&str> = clean.split('.').collect();
|
let parts: Vec<&str> = clean.split('.').collect();
|
||||||
if parts.len() >= 3 {
|
if parts.len() >= 3 {
|
||||||
parts[2].parse().ok()
|
let major = parts[0].parse().ok()?;
|
||||||
|
let minor = parts[1].parse().ok()?;
|
||||||
|
let patch = parts[2].parse().ok()?;
|
||||||
|
Some((major, minor, patch))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse patch version from a tag like "v0.1.5", "v0.1.5-win", "0.1.5" -> 5
|
/// Parse semver from a tag like "v0.2.5", "v0.2.5-win", "v0.2.5-mac" -> (0, 2, 5)
|
||||||
fn parse_patch_from_tag(tag: &str) -> Option<u32> {
|
fn parse_semver_from_tag(tag: &str) -> Option<(u32, u32, u32)> {
|
||||||
let clean = tag.trim_start_matches('v');
|
let clean = tag.trim_start_matches('v');
|
||||||
// Remove platform suffix
|
// Remove platform suffix
|
||||||
let clean = clean.strip_suffix("-win").unwrap_or(clean);
|
let clean = clean.strip_suffix("-win")
|
||||||
parse_patch_version(clean)
|
.or_else(|| clean.strip_suffix("-mac"))
|
||||||
|
.unwrap_or(clean);
|
||||||
|
parse_semver(clean)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a clean version string from a tag like "v0.1.5-win" -> "0.1.5"
|
/// Extract a clean version string from a tag like "v0.2.5-win" -> "0.2.5"
|
||||||
fn extract_version_from_tag(tag: &str) -> Option<String> {
|
fn extract_version_from_tag(tag: &str) -> Option<String> {
|
||||||
let clean = tag.trim_start_matches('v');
|
let (major, minor, patch) = parse_semver_from_tag(tag)?;
|
||||||
let clean = clean.strip_suffix("-win").unwrap_or(clean);
|
Some(format!("{}.{}.{}", major, minor, patch))
|
||||||
// Validate it looks like a version
|
|
||||||
let parts: Vec<&str> = clean.split('.').collect();
|
|
||||||
if parts.len() >= 3 && parts.iter().all(|p| p.parse::<u32>().is_ok()) {
|
|
||||||
Some(clean.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether a newer container image is available in the registry.
|
/// Check whether a newer container image is available in the registry.
|
||||||
|
|||||||
775
app/src/components/layout/HelpDialog.tsx
Normal file
775
app/src/components/layout/HelpDialog.tsx
Normal file
@@ -0,0 +1,775 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HELP_MARKDOWN = `# How to Use Triple-C
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
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. */
|
||||||
|
function renderMarkdown(md: string): string {
|
||||||
|
let html = md;
|
||||||
|
|
||||||
|
// Normalize line endings
|
||||||
|
html = html.replace(/\r\n/g, "\n");
|
||||||
|
|
||||||
|
// Escape HTML entities (but we'll re-introduce tags below)
|
||||||
|
html = html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
|
||||||
|
// Fenced code blocks (```...```)
|
||||||
|
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
|
||||||
|
return `<pre class="help-code-block"><code>${code.trimEnd()}</code></pre>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inline code (`...`)
|
||||||
|
html = html.replace(/`([^`]+)`/g, '<code class="help-inline-code">$1</code>');
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
html = html.replace(
|
||||||
|
/(?:^|\n)(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/g,
|
||||||
|
(_m, headerRow: string, _sep: string, bodyRows: string) => {
|
||||||
|
const headers = headerRow
|
||||||
|
.split("|")
|
||||||
|
.slice(1, -1)
|
||||||
|
.map((c: string) => `<th>${c.trim()}</th>`)
|
||||||
|
.join("");
|
||||||
|
const rows = bodyRows
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((row: string) => {
|
||||||
|
const cells = row
|
||||||
|
.split("|")
|
||||||
|
.slice(1, -1)
|
||||||
|
.map((c: string) => `<td>${c.trim()}</td>`)
|
||||||
|
.join("");
|
||||||
|
return `<tr>${cells}</tr>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
return `<table class="help-table"><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Blockquotes (> ...)
|
||||||
|
html = html.replace(/(?:^|\n)> (.+)/g, '<blockquote class="help-blockquote">$1</blockquote>');
|
||||||
|
// Merge adjacent blockquotes
|
||||||
|
html = html.replace(/<\/blockquote>\s*<blockquote class="help-blockquote">/g, "<br/>");
|
||||||
|
|
||||||
|
// Horizontal rules
|
||||||
|
html = html.replace(/\n---\n/g, '<hr class="help-hr"/>');
|
||||||
|
|
||||||
|
// Headers (process from h4 down to h1)
|
||||||
|
html = html.replace(/^#### (.+)$/gm, '<h4 class="help-h4">$1</h4>');
|
||||||
|
html = html.replace(/^### (.+)$/gm, '<h3 class="help-h3">$1</h3>');
|
||||||
|
html = html.replace(/^## (.+)$/gm, '<h2 class="help-h2">$1</h2>');
|
||||||
|
html = html.replace(/^# (.+)$/gm, '<h1 class="help-h1">$1</h1>');
|
||||||
|
|
||||||
|
// Bold (**...**)
|
||||||
|
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||||
|
|
||||||
|
// Italic (*...*)
|
||||||
|
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||||
|
|
||||||
|
// Unordered list items (- ...)
|
||||||
|
// Group consecutive list items
|
||||||
|
html = html.replace(/((?:^|\n)- .+(?:\n- .+)*)/g, (block) => {
|
||||||
|
const items = block
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => `<li>${line.replace(/^- /, "")}</li>`)
|
||||||
|
.join("");
|
||||||
|
return `<ul class="help-ul">${items}</ul>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ordered list items (1. ...)
|
||||||
|
html = html.replace(/((?:^|\n)\d+\. .+(?:\n\d+\. .+)*)/g, (block) => {
|
||||||
|
const items = block
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => `<li>${line.replace(/^\d+\. /, "")}</li>`)
|
||||||
|
.join("");
|
||||||
|
return `<ol class="help-ol">${items}</ol>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Links - convert URLs to clickable links
|
||||||
|
html = html.replace(
|
||||||
|
/(?<!="|'>)(https?:\/\/[^\s<)]+)/g,
|
||||||
|
'<a class="help-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrap remaining loose text lines in paragraphs
|
||||||
|
// Split by double newlines for paragraph breaks
|
||||||
|
const blocks = html.split(/\n\n+/);
|
||||||
|
html = blocks
|
||||||
|
.map((block) => {
|
||||||
|
const trimmed = block.trim();
|
||||||
|
if (!trimmed) return "";
|
||||||
|
// Don't wrap blocks that are already HTML elements
|
||||||
|
if (
|
||||||
|
/^<(h[1-4]|ul|ol|pre|table|blockquote|hr)/.test(trimmed)
|
||||||
|
) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
// Wrap in paragraph, replacing single newlines with <br/>
|
||||||
|
return `<p class="help-p">${trimmed.replace(/\n/g, "<br/>")}</p>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HelpDialog({ onClose }: Props) {
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleOverlayClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target === overlayRef.current) onClose();
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderedHtml = renderMarkdown(HELP_MARKDOWN);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={overlayRef}
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg shadow-xl w-[48rem] max-w-[90vw] max-h-[85vh] flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border-color)] flex-shrink-0">
|
||||||
|
<h2 className="text-lg font-semibold">How to Use Triple-C</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-3 py-1.5 text-xs bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable content */}
|
||||||
|
<div
|
||||||
|
className="flex-1 overflow-y-auto px-6 py-4 help-content"
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { useAppState } from "../../store/appState";
|
|||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
import UpdateDialog from "../settings/UpdateDialog";
|
import UpdateDialog from "../settings/UpdateDialog";
|
||||||
import ImageUpdateDialog from "../settings/ImageUpdateDialog";
|
import ImageUpdateDialog from "../settings/ImageUpdateDialog";
|
||||||
|
import HelpDialog from "./HelpDialog";
|
||||||
|
|
||||||
export default function TopBar() {
|
export default function TopBar() {
|
||||||
const { dockerAvailable, imageExists, updateInfo, imageUpdateInfo, appVersion, setUpdateInfo, setImageUpdateInfo } = useAppState(
|
const { dockerAvailable, imageExists, updateInfo, imageUpdateInfo, appVersion, setUpdateInfo, setImageUpdateInfo } = useAppState(
|
||||||
@@ -21,6 +22,7 @@ export default function TopBar() {
|
|||||||
const { appSettings, saveSettings } = useSettings();
|
const { appSettings, saveSettings } = useSettings();
|
||||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||||
const [showImageUpdateDialog, setShowImageUpdateDialog] = useState(false);
|
const [showImageUpdateDialog, setShowImageUpdateDialog] = useState(false);
|
||||||
|
const [showHelpDialog, setShowHelpDialog] = useState(false);
|
||||||
|
|
||||||
const handleDismiss = async () => {
|
const handleDismiss = async () => {
|
||||||
if (appSettings && updateInfo) {
|
if (appSettings && updateInfo) {
|
||||||
@@ -70,6 +72,13 @@ export default function TopBar() {
|
|||||||
)}
|
)}
|
||||||
<StatusDot ok={dockerAvailable === true} label="Docker" />
|
<StatusDot ok={dockerAvailable === true} label="Docker" />
|
||||||
<StatusDot ok={imageExists === true} label="Image" />
|
<StatusDot ok={imageExists === true} label="Image" />
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHelpDialog(true)}
|
||||||
|
title="Help"
|
||||||
|
className="ml-1 w-5 h-5 flex items-center justify-center rounded-full border border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-secondary)] transition-colors text-xs font-semibold leading-none"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showUpdateDialog && updateInfo && (
|
{showUpdateDialog && updateInfo && (
|
||||||
@@ -87,6 +96,9 @@ export default function TopBar() {
|
|||||||
onClose={() => setShowImageUpdateDialog(false)}
|
onClose={() => setShowImageUpdateDialog(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showHelpDialog && (
|
||||||
|
<HelpDialog onClose={() => setShowHelpDialog(false)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ClaudeInstructionsModal from "./ClaudeInstructionsModal";
|
|||||||
import ContainerProgressModal from "./ContainerProgressModal";
|
import ContainerProgressModal from "./ContainerProgressModal";
|
||||||
import FileManagerModal from "./FileManagerModal";
|
import FileManagerModal from "./FileManagerModal";
|
||||||
import ConfirmRemoveModal from "./ConfirmRemoveModal";
|
import ConfirmRemoveModal from "./ConfirmRemoveModal";
|
||||||
|
import Tooltip from "../ui/Tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: Project;
|
project: Project;
|
||||||
@@ -448,7 +449,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<div className="mt-2 ml-4 space-y-2 min-w-0 overflow-hidden">
|
<div className="mt-2 ml-4 space-y-2 min-w-0 overflow-hidden">
|
||||||
{/* Backend selector */}
|
{/* Backend selector */}
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
<span className="text-[var(--text-secondary)] mr-1">Backend:</span>
|
<span className="text-[var(--text-secondary)] mr-1">Backend:<Tooltip text="Anthropic = direct Claude API via OAuth. Bedrock = AWS Bedrock. Ollama = local models. LiteLLM = proxy gateway for 100+ providers." /></span>
|
||||||
<select
|
<select
|
||||||
value={project.backend}
|
value={project.backend}
|
||||||
onChange={(e) => { e.stopPropagation(); handleBackendChange(e.target.value as Backend); }}
|
onChange={(e) => { e.stopPropagation(); handleBackendChange(e.target.value as Backend); }}
|
||||||
@@ -609,7 +610,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* SSH Key */}
|
{/* SSH Key */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">SSH Key Directory</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">SSH Key Directory<Tooltip text="Path to your .ssh directory. Mounted into the container so Claude can authenticate with Git remotes over SSH." /></label>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<input
|
<input
|
||||||
value={sshKeyPath}
|
value={sshKeyPath}
|
||||||
@@ -631,7 +632,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* Git Name */}
|
{/* Git Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Name</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Name<Tooltip text="Sets git user.name inside the container for commit authorship." /></label>
|
||||||
<input
|
<input
|
||||||
value={gitName}
|
value={gitName}
|
||||||
onChange={(e) => setGitName(e.target.value)}
|
onChange={(e) => setGitName(e.target.value)}
|
||||||
@@ -644,7 +645,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* Git Email */}
|
{/* Git Email */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Email</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Email<Tooltip text="Sets git user.email inside the container for commit authorship." /></label>
|
||||||
<input
|
<input
|
||||||
value={gitEmail}
|
value={gitEmail}
|
||||||
onChange={(e) => setGitEmail(e.target.value)}
|
onChange={(e) => setGitEmail(e.target.value)}
|
||||||
@@ -657,7 +658,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* Git Token (HTTPS) */}
|
{/* Git Token (HTTPS) */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git HTTPS Token</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git HTTPS Token<Tooltip text="A personal access token (e.g. GitHub PAT) for HTTPS git operations inside the container." /></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={gitToken}
|
value={gitToken}
|
||||||
@@ -671,7 +672,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* Docker access toggle */}
|
{/* Docker access toggle */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">Allow container spawning</label>
|
<label className="text-xs text-[var(--text-secondary)]">Allow container spawning<Tooltip text="Mounts the Docker socket so Claude can build and run Docker containers from inside the sandbox." /></label>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try { await update({ ...project, allow_docker_access: !project.allow_docker_access }); } catch (err) {
|
try { await update({ ...project, allow_docker_access: !project.allow_docker_access }); } catch (err) {
|
||||||
@@ -691,7 +692,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* Mission Control toggle */}
|
{/* Mission Control toggle */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">Mission Control</label>
|
<label className="text-xs text-[var(--text-secondary)]">Mission Control<Tooltip text="Enables a web dashboard for monitoring and managing Claude sessions remotely." /></label>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
@@ -714,7 +715,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">
|
<label className="text-xs text-[var(--text-secondary)]">
|
||||||
Environment Variables{envVars.length > 0 && ` (${envVars.length})`}
|
Environment Variables{envVars.length > 0 && ` (${envVars.length})`}<Tooltip text="Custom env vars injected into this project's container. Useful for API keys or tool configuration." />
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowEnvVarsModal(true)}
|
onClick={() => setShowEnvVarsModal(true)}
|
||||||
@@ -727,7 +728,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{/* Port Mappings */}
|
{/* Port Mappings */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">
|
<label className="text-xs text-[var(--text-secondary)]">
|
||||||
Port Mappings{portMappings.length > 0 && ` (${portMappings.length})`}
|
Port Mappings{portMappings.length > 0 && ` (${portMappings.length})`}<Tooltip text="Map container ports to host ports so you can access dev servers running inside the container." />
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPortMappingsModal(true)}
|
onClick={() => setShowPortMappingsModal(true)}
|
||||||
@@ -740,7 +741,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{/* Claude Instructions */}
|
{/* Claude Instructions */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">
|
<label className="text-xs text-[var(--text-secondary)]">
|
||||||
Claude Instructions{claudeInstructions ? " (set)" : ""}
|
Claude Instructions{claudeInstructions ? " (set)" : ""}<Tooltip text="Project-specific instructions written to CLAUDE.md. Guides Claude's behavior for this project." />
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowClaudeInstructionsModal(true)}
|
onClick={() => setShowClaudeInstructionsModal(true)}
|
||||||
@@ -753,7 +754,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{/* MCP Servers */}
|
{/* MCP Servers */}
|
||||||
{mcpServers.length > 0 && (
|
{mcpServers.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-1">MCP Servers</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-1">MCP Servers<Tooltip text="Model Context Protocol servers give Claude access to external tools and data sources." /></label>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{mcpServers.map((server) => {
|
{mcpServers.map((server) => {
|
||||||
const enabled = project.enabled_mcp_servers.includes(server.id);
|
const enabled = project.enabled_mcp_servers.includes(server.id);
|
||||||
@@ -819,7 +820,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* AWS Region (always shown) */}
|
{/* AWS Region (always shown) */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Region</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Region<Tooltip text="The AWS region where your Bedrock endpoint is available (e.g. us-east-1)." /></label>
|
||||||
<input
|
<input
|
||||||
value={bedrockRegion}
|
value={bedrockRegion}
|
||||||
onChange={(e) => setBedrockRegion(e.target.value)}
|
onChange={(e) => setBedrockRegion(e.target.value)}
|
||||||
@@ -834,7 +835,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{bc.auth_method === "static_credentials" && (
|
{bc.auth_method === "static_credentials" && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Access Key ID</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Access Key ID<Tooltip text="Your AWS IAM access key ID for Bedrock API authentication." /></label>
|
||||||
<input
|
<input
|
||||||
value={bedrockAccessKeyId}
|
value={bedrockAccessKeyId}
|
||||||
onChange={(e) => setBedrockAccessKeyId(e.target.value)}
|
onChange={(e) => setBedrockAccessKeyId(e.target.value)}
|
||||||
@@ -845,7 +846,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Secret Access Key</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Secret Access Key<Tooltip text="Your AWS IAM secret key. Stored locally and injected as an env var into the container." /></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={bedrockSecretKey}
|
value={bedrockSecretKey}
|
||||||
@@ -856,7 +857,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Session Token (optional)</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Session Token (optional)<Tooltip text="Temporary session token for assumed-role or MFA-based AWS credentials." /></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={bedrockSessionToken}
|
value={bedrockSessionToken}
|
||||||
@@ -872,7 +873,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{/* Profile field */}
|
{/* Profile field */}
|
||||||
{bc.auth_method === "profile" && (
|
{bc.auth_method === "profile" && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Profile</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Profile<Tooltip text="Named profile from your AWS config/credentials files (e.g. 'default' or 'prod')." /></label>
|
||||||
<input
|
<input
|
||||||
value={bedrockProfile}
|
value={bedrockProfile}
|
||||||
onChange={(e) => setBedrockProfile(e.target.value)}
|
onChange={(e) => setBedrockProfile(e.target.value)}
|
||||||
@@ -887,7 +888,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{/* Bearer token field */}
|
{/* Bearer token field */}
|
||||||
{bc.auth_method === "bearer_token" && (
|
{bc.auth_method === "bearer_token" && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Bearer Token</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Bearer Token<Tooltip text="An SSO or identity-center bearer token for Bedrock authentication." /></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={bedrockBearerToken}
|
value={bedrockBearerToken}
|
||||||
@@ -901,7 +902,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
{/* Model override */}
|
{/* Model override */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model ID (optional)</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model ID (optional)<Tooltip text="Override the default Bedrock model. Leave blank to use Claude's default." /></label>
|
||||||
<input
|
<input
|
||||||
value={bedrockModelId}
|
value={bedrockModelId}
|
||||||
onChange={(e) => setBedrockModelId(e.target.value)}
|
onChange={(e) => setBedrockModelId(e.target.value)}
|
||||||
@@ -926,7 +927,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL<Tooltip text="URL of your Ollama server. Use host.docker.internal to reach the host machine from inside the container." /></label>
|
||||||
<input
|
<input
|
||||||
value={ollamaBaseUrl}
|
value={ollamaBaseUrl}
|
||||||
onChange={(e) => setOllamaBaseUrl(e.target.value)}
|
onChange={(e) => setOllamaBaseUrl(e.target.value)}
|
||||||
@@ -941,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)</label>
|
<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>
|
||||||
<input
|
<input
|
||||||
value={ollamaModelId}
|
value={ollamaModelId}
|
||||||
onChange={(e) => setOllamaModelId(e.target.value)}
|
onChange={(e) => setOllamaModelId(e.target.value)}
|
||||||
@@ -966,7 +967,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL<Tooltip text="URL of your LiteLLM proxy server. Use host.docker.internal for a locally running proxy." /></label>
|
||||||
<input
|
<input
|
||||||
value={litellmBaseUrl}
|
value={litellmBaseUrl}
|
||||||
onChange={(e) => setLitellmBaseUrl(e.target.value)}
|
onChange={(e) => setLitellmBaseUrl(e.target.value)}
|
||||||
@@ -981,7 +982,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">API Key</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">API Key<Tooltip text="Authentication key for your LiteLLM proxy, if required." /></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={litellmApiKey}
|
value={litellmApiKey}
|
||||||
@@ -994,7 +995,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)</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (optional)<Tooltip text="Model identifier as configured in your LiteLLM proxy (e.g. gpt-4o, gemini-pro)." /></label>
|
||||||
<input
|
<input
|
||||||
value={litellmModelId}
|
value={litellmModelId}
|
||||||
onChange={(e) => setLitellmModelId(e.target.value)}
|
onChange={(e) => setLitellmModelId(e.target.value)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
import * as commands from "../../lib/tauri-commands";
|
import * as commands from "../../lib/tauri-commands";
|
||||||
|
import Tooltip from "../ui/Tooltip";
|
||||||
|
|
||||||
export default function AwsSettings() {
|
export default function AwsSettings() {
|
||||||
const { appSettings, saveSettings } = useSettings();
|
const { appSettings, saveSettings } = useSettings();
|
||||||
@@ -56,7 +57,7 @@ export default function AwsSettings() {
|
|||||||
|
|
||||||
{/* AWS Config Path */}
|
{/* AWS Config Path */}
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">AWS Config Path</span>
|
<span className="text-[var(--text-secondary)] text-xs block mb-1">AWS Config Path<Tooltip text="Path to your AWS config/credentials directory. Mounted into containers for Bedrock auth." /></span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -80,7 +81,7 @@ export default function AwsSettings() {
|
|||||||
|
|
||||||
{/* AWS Profile */}
|
{/* AWS Profile */}
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Profile</span>
|
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Profile<Tooltip text="AWS named profile to use by default. Per-project settings can override this." /></span>
|
||||||
<select
|
<select
|
||||||
value={globalAws.aws_profile ?? ""}
|
value={globalAws.aws_profile ?? ""}
|
||||||
onChange={(e) => handleChange("aws_profile", e.target.value)}
|
onChange={(e) => handleChange("aws_profile", e.target.value)}
|
||||||
@@ -95,7 +96,7 @@ export default function AwsSettings() {
|
|||||||
|
|
||||||
{/* AWS Region */}
|
{/* AWS Region */}
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Region</span>
|
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Region<Tooltip text="Default AWS region for Bedrock API calls (e.g. us-east-1). Can be overridden per project." /></span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={globalAws.aws_region ?? ""}
|
value={globalAws.aws_region ?? ""}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { useDocker } from "../../hooks/useDocker";
|
import { useDocker } from "../../hooks/useDocker";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
import type { ImageSource } from "../../lib/types";
|
import type { ImageSource } from "../../lib/types";
|
||||||
|
import Tooltip from "../ui/Tooltip";
|
||||||
|
|
||||||
const REGISTRY_IMAGE = "repo.anhonesthost.net/cybercovellc/triple-c/triple-c-sandbox:latest";
|
const REGISTRY_IMAGE = "repo.anhonesthost.net/cybercovellc/triple-c/triple-c-sandbox:latest";
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ export default function DockerSettings() {
|
|||||||
|
|
||||||
{/* Image Source Selector */}
|
{/* Image Source Selector */}
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--text-secondary)] text-xs block mb-1.5">Image Source</span>
|
<span className="text-[var(--text-secondary)] text-xs block mb-1.5">Image Source<Tooltip text="Registry pulls the pre-built image. Local Build compiles from the bundled Dockerfile. Custom lets you specify any image." /></span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{IMAGE_SOURCE_OPTIONS.map((opt) => (
|
{IMAGE_SOURCE_OPTIONS.map((opt) => (
|
||||||
<button
|
<button
|
||||||
@@ -109,7 +110,7 @@ export default function DockerSettings() {
|
|||||||
{/* Custom image input */}
|
{/* Custom image input */}
|
||||||
{imageSource === "custom" && (
|
{imageSource === "custom" && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Custom Image</span>
|
<span className="text-[var(--text-secondary)] text-xs block mb-1">Custom Image<Tooltip text="Full image name including registry and tag (e.g. myregistry.com/image:tag)." /></span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customInput}
|
value={customInput}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
|
|||||||
import EnvVarsModal from "../projects/EnvVarsModal";
|
import EnvVarsModal from "../projects/EnvVarsModal";
|
||||||
import { detectHostTimezone } from "../../lib/tauri-commands";
|
import { detectHostTimezone } from "../../lib/tauri-commands";
|
||||||
import type { EnvVar } from "../../lib/types";
|
import type { EnvVar } from "../../lib/types";
|
||||||
|
import Tooltip from "../ui/Tooltip";
|
||||||
|
|
||||||
export default function SettingsPanel() {
|
export default function SettingsPanel() {
|
||||||
const { appSettings, saveSettings } = useSettings();
|
const { appSettings, saveSettings } = useSettings();
|
||||||
@@ -59,7 +60,7 @@ export default function SettingsPanel() {
|
|||||||
|
|
||||||
{/* Container Timezone */}
|
{/* Container Timezone */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Container Timezone</label>
|
<label className="block text-sm font-medium mb-1">Container Timezone<Tooltip text="Sets the timezone inside containers. Affects scheduled task timing and log timestamps." /></label>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
Timezone for containers — affects scheduled task timing (IANA format, e.g. America/New_York)
|
Timezone for containers — affects scheduled task timing (IANA format, e.g. America/New_York)
|
||||||
</p>
|
</p>
|
||||||
@@ -79,7 +80,7 @@ export default function SettingsPanel() {
|
|||||||
|
|
||||||
{/* Global Claude Instructions */}
|
{/* Global Claude Instructions */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Claude Instructions</label>
|
<label className="block text-sm font-medium mb-1">Claude Instructions<Tooltip text="Global instructions applied to all projects. Written to ~/.claude/CLAUDE.md in every container." /></label>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
Global instructions applied to all projects (written to ~/.claude/CLAUDE.md in containers)
|
Global instructions applied to all projects (written to ~/.claude/CLAUDE.md in containers)
|
||||||
</p>
|
</p>
|
||||||
@@ -98,7 +99,7 @@ export default function SettingsPanel() {
|
|||||||
|
|
||||||
{/* Global Environment Variables */}
|
{/* Global Environment Variables */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Global Environment Variables</label>
|
<label className="block text-sm font-medium mb-1">Global Environment Variables<Tooltip text="Env vars injected into all containers. Per-project vars with the same key take precedence." /></label>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
Applied to all project containers. Per-project variables override global ones with the same key.
|
Applied to all project containers. Per-project variables override global ones with the same key.
|
||||||
</p>
|
</p>
|
||||||
@@ -117,7 +118,7 @@ export default function SettingsPanel() {
|
|||||||
|
|
||||||
{/* Updates section */}
|
{/* Updates section */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2">Updates</label>
|
<label className="block text-sm font-medium mb-2">Updates<Tooltip text="Check for new versions of the Triple-C app and container image." /></label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{appVersion && (
|
{appVersion && (
|
||||||
<p className="text-xs text-[var(--text-secondary)]">
|
<p className="text-xs text-[var(--text-secondary)]">
|
||||||
|
|||||||
78
app/src/components/ui/Tooltip.tsx
Normal file
78
app/src/components/ui/Tooltip.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useState, useRef, useEffect, type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
text: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small circled question-mark icon that shows a tooltip on hover.
|
||||||
|
* Renders inline and automatically repositions to stay within the viewport.
|
||||||
|
*/
|
||||||
|
export default function Tooltip({ text, children }: TooltipProps) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [position, setPosition] = useState<"top" | "bottom">("top");
|
||||||
|
const [align, setAlign] = useState<"center" | "left" | "right">("center");
|
||||||
|
const triggerRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible || !triggerRef.current || !tooltipRef.current) return;
|
||||||
|
|
||||||
|
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||||
|
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Decide vertical position: prefer top, fall back to bottom
|
||||||
|
if (triggerRect.top - tooltipRect.height - 6 < 4) {
|
||||||
|
setPosition("bottom");
|
||||||
|
} else {
|
||||||
|
setPosition("top");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide horizontal alignment
|
||||||
|
const centerLeft = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
||||||
|
const centerRight = centerLeft + tooltipRect.width;
|
||||||
|
if (centerLeft < 4) {
|
||||||
|
setAlign("left");
|
||||||
|
} else if (centerRight > window.innerWidth - 4) {
|
||||||
|
setAlign("right");
|
||||||
|
} else {
|
||||||
|
setAlign("center");
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
const positionClasses = position === "top" ? "bottom-full mb-1.5" : "top-full mt-1.5";
|
||||||
|
|
||||||
|
const alignClasses =
|
||||||
|
align === "left"
|
||||||
|
? "left-0"
|
||||||
|
: align === "right"
|
||||||
|
? "right-0"
|
||||||
|
: "left-1/2 -translate-x-1/2";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
ref={triggerRef}
|
||||||
|
className="relative inline-flex items-center ml-1"
|
||||||
|
onMouseEnter={() => setVisible(true)}
|
||||||
|
onMouseLeave={() => setVisible(false)}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-[var(--text-secondary)] text-[var(--text-secondary)] text-[9px] leading-none cursor-help select-none hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors"
|
||||||
|
aria-label="Help"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
ref={tooltipRef}
|
||||||
|
className={`absolute z-50 ${positionClasses} ${alignClasses} px-2.5 py-1.5 text-[11px] leading-snug text-[var(--text-primary)] bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded shadow-lg whitespace-normal max-w-[220px] w-max pointer-events-none`}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -53,3 +53,135 @@ body {
|
|||||||
to { opacity: 1; transform: translate(-50%, 0); }
|
to { opacity: 1; transform: translate(-50%, 0); }
|
||||||
}
|
}
|
||||||
.animate-slide-down { animation: slide-down 0.2s ease-out; }
|
.animate-slide-down { animation: slide-down 0.2s ease-out; }
|
||||||
|
|
||||||
|
/* Help dialog content styles */
|
||||||
|
.help-content {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-h2 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 1.5rem 0 0.75rem 0;
|
||||||
|
padding-bottom: 0.375rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-h3 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 1.25rem 0 0.5rem 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 1rem 0 0.375rem 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-ul,
|
||||||
|
.help-content .help-ol {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-ul li,
|
||||||
|
.help-content .help-ol li {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-code-block {
|
||||||
|
display: block;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-inline-code {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-table th,
|
||||||
|
.help-content .help-table td {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-table th {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-table td {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-blockquote {
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-link {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content .help-link:hover {
|
||||||
|
color: var(--accent-hover);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user