Compare commits
5 Commits
v0.2.7-win
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d7d7a83aec | |||
| 879322bc9a | |||
| ecaa42fa77 | |||
| 280358166a | |||
| 4732feb33e |
@@ -72,7 +72,7 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
|||||||
- `container.rs` — Container lifecycle (create, start, stop, remove, inspect)
|
- `container.rs` — Container lifecycle (create, start, stop, remove, inspect)
|
||||||
- `exec.rs` — PTY exec sessions with bidirectional stdin/stdout streaming
|
- `exec.rs` — PTY exec sessions with bidirectional stdin/stdout streaming
|
||||||
- `image.rs` — Image build/pull with progress streaming
|
- `image.rs` — Image build/pull with progress streaming
|
||||||
- **`models/`** — Serde structs (`Project`, `Backend`, `BedrockConfig`, `OllamaConfig`, `LiteLlmConfig`, `ContainerInfo`, `AppSettings`). These define the IPC contract with the frontend.
|
- **`models/`** — Serde structs (`Project`, `Backend`, `BedrockConfig`, `OllamaConfig`, `OpenAiCompatibleConfig`, `ContainerInfo`, `AppSettings`). These define the IPC contract with the frontend.
|
||||||
- **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs`
|
- **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs`
|
||||||
|
|
||||||
### Container (`container/`)
|
### Container (`container/`)
|
||||||
@@ -91,7 +91,7 @@ Per-project, independently configured:
|
|||||||
- **Anthropic (OAuth)** — `claude login` in terminal, token persists in config volume
|
- **Anthropic (OAuth)** — `claude login` in terminal, token persists in config volume
|
||||||
- **AWS Bedrock** — Static keys, profile, or bearer token injected as env vars
|
- **AWS Bedrock** — Static keys, profile, or bearer token injected as env vars
|
||||||
- **Ollama** — Connect to a local or remote Ollama server via `ANTHROPIC_BASE_URL` (e.g., `http://host.docker.internal:11434`)
|
- **Ollama** — Connect to a local or remote Ollama server via `ANTHROPIC_BASE_URL` (e.g., `http://host.docker.internal:11434`)
|
||||||
- **LiteLLM** — Connect through a LiteLLM proxy gateway via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` to access 100+ model providers
|
- **OpenAI Compatible** — Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, etc.) via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Triple-C (Claude-Code-Container) is a desktop application that runs Claude Code
|
|||||||
- [MCP Servers (Beta)](#mcp-servers-beta)
|
- [MCP Servers (Beta)](#mcp-servers-beta)
|
||||||
- [AWS Bedrock Configuration](#aws-bedrock-configuration)
|
- [AWS Bedrock Configuration](#aws-bedrock-configuration)
|
||||||
- [Ollama Configuration](#ollama-configuration)
|
- [Ollama Configuration](#ollama-configuration)
|
||||||
- [LiteLLM Configuration](#litellm-configuration)
|
- [OpenAI Compatible Configuration](#openai-compatible-configuration)
|
||||||
- [Settings](#settings)
|
- [Settings](#settings)
|
||||||
- [Terminal Features](#terminal-features)
|
- [Terminal Features](#terminal-features)
|
||||||
- [Scheduled Tasks (Inside the Container)](#scheduled-tasks-inside-the-container)
|
- [Scheduled Tasks (Inside the Container)](#scheduled-tasks-inside-the-container)
|
||||||
@@ -53,7 +53,7 @@ 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
|
- **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
|
- **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)
|
- **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)
|
- **OpenAI Compatible** — Any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.) (best-effort support)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -117,11 +117,11 @@ Claude Code launches automatically with `--dangerously-skip-permissions` inside
|
|||||||
4. Make sure the model has been pulled in Ollama (e.g., `ollama pull qwen3.5:27b`) or used via Ollama cloud before starting.
|
4. Make sure the model has been pulled in Ollama (e.g., `ollama pull qwen3.5:27b`) or used via Ollama cloud before starting.
|
||||||
5. Start the container again.
|
5. Start the container again.
|
||||||
|
|
||||||
**LiteLLM:**
|
**OpenAI Compatible:**
|
||||||
|
|
||||||
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 **LiteLLM**.
|
2. In the project card, switch the backend to **OpenAI Compatible**.
|
||||||
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.
|
3. Expand the **Config** panel and set the base URL of your OpenAI-compatible endpoint (defaults to `http://host.docker.internal:4000` as an example). Optionally set an API key and model ID.
|
||||||
4. Start the container again.
|
4. Start the container again.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -427,21 +427,21 @@ Triple-C sets `ANTHROPIC_BASE_URL` to point Claude Code at your Ollama server in
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## LiteLLM Configuration
|
## OpenAI Compatible Configuration
|
||||||
|
|
||||||
To use Claude Code through a [LiteLLM](https://docs.litellm.ai/) 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.
|
To use Claude Code through any OpenAI API-compatible endpoint, switch the backend to **OpenAI Compatible** on the project card. This works with any server that exposes an OpenAI-compatible API, including LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, and others.
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
- **Base URL** — The URL of your LiteLLM proxy. Defaults to `http://host.docker.internal:4000` for a locally running proxy.
|
- **Base URL** — The URL of your OpenAI-compatible endpoint. Defaults to `http://host.docker.internal:4000` as an example (adjust to match your server's address and port).
|
||||||
- **API Key** — Optional. The API key for your LiteLLM proxy, if authentication is required. Stored securely in your OS keychain.
|
- **API Key** — Optional. The API key for your endpoint, if authentication is required. Stored securely in your OS keychain.
|
||||||
- **Model ID** — Optional. Override the model to use.
|
- **Model ID** — Optional. Override the model to use.
|
||||||
|
|
||||||
### How It Works
|
### 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`.
|
Triple-C sets `ANTHROPIC_BASE_URL` to point Claude Code at your OpenAI-compatible endpoint. 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.
|
> **Note:** OpenAI Compatible 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 endpoint.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -494,6 +494,10 @@ When Claude Code prints a long URL (e.g., during `claude login`), Triple-C detec
|
|||||||
|
|
||||||
Shorter URLs in terminal output are also clickable directly.
|
Shorter URLs in terminal output are also clickable directly.
|
||||||
|
|
||||||
|
### Copying and Pasting
|
||||||
|
|
||||||
|
Use **Ctrl+Shift+C** (or **Cmd+C** on macOS) to copy selected text from the terminal, and **Ctrl+Shift+V** (or **Cmd+V** on macOS) to paste. This follows standard terminal emulator conventions since Ctrl+C is reserved for sending SIGINT.
|
||||||
|
|
||||||
### Clipboard Support (OSC 52)
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ 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`). Requires a model ID, and the model must be pulled (or used via Ollama cloud) before starting the container.
|
- **Ollama**: Connect to a local or remote Ollama server via `ANTHROPIC_BASE_URL` (e.g., `http://host.docker.internal:11434`). Requires a model ID, and the model must be pulled (or used via Ollama cloud) before starting the container.
|
||||||
- **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.
|
- **OpenAI Compatible**: Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.) via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`. API key stored securely in OS keychain.
|
||||||
|
|
||||||
> **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 OpenAI Compatible 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.
|
||||||
|
|
||||||
### Container Spawning (Sibling Containers)
|
### Container Spawning (Sibling Containers)
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ fn store_secrets_for_project(project: &Project) -> Result<(), String> {
|
|||||||
secure::store_project_secret(&project.id, "aws-bearer-token", v)?;
|
secure::store_project_secret(&project.id, "aws-bearer-token", v)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(ref litellm) = project.litellm_config {
|
if let Some(ref oai_config) = project.openai_compatible_config {
|
||||||
if let Some(ref v) = litellm.api_key {
|
if let Some(ref v) = oai_config.api_key {
|
||||||
secure::store_project_secret(&project.id, "litellm-api-key", v)?;
|
secure::store_project_secret(&project.id, "openai-compatible-api-key", v)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -56,8 +56,8 @@ fn load_secrets_for_project(project: &mut Project) {
|
|||||||
bedrock.aws_bearer_token = secure::get_project_secret(&project.id, "aws-bearer-token")
|
bedrock.aws_bearer_token = secure::get_project_secret(&project.id, "aws-bearer-token")
|
||||||
.unwrap_or(None);
|
.unwrap_or(None);
|
||||||
}
|
}
|
||||||
if let Some(ref mut litellm) = project.litellm_config {
|
if let Some(ref mut oai_config) = project.openai_compatible_config {
|
||||||
litellm.api_key = secure::get_project_secret(&project.id, "litellm-api-key")
|
oai_config.api_key = secure::get_project_secret(&project.id, "openai-compatible-api-key")
|
||||||
.unwrap_or(None);
|
.unwrap_or(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,11 +197,11 @@ pub async fn start_project_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if project.backend == Backend::LiteLlm {
|
if project.backend == Backend::OpenAiCompatible {
|
||||||
let litellm = project.litellm_config.as_ref()
|
let oai_config = project.openai_compatible_config.as_ref()
|
||||||
.ok_or_else(|| "LiteLLM backend selected but no LiteLLM configuration found.".to_string())?;
|
.ok_or_else(|| "OpenAI Compatible backend selected but no configuration found.".to_string())?;
|
||||||
if litellm.base_url.is_empty() {
|
if oai_config.base_url.is_empty() {
|
||||||
return Err("LiteLLM base URL is required.".to_string());
|
return Err("OpenAI Compatible base URL is required.".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,13 +244,13 @@ fn compute_ollama_fingerprint(project: &Project) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute a fingerprint for the LiteLLM configuration so we can detect changes.
|
/// Compute a fingerprint for the OpenAI Compatible configuration so we can detect changes.
|
||||||
fn compute_litellm_fingerprint(project: &Project) -> String {
|
fn compute_openai_compatible_fingerprint(project: &Project) -> String {
|
||||||
if let Some(ref litellm) = project.litellm_config {
|
if let Some(ref config) = project.openai_compatible_config {
|
||||||
let parts = vec![
|
let parts = vec![
|
||||||
litellm.base_url.clone(),
|
config.base_url.clone(),
|
||||||
litellm.api_key.as_deref().unwrap_or("").to_string(),
|
config.api_key.as_deref().unwrap_or("").to_string(),
|
||||||
litellm.model_id.as_deref().unwrap_or("").to_string(),
|
config.model_id.as_deref().unwrap_or("").to_string(),
|
||||||
];
|
];
|
||||||
sha256_hex(&parts.join("|"))
|
sha256_hex(&parts.join("|"))
|
||||||
} else {
|
} else {
|
||||||
@@ -516,14 +516,14 @@ pub async fn create_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LiteLLM configuration
|
// OpenAI Compatible configuration
|
||||||
if project.backend == Backend::LiteLlm {
|
if project.backend == Backend::OpenAiCompatible {
|
||||||
if let Some(ref litellm) = project.litellm_config {
|
if let Some(ref config) = project.openai_compatible_config {
|
||||||
env_vars.push(format!("ANTHROPIC_BASE_URL={}", litellm.base_url));
|
env_vars.push(format!("ANTHROPIC_BASE_URL={}", config.base_url));
|
||||||
if let Some(ref key) = litellm.api_key {
|
if let Some(ref key) = config.api_key {
|
||||||
env_vars.push(format!("ANTHROPIC_AUTH_TOKEN={}", key));
|
env_vars.push(format!("ANTHROPIC_AUTH_TOKEN={}", key));
|
||||||
}
|
}
|
||||||
if let Some(ref model) = litellm.model_id {
|
if let Some(ref model) = config.model_id {
|
||||||
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
|
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,7 +698,7 @@ pub async fn create_container(
|
|||||||
labels.insert("triple-c.paths-fingerprint".to_string(), compute_paths_fingerprint(&project.paths));
|
labels.insert("triple-c.paths-fingerprint".to_string(), compute_paths_fingerprint(&project.paths));
|
||||||
labels.insert("triple-c.bedrock-fingerprint".to_string(), compute_bedrock_fingerprint(project));
|
labels.insert("triple-c.bedrock-fingerprint".to_string(), compute_bedrock_fingerprint(project));
|
||||||
labels.insert("triple-c.ollama-fingerprint".to_string(), compute_ollama_fingerprint(project));
|
labels.insert("triple-c.ollama-fingerprint".to_string(), compute_ollama_fingerprint(project));
|
||||||
labels.insert("triple-c.litellm-fingerprint".to_string(), compute_litellm_fingerprint(project));
|
labels.insert("triple-c.openai-compatible-fingerprint".to_string(), compute_openai_compatible_fingerprint(project));
|
||||||
labels.insert("triple-c.ports-fingerprint".to_string(), compute_ports_fingerprint(&project.port_mappings));
|
labels.insert("triple-c.ports-fingerprint".to_string(), compute_ports_fingerprint(&project.port_mappings));
|
||||||
labels.insert("triple-c.image".to_string(), image_name.to_string());
|
labels.insert("triple-c.image".to_string(), image_name.to_string());
|
||||||
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
|
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
|
||||||
@@ -948,11 +948,11 @@ pub async fn container_needs_recreation(
|
|||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LiteLLM config fingerprint ───────────────────────────────────────
|
// ── OpenAI Compatible config fingerprint ────────────────────────────
|
||||||
let expected_litellm_fp = compute_litellm_fingerprint(project);
|
let expected_oai_fp = compute_openai_compatible_fingerprint(project);
|
||||||
let container_litellm_fp = get_label("triple-c.litellm-fingerprint").unwrap_or_default();
|
let container_oai_fp = get_label("triple-c.openai-compatible-fingerprint").unwrap_or_default();
|
||||||
if container_litellm_fp != expected_litellm_fp {
|
if container_oai_fp != expected_oai_fp {
|
||||||
log::info!("LiteLLM config mismatch");
|
log::info!("OpenAI Compatible config mismatch");
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ pub struct Project {
|
|||||||
pub backend: Backend,
|
pub backend: Backend,
|
||||||
pub bedrock_config: Option<BedrockConfig>,
|
pub bedrock_config: Option<BedrockConfig>,
|
||||||
pub ollama_config: Option<OllamaConfig>,
|
pub ollama_config: Option<OllamaConfig>,
|
||||||
pub litellm_config: Option<LiteLlmConfig>,
|
#[serde(alias = "litellm_config")]
|
||||||
|
pub openai_compatible_config: Option<OpenAiCompatibleConfig>,
|
||||||
pub allow_docker_access: bool,
|
pub allow_docker_access: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mission_control_enabled: bool,
|
pub mission_control_enabled: bool,
|
||||||
@@ -70,7 +71,7 @@ pub enum ProjectStatus {
|
|||||||
/// - `Anthropic`: Direct Anthropic API (user runs `claude login` inside the container)
|
/// - `Anthropic`: Direct Anthropic API (user runs `claude login` inside the container)
|
||||||
/// - `Bedrock`: AWS Bedrock with per-project AWS credentials
|
/// - `Bedrock`: AWS Bedrock with per-project AWS credentials
|
||||||
/// - `Ollama`: Local or remote Ollama server
|
/// - `Ollama`: Local or remote Ollama server
|
||||||
/// - `LiteLlm`: LiteLLM proxy gateway for 100+ model providers
|
/// - `OpenAiCompatible`: Any OpenAI API-compatible endpoint (e.g., LiteLLM, vLLM, etc.)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Backend {
|
pub enum Backend {
|
||||||
@@ -79,8 +80,8 @@ pub enum Backend {
|
|||||||
Anthropic,
|
Anthropic,
|
||||||
Bedrock,
|
Bedrock,
|
||||||
Ollama,
|
Ollama,
|
||||||
#[serde(alias = "litellm")]
|
#[serde(alias = "lite_llm", alias = "litellm")]
|
||||||
LiteLlm,
|
OpenAiCompatible,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Backend {
|
impl Default for Backend {
|
||||||
@@ -132,13 +133,14 @@ pub struct OllamaConfig {
|
|||||||
pub model_id: Option<String>,
|
pub model_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// LiteLLM gateway configuration for a project.
|
/// OpenAI Compatible endpoint configuration for a project.
|
||||||
/// LiteLLM translates Anthropic API calls to 100+ model providers.
|
/// Routes Anthropic API calls through any OpenAI API-compatible endpoint
|
||||||
|
/// (e.g., LiteLLM, vLLM, or other compatible gateways).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LiteLlmConfig {
|
pub struct OpenAiCompatibleConfig {
|
||||||
/// The base URL of the LiteLLM proxy (e.g., "http://host.docker.internal:4000" or "https://litellm.example.com")
|
/// The base URL of the OpenAI-compatible endpoint (e.g., "http://host.docker.internal:4000" or "https://api.example.com")
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
/// API key for the LiteLLM proxy
|
/// API key for the OpenAI-compatible endpoint
|
||||||
#[serde(skip_serializing, default)]
|
#[serde(skip_serializing, default)]
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
/// Optional model override
|
/// Optional model override
|
||||||
@@ -157,7 +159,7 @@ impl Project {
|
|||||||
backend: Backend::default(),
|
backend: Backend::default(),
|
||||||
bedrock_config: None,
|
bedrock_config: None,
|
||||||
ollama_config: None,
|
ollama_config: None,
|
||||||
litellm_config: None,
|
openai_compatible_config: None,
|
||||||
allow_docker_access: false,
|
allow_docker_access: false,
|
||||||
mission_control_enabled: false,
|
mission_control_enabled: false,
|
||||||
ssh_key_path: None,
|
ssh_key_path: None,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useShallow } from "zustand/react/shallow";
|
|||||||
import { useAppState } from "../../store/appState";
|
import { useAppState } from "../../store/appState";
|
||||||
|
|
||||||
export default function StatusBar() {
|
export default function StatusBar() {
|
||||||
const { projects, sessions } = useAppState(
|
const { projects, sessions, terminalHasSelection } = useAppState(
|
||||||
useShallow(s => ({ projects: s.projects, sessions: s.sessions }))
|
useShallow(s => ({ projects: s.projects, sessions: s.sessions, terminalHasSelection: s.terminalHasSelection }))
|
||||||
);
|
);
|
||||||
const running = projects.filter((p) => p.status === "running").length;
|
const running = projects.filter((p) => p.status === "running").length;
|
||||||
|
|
||||||
@@ -20,6 +20,12 @@ export default function StatusBar() {
|
|||||||
<span>
|
<span>
|
||||||
{sessions.length} terminal{sessions.length !== 1 ? "s" : ""}
|
{sessions.length} terminal{sessions.length !== 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
|
{terminalHasSelection && (
|
||||||
|
<>
|
||||||
|
<span className="mx-2">|</span>
|
||||||
|
<span className="text-[var(--accent)]">Ctrl+Shift+C to copy</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import type { Project, ProjectPath, Backend, BedrockConfig, BedrockAuthMethod, OllamaConfig, LiteLlmConfig } from "../../lib/types";
|
import type { Project, ProjectPath, Backend, BedrockConfig, BedrockAuthMethod, OllamaConfig, OpenAiCompatibleConfig } from "../../lib/types";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useMcpServers } from "../../hooks/useMcpServers";
|
import { useMcpServers } from "../../hooks/useMcpServers";
|
||||||
import { useTerminal } from "../../hooks/useTerminal";
|
import { useTerminal } from "../../hooks/useTerminal";
|
||||||
@@ -63,10 +63,10 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||||
const [ollamaModelId, setOllamaModelId] = useState(project.ollama_config?.model_id ?? "");
|
const [ollamaModelId, setOllamaModelId] = useState(project.ollama_config?.model_id ?? "");
|
||||||
|
|
||||||
// LiteLLM local state
|
// OpenAI Compatible local state
|
||||||
const [litellmBaseUrl, setLitellmBaseUrl] = useState(project.litellm_config?.base_url ?? "http://host.docker.internal:4000");
|
const [openaiCompatibleBaseUrl, setOpenaiCompatibleBaseUrl] = useState(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
|
||||||
const [litellmApiKey, setLitellmApiKey] = useState(project.litellm_config?.api_key ?? "");
|
const [openaiCompatibleApiKey, setOpenaiCompatibleApiKey] = useState(project.openai_compatible_config?.api_key ?? "");
|
||||||
const [litellmModelId, setLitellmModelId] = useState(project.litellm_config?.model_id ?? "");
|
const [openaiCompatibleModelId, setOpenaiCompatibleModelId] = useState(project.openai_compatible_config?.model_id ?? "");
|
||||||
|
|
||||||
// Sync local state when project prop changes (e.g., after save or external update)
|
// Sync local state when project prop changes (e.g., after save or external update)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -88,9 +88,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
||||||
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||||
setOllamaModelId(project.ollama_config?.model_id ?? "");
|
setOllamaModelId(project.ollama_config?.model_id ?? "");
|
||||||
setLitellmBaseUrl(project.litellm_config?.base_url ?? "http://host.docker.internal:4000");
|
setOpenaiCompatibleBaseUrl(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
|
||||||
setLitellmApiKey(project.litellm_config?.api_key ?? "");
|
setOpenaiCompatibleApiKey(project.openai_compatible_config?.api_key ?? "");
|
||||||
setLitellmModelId(project.litellm_config?.model_id ?? "");
|
setOpenaiCompatibleModelId(project.openai_compatible_config?.model_id ?? "");
|
||||||
}, [project]);
|
}, [project]);
|
||||||
|
|
||||||
// Listen for container progress events
|
// Listen for container progress events
|
||||||
@@ -197,7 +197,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
model_id: null,
|
model_id: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultLiteLlmConfig: LiteLlmConfig = {
|
const defaultOpenAiCompatibleConfig: OpenAiCompatibleConfig = {
|
||||||
base_url: "http://host.docker.internal:4000",
|
base_url: "http://host.docker.internal:4000",
|
||||||
api_key: null,
|
api_key: null,
|
||||||
model_id: null,
|
model_id: null,
|
||||||
@@ -212,8 +212,8 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
if (mode === "ollama" && !project.ollama_config) {
|
if (mode === "ollama" && !project.ollama_config) {
|
||||||
updates.ollama_config = defaultOllamaConfig;
|
updates.ollama_config = defaultOllamaConfig;
|
||||||
}
|
}
|
||||||
if (mode === "lite_llm" && !project.litellm_config) {
|
if (mode === "open_ai_compatible" && !project.openai_compatible_config) {
|
||||||
updates.litellm_config = defaultLiteLlmConfig;
|
updates.openai_compatible_config = defaultOpenAiCompatibleConfig;
|
||||||
}
|
}
|
||||||
await update({ ...project, ...updates });
|
await update({ ...project, ...updates });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -355,30 +355,30 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLitellmBaseUrlBlur = async () => {
|
const handleOpenaiCompatibleBaseUrlBlur = async () => {
|
||||||
try {
|
try {
|
||||||
const current = project.litellm_config ?? defaultLiteLlmConfig;
|
const current = project.openai_compatible_config ?? defaultOpenAiCompatibleConfig;
|
||||||
await update({ ...project, litellm_config: { ...current, base_url: litellmBaseUrl } });
|
await update({ ...project, openai_compatible_config: { ...current, base_url: openaiCompatibleBaseUrl } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to update LiteLLM base URL:", err);
|
console.error("Failed to update OpenAI Compatible base URL:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLitellmApiKeyBlur = async () => {
|
const handleOpenaiCompatibleApiKeyBlur = async () => {
|
||||||
try {
|
try {
|
||||||
const current = project.litellm_config ?? defaultLiteLlmConfig;
|
const current = project.openai_compatible_config ?? defaultOpenAiCompatibleConfig;
|
||||||
await update({ ...project, litellm_config: { ...current, api_key: litellmApiKey || null } });
|
await update({ ...project, openai_compatible_config: { ...current, api_key: openaiCompatibleApiKey || null } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to update LiteLLM API key:", err);
|
console.error("Failed to update OpenAI Compatible API key:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLitellmModelIdBlur = async () => {
|
const handleOpenaiCompatibleModelIdBlur = async () => {
|
||||||
try {
|
try {
|
||||||
const current = project.litellm_config ?? defaultLiteLlmConfig;
|
const current = project.openai_compatible_config ?? defaultOpenAiCompatibleConfig;
|
||||||
await update({ ...project, litellm_config: { ...current, model_id: litellmModelId || null } });
|
await update({ ...project, openai_compatible_config: { ...current, model_id: openaiCompatibleModelId || null } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to update LiteLLM model ID:", err);
|
console.error("Failed to update OpenAI Compatible model ID:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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="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>
|
<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. OpenAI Compatible: Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, etc.) to access 100+ model 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); }}
|
||||||
@@ -460,7 +460,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<option value="anthropic">Anthropic</option>
|
<option value="anthropic">Anthropic</option>
|
||||||
<option value="bedrock">Bedrock</option>
|
<option value="bedrock">Bedrock</option>
|
||||||
<option value="ollama">Ollama</option>
|
<option value="ollama">Ollama</option>
|
||||||
<option value="lite_llm">LiteLLM</option>
|
<option value="open_ai_compatible">OpenAI Compatible</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -956,38 +956,38 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* LiteLLM config */}
|
{/* OpenAI Compatible config */}
|
||||||
{project.backend === "lite_llm" && (() => {
|
{project.backend === "open_ai_compatible" && (() => {
|
||||||
const inputCls = "w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50";
|
const inputCls = "w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50";
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 pt-1 border-t border-[var(--border-color)]">
|
<div className="space-y-2 pt-1 border-t border-[var(--border-color)]">
|
||||||
<label className="block text-xs font-medium text-[var(--text-primary)]">LiteLLM Gateway</label>
|
<label className="block text-xs font-medium text-[var(--text-primary)]">OpenAI Compatible Endpoint</label>
|
||||||
<p className="text-xs text-[var(--text-secondary)]">
|
<p className="text-xs text-[var(--text-secondary)]">
|
||||||
Connect through a LiteLLM proxy to use 100+ model providers.
|
Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, etc.).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL<Tooltip text="URL of your OpenAI API-compatible server. Use host.docker.internal for a locally running service." /></label>
|
||||||
<input
|
<input
|
||||||
value={litellmBaseUrl}
|
value={openaiCompatibleBaseUrl}
|
||||||
onChange={(e) => setLitellmBaseUrl(e.target.value)}
|
onChange={(e) => setOpenaiCompatibleBaseUrl(e.target.value)}
|
||||||
onBlur={handleLitellmBaseUrlBlur}
|
onBlur={handleOpenaiCompatibleBaseUrlBlur}
|
||||||
placeholder="http://host.docker.internal:4000"
|
placeholder="http://host.docker.internal:4000"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-70">
|
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-70">
|
||||||
Use host.docker.internal for local, or a URL for remote/containerized LiteLLM.
|
Use host.docker.internal for local, or a URL for a remote OpenAI-compatible service.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">API Key<Tooltip text="Authentication key for your OpenAI-compatible endpoint, if required." /></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={litellmApiKey}
|
value={openaiCompatibleApiKey}
|
||||||
onChange={(e) => setLitellmApiKey(e.target.value)}
|
onChange={(e) => setOpenaiCompatibleApiKey(e.target.value)}
|
||||||
onBlur={handleLitellmApiKeyBlur}
|
onBlur={handleOpenaiCompatibleApiKeyBlur}
|
||||||
placeholder="sk-..."
|
placeholder="sk-..."
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
@@ -995,11 +995,11 @@ 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="Model identifier as configured in your LiteLLM proxy (e.g. gpt-4o, gemini-pro)." /></label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (optional)<Tooltip text="Model identifier as configured in your provider (e.g. gpt-4o, gemini-pro)." /></label>
|
||||||
<input
|
<input
|
||||||
value={litellmModelId}
|
value={openaiCompatibleModelId}
|
||||||
onChange={(e) => setLitellmModelId(e.target.value)}
|
onChange={(e) => setOpenaiCompatibleModelId(e.target.value)}
|
||||||
onBlur={handleLitellmModelIdBlur}
|
onBlur={handleOpenaiCompatibleModelIdBlur}
|
||||||
placeholder="gpt-4o / gemini-pro / etc."
|
placeholder="gpt-4o / gemini-pro / etc."
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
const webglRef = useRef<WebglAddon | null>(null);
|
const webglRef = useRef<WebglAddon | null>(null);
|
||||||
const detectorRef = useRef<UrlDetector | null>(null);
|
const detectorRef = useRef<UrlDetector | null>(null);
|
||||||
const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal();
|
const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal();
|
||||||
|
const setTerminalHasSelection = useAppState(s => s.setTerminalHasSelection);
|
||||||
|
|
||||||
const ssoBufferRef = useRef("");
|
const ssoBufferRef = useRef("");
|
||||||
const ssoTriggeredRef = useRef(false);
|
const ssoTriggeredRef = useRef(false);
|
||||||
@@ -80,6 +81,22 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
|
|
||||||
term.open(containerRef.current);
|
term.open(containerRef.current);
|
||||||
|
|
||||||
|
// Ctrl+Shift+C copies selected terminal text to clipboard.
|
||||||
|
// This prevents the keystroke from reaching the container (where
|
||||||
|
// Ctrl+C would send SIGINT and cancel running work).
|
||||||
|
term.attachCustomKeyEventHandler((event) => {
|
||||||
|
if (event.type === "keydown" && event.ctrlKey && event.shiftKey && event.key === "C") {
|
||||||
|
const sel = term.getSelection();
|
||||||
|
if (sel) {
|
||||||
|
navigator.clipboard.writeText(sel).catch((e) =>
|
||||||
|
console.error("Ctrl+Shift+C clipboard write failed:", e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false; // prevent xterm from processing this key
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// WebGL addon is loaded/disposed dynamically in the active effect
|
// WebGL addon is loaded/disposed dynamically in the active effect
|
||||||
// to avoid exhausting the browser's limited WebGL context pool.
|
// to avoid exhausting the browser's limited WebGL context pool.
|
||||||
|
|
||||||
@@ -120,6 +137,11 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
setIsAtBottom(buf.viewportY >= buf.baseY);
|
setIsAtBottom(buf.viewportY >= buf.baseY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track text selection to show copy hint in status bar
|
||||||
|
const selectionDisposable = term.onSelectionChange(() => {
|
||||||
|
setTerminalHasSelection(term.hasSelection());
|
||||||
|
});
|
||||||
|
|
||||||
// Handle image paste: intercept paste events with image data,
|
// Handle image paste: intercept paste events with image data,
|
||||||
// upload to the container, and inject the file path into terminal input.
|
// upload to the container, and inject the file path into terminal input.
|
||||||
const handlePaste = (e: ClipboardEvent) => {
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
@@ -222,6 +244,8 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
osc52Disposable.dispose();
|
osc52Disposable.dispose();
|
||||||
inputDisposable.dispose();
|
inputDisposable.dispose();
|
||||||
scrollDisposable.dispose();
|
scrollDisposable.dispose();
|
||||||
|
selectionDisposable.dispose();
|
||||||
|
setTerminalHasSelection(false);
|
||||||
containerRef.current?.removeEventListener("paste", handlePaste, { capture: true });
|
containerRef.current?.removeEventListener("paste", handlePaste, { capture: true });
|
||||||
outputPromise.then((fn) => fn?.());
|
outputPromise.then((fn) => fn?.());
|
||||||
exitPromise.then((fn) => fn?.());
|
exitPromise.then((fn) => fn?.());
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface Project {
|
|||||||
backend: Backend;
|
backend: Backend;
|
||||||
bedrock_config: BedrockConfig | null;
|
bedrock_config: BedrockConfig | null;
|
||||||
ollama_config: OllamaConfig | null;
|
ollama_config: OllamaConfig | null;
|
||||||
litellm_config: LiteLlmConfig | null;
|
openai_compatible_config: OpenAiCompatibleConfig | null;
|
||||||
allow_docker_access: boolean;
|
allow_docker_access: boolean;
|
||||||
mission_control_enabled: boolean;
|
mission_control_enabled: boolean;
|
||||||
ssh_key_path: string | null;
|
ssh_key_path: string | null;
|
||||||
@@ -45,7 +45,7 @@ export type ProjectStatus =
|
|||||||
| "stopping"
|
| "stopping"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
export type Backend = "anthropic" | "bedrock" | "ollama" | "lite_llm";
|
export type Backend = "anthropic" | "bedrock" | "ollama" | "open_ai_compatible";
|
||||||
|
|
||||||
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
|
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export interface OllamaConfig {
|
|||||||
model_id: string | null;
|
model_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LiteLlmConfig {
|
export interface OpenAiCompatibleConfig {
|
||||||
base_url: string;
|
base_url: string;
|
||||||
api_key: string | null;
|
api_key: string | null;
|
||||||
model_id: string | null;
|
model_id: string | null;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ interface AppState {
|
|||||||
removeMcpServerFromList: (id: string) => void;
|
removeMcpServerFromList: (id: string) => void;
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
|
terminalHasSelection: boolean;
|
||||||
|
setTerminalHasSelection: (has: boolean) => void;
|
||||||
sidebarView: "projects" | "mcp" | "settings";
|
sidebarView: "projects" | "mcp" | "settings";
|
||||||
setSidebarView: (view: "projects" | "mcp" | "settings") => void;
|
setSidebarView: (view: "projects" | "mcp" | "settings") => void;
|
||||||
dockerAvailable: boolean | null;
|
dockerAvailable: boolean | null;
|
||||||
@@ -100,6 +102,8 @@ export const useAppState = create<AppState>((set) => ({
|
|||||||
})),
|
})),
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
|
terminalHasSelection: false,
|
||||||
|
setTerminalHasSelection: (has) => set({ terminalHasSelection: has }),
|
||||||
sidebarView: "projects",
|
sidebarView: "projects",
|
||||||
setSidebarView: (view) => set({ sidebarView: view }),
|
setSidebarView: (view) => set({ sidebarView: view }),
|
||||||
dockerAvailable: null,
|
dockerAvailable: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user