Compare commits

...

3 Commits

Author SHA1 Message Date
5977024953 Update Ollama docs and UI to mark model as required
All checks were successful
Build App / compute-version (push) Successful in 4s
Build App / build-macos (push) Successful in 2m22s
Build App / build-windows (push) Successful in 3m25s
Build App / build-linux (push) Successful in 4m48s
Build App / create-tag (push) Successful in 9s
Build App / sync-to-github (push) Successful in 14s
The model field must be set and the model must be pre-pulled in Ollama
before the container will work. Updated README, HOW-TO-USE, and the
ProjectCard UI label/tooltip to reflect this.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:53:24 -07:00
27007b90e3 Fetch help content from repo, add TOC and marketplace troubleshooting
All checks were successful
Build App / compute-version (push) Successful in 6s
Build App / build-macos (push) Successful in 2m21s
Build App / build-windows (push) Successful in 3m57s
Build App / build-linux (push) Successful in 5m2s
Build App / create-tag (push) Successful in 5s
Build App / sync-to-github (push) Successful in 10s
Help dialog now fetches HOW-TO-USE.md live from the gitea repo on open,
falling back to the compile-time embedded copy when offline. Content is
cached for the session. Removes the ~600-line hardcoded markdown constant
from HelpDialog.tsx in favor of a single source of truth.

Adds a Table of Contents with anchor links for quick navigation and a new
troubleshooting entry for the "Failed to install Anthropic marketplace"
error with the jq fix. Markdown renderer updated to support anchor links
and header id attributes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:00:59 -07:00
38e65619e9 Fix tooltips clipped by overflow containers, improve Backend tooltip text
All checks were successful
Build App / compute-version (push) Successful in 4s
Build App / build-macos (push) Successful in 2m21s
Build App / build-windows (push) Successful in 3m28s
Build App / build-linux (push) Successful in 5m44s
Build App / create-tag (push) Successful in 6s
Build App / sync-to-github (push) Successful in 12s
Rewrite Tooltip to use React portal (createPortal to document.body) so
tooltips render above all UI elements regardless of ancestor overflow:hidden.
Also increased max-width from 220px to 280px for longer descriptions.

Expanded Backend tooltip to explain each option (Anthropic, Bedrock,
Ollama, LiteLLM) with practical context for new users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:56:50 -07:00
9 changed files with 208 additions and 667 deletions

View File

@@ -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.

View File

@@ -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.

View 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))
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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>
); );

View File

@@ -449,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:<Tooltip text="Anthropic = direct Claude API via OAuth. Bedrock = AWS Bedrock. Ollama = local models. LiteLLM = proxy gateway for 100+ providers." /></span> <span className="text-[var(--text-secondary)] mr-1">Backend:<Tooltip text="Choose the AI model provider for this project. Anthropic: Connect directly to Claude via OAuth login (run 'claude login' in terminal). Bedrock: Route through AWS Bedrock using your AWS credentials. Ollama: Use locally-hosted open-source models (Llama, Mistral, etc.) via an Ollama server. LiteLLM: Connect through a LiteLLM proxy gateway to access 100+ model providers (OpenAI, Azure, Gemini, etc.)." /></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); }}
@@ -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)}

View File

@@ -1,4 +1,5 @@
import { useState, useRef, useEffect, type ReactNode } from "react"; import { useState, useRef, useLayoutEffect, type ReactNode } from "react";
import { createPortal } from "react-dom";
interface TooltipProps { interface TooltipProps {
text: string; text: string;
@@ -7,53 +8,44 @@ interface TooltipProps {
/** /**
* A small circled question-mark icon that shows a tooltip on hover. * A small circled question-mark icon that shows a tooltip on hover.
* Renders inline and automatically repositions to stay within the viewport. * Uses a portal to render at `document.body` so the tooltip is never
* clipped by ancestor `overflow: hidden` containers.
*/ */
export default function Tooltip({ text, children }: TooltipProps) { export default function Tooltip({ text, children }: TooltipProps) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [position, setPosition] = useState<"top" | "bottom">("top"); const [coords, setCoords] = useState({ top: 0, left: 0 });
const [align, setAlign] = useState<"center" | "left" | "right">("center"); const [, setPlacement] = useState<"top" | "bottom">("top");
const triggerRef = useRef<HTMLSpanElement>(null); const triggerRef = useRef<HTMLSpanElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null); const tooltipRef = useRef<HTMLDivElement>(null);
useEffect(() => { useLayoutEffect(() => {
if (!visible || !triggerRef.current || !tooltipRef.current) return; if (!visible || !triggerRef.current || !tooltipRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect(); const trigger = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect(); const tooltip = tooltipRef.current.getBoundingClientRect();
const gap = 6;
// Decide vertical position: prefer top, fall back to bottom // Vertical: prefer above, fall back to below
if (triggerRect.top - tooltipRect.height - 6 < 4) { const above = trigger.top - tooltip.height - gap >= 4;
setPosition("bottom"); const pos = above ? "top" : "bottom";
} else { setPlacement(pos);
setPosition("top");
}
// Decide horizontal alignment const top =
const centerLeft = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2; pos === "top"
const centerRight = centerLeft + tooltipRect.width; ? trigger.top - tooltip.height - gap
if (centerLeft < 4) { : trigger.bottom + gap;
setAlign("left");
} else if (centerRight > window.innerWidth - 4) { // Horizontal: center on trigger, clamp to viewport
setAlign("right"); let left = trigger.left + trigger.width / 2 - tooltip.width / 2;
} else { left = Math.max(4, Math.min(left, window.innerWidth - tooltip.width - 4));
setAlign("center");
} setCoords({ top, left });
}, [visible]); }, [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 ( return (
<span <span
ref={triggerRef} ref={triggerRef}
className="relative inline-flex items-center ml-1" className="inline-flex items-center ml-1"
onMouseEnter={() => setVisible(true)} onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)} onMouseLeave={() => setVisible(false)}
> >
@@ -65,14 +57,22 @@ export default function Tooltip({ text, children }: TooltipProps) {
? ?
</span> </span>
)} )}
{visible && ( {visible &&
<div createPortal(
ref={tooltipRef} <div
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`} ref={tooltipRef}
> style={{
{text} position: "fixed",
</div> top: coords.top,
)} left: coords.left,
zIndex: 9999,
}}
className={`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-[280px] w-max pointer-events-none`}
>
{text}
</div>,
document.body
)}
</span> </span>
); );
} }

View File

@@ -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");