Compare commits
3 Commits
v0.1.45-wi
...
v0.1.48-wi
| Author | SHA1 | Date | |
|---|---|---|---|
| da078af73f | |||
| 01ea581f8a | |||
| 552aaebf16 |
@@ -47,8 +47,7 @@ Triple-C is a cross-platform desktop application that sandboxes Claude Code insi
|
|||||||
|
|
||||||
Each project can independently use one of:
|
Each project can independently use one of:
|
||||||
|
|
||||||
- **`/login`** (OAuth): User runs `claude login` inside the terminal. Token persisted in the config volume.
|
- **Anthropic** (OAuth): User runs `claude login` inside the terminal on first use. Token persisted in the config volume across restarts and resets.
|
||||||
- **API Key**: Stored in the OS keychain, injected as `ANTHROPIC_API_KEY` env var.
|
|
||||||
- **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token).
|
- **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token).
|
||||||
|
|
||||||
### Container Spawning (Sibling Containers)
|
### Container Spawning (Sibling Containers)
|
||||||
|
|||||||
@@ -124,26 +124,15 @@ pub async fn start_project_container(
|
|||||||
let settings = state.settings_store.get();
|
let settings = state.settings_store.get();
|
||||||
let image_name = container_config::resolve_image_name(&settings.image_source, &settings.custom_image_name);
|
let image_name = container_config::resolve_image_name(&settings.image_source, &settings.custom_image_name);
|
||||||
|
|
||||||
// Get API key only if auth mode requires it
|
// Validate auth mode requirements
|
||||||
let api_key = match project.auth_mode {
|
if project.auth_mode == AuthMode::Bedrock {
|
||||||
AuthMode::ApiKey => {
|
|
||||||
let key = secure::get_api_key()?
|
|
||||||
.ok_or_else(|| "No API key configured. Please set your Anthropic API key in Settings.".to_string())?;
|
|
||||||
Some(key)
|
|
||||||
}
|
|
||||||
AuthMode::Login => {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
AuthMode::Bedrock => {
|
|
||||||
let bedrock = project.bedrock_config.as_ref()
|
let bedrock = project.bedrock_config.as_ref()
|
||||||
.ok_or_else(|| "Bedrock auth mode selected but no Bedrock configuration found.".to_string())?;
|
.ok_or_else(|| "Bedrock auth mode selected but no Bedrock configuration found.".to_string())?;
|
||||||
// Region can come from per-project or global
|
// Region can come from per-project or global
|
||||||
if bedrock.aws_region.is_empty() && settings.global_aws.aws_region.is_none() {
|
if bedrock.aws_region.is_empty() && settings.global_aws.aws_region.is_none() {
|
||||||
return Err("AWS region is required for Bedrock auth mode. Set it per-project or in global AWS settings.".to_string());
|
return Err("AWS region is required for Bedrock auth mode. Set it per-project or in global AWS settings.".to_string());
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Update status to starting
|
// Update status to starting
|
||||||
state.projects_store.update_status(&project_id, ProjectStatus::Starting)?;
|
state.projects_store.update_status(&project_id, ProjectStatus::Starting)?;
|
||||||
@@ -169,7 +158,6 @@ pub async fn start_project_container(
|
|||||||
let needs_recreation = docker::container_needs_recreation(
|
let needs_recreation = docker::container_needs_recreation(
|
||||||
&existing_id,
|
&existing_id,
|
||||||
&project,
|
&project,
|
||||||
api_key.as_deref(),
|
|
||||||
settings.global_claude_instructions.as_deref(),
|
settings.global_claude_instructions.as_deref(),
|
||||||
&settings.global_custom_env_vars,
|
&settings.global_custom_env_vars,
|
||||||
)
|
)
|
||||||
@@ -181,7 +169,6 @@ pub async fn start_project_container(
|
|||||||
docker::remove_container(&existing_id).await?;
|
docker::remove_container(&existing_id).await?;
|
||||||
let new_id = docker::create_container(
|
let new_id = docker::create_container(
|
||||||
&project,
|
&project,
|
||||||
api_key.as_deref(),
|
|
||||||
&docker_socket,
|
&docker_socket,
|
||||||
&image_name,
|
&image_name,
|
||||||
aws_config_path.as_deref(),
|
aws_config_path.as_deref(),
|
||||||
@@ -198,7 +185,6 @@ pub async fn start_project_container(
|
|||||||
} else {
|
} else {
|
||||||
let new_id = docker::create_container(
|
let new_id = docker::create_container(
|
||||||
&project,
|
&project,
|
||||||
api_key.as_deref(),
|
|
||||||
&docker_socket,
|
&docker_socket,
|
||||||
&image_name,
|
&image_name,
|
||||||
aws_config_path.as_deref(),
|
aws_config_path.as_deref(),
|
||||||
|
|||||||
@@ -2,24 +2,8 @@ use tauri::State;
|
|||||||
|
|
||||||
use crate::docker;
|
use crate::docker;
|
||||||
use crate::models::AppSettings;
|
use crate::models::AppSettings;
|
||||||
use crate::storage::secure;
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn set_api_key(key: String) -> Result<(), String> {
|
|
||||||
secure::store_api_key(&key)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn has_api_key() -> Result<bool, String> {
|
|
||||||
secure::has_api_key()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn delete_api_key() -> Result<(), String> {
|
|
||||||
secure::delete_api_key()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_settings(state: State<'_, AppState>) -> Result<AppSettings, String> {
|
pub async fn get_settings(state: State<'_, AppState>) -> Result<AppSettings, String> {
|
||||||
Ok(state.settings_store.get())
|
Ok(state.settings_store.get())
|
||||||
|
|||||||
@@ -10,19 +10,6 @@ use std::hash::{Hash, Hasher};
|
|||||||
use super::client::get_docker;
|
use super::client::get_docker;
|
||||||
use crate::models::{AuthMode, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, Project, ProjectPath};
|
use crate::models::{AuthMode, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, Project, ProjectPath};
|
||||||
|
|
||||||
/// Compute a fingerprint for the API key so we can detect when it changes
|
|
||||||
/// without storing the actual key in Docker labels.
|
|
||||||
fn compute_api_key_fingerprint(api_key: Option<&str>) -> String {
|
|
||||||
match api_key {
|
|
||||||
Some(key) => {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
key.hash(&mut hasher);
|
|
||||||
format!("{:x}", hasher.finish())
|
|
||||||
}
|
|
||||||
None => String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute a fingerprint string for the custom environment variables.
|
/// Compute a fingerprint string for the custom environment variables.
|
||||||
/// Sorted alphabetically so order changes do not cause spurious recreation.
|
/// Sorted alphabetically so order changes do not cause spurious recreation.
|
||||||
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
|
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
|
||||||
@@ -140,7 +127,6 @@ pub async fn find_existing_container(project: &Project) -> Result<Option<String>
|
|||||||
|
|
||||||
pub async fn create_container(
|
pub async fn create_container(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
api_key: Option<&str>,
|
|
||||||
docker_socket_path: &str,
|
docker_socket_path: &str,
|
||||||
image_name: &str,
|
image_name: &str,
|
||||||
aws_config_path: Option<&str>,
|
aws_config_path: Option<&str>,
|
||||||
@@ -189,10 +175,6 @@ pub async fn create_container(
|
|||||||
log::debug!("Skipping HOST_UID/HOST_GID on Windows — Docker Desktop's Linux VM handles user mapping");
|
log::debug!("Skipping HOST_UID/HOST_GID on Windows — Docker Desktop's Linux VM handles user mapping");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(key) = api_key {
|
|
||||||
env_vars.push(format!("ANTHROPIC_API_KEY={}", key));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref token) = project.git_token {
|
if let Some(ref token) = project.git_token {
|
||||||
env_vars.push(format!("GIT_TOKEN={}", token));
|
env_vars.push(format!("GIT_TOKEN={}", token));
|
||||||
}
|
}
|
||||||
@@ -369,7 +351,6 @@ pub async fn create_container(
|
|||||||
labels.insert("triple-c.project-id".to_string(), project.id.clone());
|
labels.insert("triple-c.project-id".to_string(), project.id.clone());
|
||||||
labels.insert("triple-c.project-name".to_string(), project.name.clone());
|
labels.insert("triple-c.project-name".to_string(), project.name.clone());
|
||||||
labels.insert("triple-c.auth-mode".to_string(), format!("{:?}", project.auth_mode));
|
labels.insert("triple-c.auth-mode".to_string(), format!("{:?}", project.auth_mode));
|
||||||
labels.insert("triple-c.api-key-fingerprint".to_string(), compute_api_key_fingerprint(api_key));
|
|
||||||
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.image".to_string(), image_name.to_string());
|
labels.insert("triple-c.image".to_string(), image_name.to_string());
|
||||||
@@ -453,7 +434,6 @@ pub async fn remove_container(container_id: &str) -> Result<(), String> {
|
|||||||
pub async fn container_needs_recreation(
|
pub async fn container_needs_recreation(
|
||||||
container_id: &str,
|
container_id: &str,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
api_key: Option<&str>,
|
|
||||||
global_claude_instructions: Option<&str>,
|
global_claude_instructions: Option<&str>,
|
||||||
global_custom_env_vars: &[EnvVar],
|
global_custom_env_vars: &[EnvVar],
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
@@ -492,14 +472,6 @@ pub async fn container_needs_recreation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API key fingerprint ─────────────────────────────────────────────
|
|
||||||
let expected_api_key_fp = compute_api_key_fingerprint(api_key);
|
|
||||||
let container_api_key_fp = get_label("triple-c.api-key-fingerprint").unwrap_or_default();
|
|
||||||
if container_api_key_fp != expected_api_key_fp {
|
|
||||||
log::info!("API key fingerprint mismatch, triggering recreation");
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Project paths fingerprint ──────────────────────────────────────────
|
// ── Project paths fingerprint ──────────────────────────────────────────
|
||||||
let expected_paths_fp = compute_paths_fingerprint(&project.paths);
|
let expected_paths_fp = compute_paths_fingerprint(&project.paths);
|
||||||
match get_label("triple-c.paths-fingerprint") {
|
match get_label("triple-c.paths-fingerprint") {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub fn run() {
|
|||||||
exec_manager: ExecSessionManager::new(),
|
exec_manager: ExecSessionManager::new(),
|
||||||
})
|
})
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
match tauri::image::Image::from_bytes(include_bytes!("../icons/icon.ico")) {
|
match tauri::image::Image::from_bytes(include_bytes!("../icons/icon.png")) {
|
||||||
Ok(icon) => {
|
Ok(icon) => {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.set_icon(icon);
|
let _ = window.set_icon(icon);
|
||||||
@@ -79,9 +79,6 @@ pub fn run() {
|
|||||||
commands::project_commands::stop_project_container,
|
commands::project_commands::stop_project_container,
|
||||||
commands::project_commands::rebuild_project_container,
|
commands::project_commands::rebuild_project_container,
|
||||||
// Settings
|
// Settings
|
||||||
commands::settings_commands::set_api_key,
|
|
||||||
commands::settings_commands::has_api_key,
|
|
||||||
commands::settings_commands::delete_api_key,
|
|
||||||
commands::settings_commands::get_settings,
|
commands::settings_commands::get_settings,
|
||||||
commands::settings_commands::update_settings,
|
commands::settings_commands::update_settings,
|
||||||
commands::settings_commands::pull_image,
|
commands::settings_commands::pull_image,
|
||||||
|
|||||||
@@ -46,20 +46,21 @@ pub enum ProjectStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// How the project authenticates with Claude.
|
/// How the project authenticates with Claude.
|
||||||
/// - `Login`: User runs `claude login` inside the container (OAuth, persisted via config volume)
|
/// - `Anthropic`: User runs `claude login` inside the container (OAuth via Anthropic Console,
|
||||||
/// - `ApiKey`: Uses the API key stored in the OS keychain
|
/// persisted in the config volume)
|
||||||
/// - `Bedrock`: Uses AWS Bedrock with per-project AWS credentials
|
/// - `Bedrock`: Uses AWS Bedrock with per-project AWS credentials
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum AuthMode {
|
pub enum AuthMode {
|
||||||
Login,
|
/// Backward compat: old projects stored as "login" or "api_key" map to Anthropic.
|
||||||
ApiKey,
|
#[serde(alias = "login", alias = "api_key")]
|
||||||
|
Anthropic,
|
||||||
Bedrock,
|
Bedrock,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AuthMode {
|
impl Default for AuthMode {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Login
|
Self::Anthropic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,3 @@
|
|||||||
const SERVICE_NAME: &str = "triple-c";
|
|
||||||
const API_KEY_USER: &str = "anthropic-api-key";
|
|
||||||
|
|
||||||
pub fn store_api_key(key: &str) -> Result<(), String> {
|
|
||||||
let entry = keyring::Entry::new(SERVICE_NAME, API_KEY_USER)
|
|
||||||
.map_err(|e| format!("Keyring error: {}", e))?;
|
|
||||||
entry
|
|
||||||
.set_password(key)
|
|
||||||
.map_err(|e| format!("Failed to store API key: {}", e))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_api_key() -> Result<Option<String>, String> {
|
|
||||||
let entry = keyring::Entry::new(SERVICE_NAME, API_KEY_USER)
|
|
||||||
.map_err(|e| format!("Keyring error: {}", e))?;
|
|
||||||
match entry.get_password() {
|
|
||||||
Ok(key) => Ok(Some(key)),
|
|
||||||
Err(keyring::Error::NoEntry) => Ok(None),
|
|
||||||
Err(e) => Err(format!("Failed to retrieve API key: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_api_key() -> Result<(), String> {
|
|
||||||
let entry = keyring::Entry::new(SERVICE_NAME, API_KEY_USER)
|
|
||||||
.map_err(|e| format!("Keyring error: {}", e))?;
|
|
||||||
match entry.delete_credential() {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(keyring::Error::NoEntry) => Ok(()),
|
|
||||||
Err(e) => Err(format!("Failed to delete API key: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_api_key() -> Result<bool, String> {
|
|
||||||
match get_api_key() {
|
|
||||||
Ok(Some(_)) => Ok(true),
|
|
||||||
Ok(None) => Ok(false),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store a per-project secret in the OS keychain.
|
/// Store a per-project secret in the OS keychain.
|
||||||
pub fn store_project_secret(project_id: &str, key_name: &str, value: &str) -> Result<(), String> {
|
pub fn store_project_secret(project_id: &str, key_name: &str, value: &str) -> Result<(), String> {
|
||||||
let service = format!("triple-c-project-{}-{}", project_id, key_name);
|
let service = format!("triple-c-project-{}-{}", project_id, key_name);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useAppState } from "./store/appState";
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { checkDocker, checkImage } = useDocker();
|
const { checkDocker, checkImage } = useDocker();
|
||||||
const { checkApiKey, loadSettings } = useSettings();
|
const { loadSettings } = useSettings();
|
||||||
const { refresh } = useProjects();
|
const { refresh } = useProjects();
|
||||||
const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates();
|
const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates();
|
||||||
const { sessions, activeSessionId } = useAppState(
|
const { sessions, activeSessionId } = useAppState(
|
||||||
@@ -25,7 +25,6 @@ export default function App() {
|
|||||||
checkDocker().then((available) => {
|
checkDocker().then((available) => {
|
||||||
if (available) checkImage();
|
if (available) checkImage();
|
||||||
});
|
});
|
||||||
checkApiKey();
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
// Update detection
|
// Update detection
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const mockProject: Project = {
|
|||||||
paths: [{ host_path: "/home/user/project", mount_name: "project" }],
|
paths: [{ host_path: "/home/user/project", mount_name: "project" }],
|
||||||
container_id: null,
|
container_id: null,
|
||||||
status: "stopped",
|
status: "stopped",
|
||||||
auth_mode: "login",
|
auth_mode: "anthropic",
|
||||||
bedrock_config: null,
|
bedrock_config: null,
|
||||||
allow_docker_access: false,
|
allow_docker_access: false,
|
||||||
ssh_key_path: null,
|
ssh_key_path: null,
|
||||||
|
|||||||
@@ -267,26 +267,15 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<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">Auth:</span>
|
<span className="text-[var(--text-secondary)] mr-1">Auth:</span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("login"); }}
|
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("anthropic"); }}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={`px-2 py-0.5 rounded transition-colors ${
|
className={`px-2 py-0.5 rounded transition-colors ${
|
||||||
project.auth_mode === "login"
|
project.auth_mode === "anthropic"
|
||||||
? "bg-[var(--accent)] text-white"
|
? "bg-[var(--accent)] text-white"
|
||||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
|
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
|
||||||
} disabled:opacity-50`}
|
} disabled:opacity-50`}
|
||||||
>
|
>
|
||||||
/login
|
Anthropic
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("api_key"); }}
|
|
||||||
disabled={!isStopped}
|
|
||||||
className={`px-2 py-0.5 rounded transition-colors ${
|
|
||||||
project.auth_mode === "api_key"
|
|
||||||
? "bg-[var(--accent)] text-white"
|
|
||||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
|
|
||||||
} disabled:opacity-50`}
|
|
||||||
>
|
|
||||||
API key
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("bedrock"); }}
|
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("bedrock"); }}
|
||||||
|
|||||||
@@ -1,68 +1,10 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
|
||||||
|
|
||||||
export default function ApiKeyInput() {
|
export default function ApiKeyInput() {
|
||||||
const { hasKey, saveApiKey, removeApiKey } = useSettings();
|
|
||||||
const [key, setKey] = useState("");
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!key.trim()) return;
|
|
||||||
setSaving(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await saveApiKey(key.trim());
|
|
||||||
setKey("");
|
|
||||||
} catch (e) {
|
|
||||||
setError(String(e));
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Authentication</label>
|
<label className="block text-sm font-medium mb-1">Authentication</label>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-3">
|
<p className="text-xs text-[var(--text-secondary)] mb-3">
|
||||||
Each project can use <strong>claude login</strong> (OAuth, run inside the terminal), an <strong>API key</strong>, or <strong>AWS Bedrock</strong>. Set auth mode per-project.
|
Each project can use <strong>claude login</strong> (OAuth, run inside the terminal) or <strong>AWS Bedrock</strong>. Set auth mode per-project.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-1 mt-3">
|
|
||||||
API Key (for projects using API key mode)
|
|
||||||
</label>
|
|
||||||
{hasKey ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-[var(--success)]">Key configured</span>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
try { await removeApiKey(); } catch (e) { setError(String(e)); }
|
|
||||||
}}
|
|
||||||
className="text-xs text-[var(--error)] hover:underline"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={key}
|
|
||||||
onChange={(e) => setKey(e.target.value)}
|
|
||||||
placeholder="sk-ant-..."
|
|
||||||
className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleSave()}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving || !key.trim()}
|
|
||||||
className="px-3 py-1.5 text-xs bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
{saving ? "Saving..." : "Save Key"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{error && <div className="text-xs text-[var(--error)] mt-1">{error}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,39 +5,13 @@ import * as commands from "../lib/tauri-commands";
|
|||||||
import type { AppSettings } from "../lib/types";
|
import type { AppSettings } from "../lib/types";
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
const { hasKey, setHasKey, appSettings, setAppSettings } = useAppState(
|
const { appSettings, setAppSettings } = useAppState(
|
||||||
useShallow(s => ({
|
useShallow(s => ({
|
||||||
hasKey: s.hasKey,
|
|
||||||
setHasKey: s.setHasKey,
|
|
||||||
appSettings: s.appSettings,
|
appSettings: s.appSettings,
|
||||||
setAppSettings: s.setAppSettings,
|
setAppSettings: s.setAppSettings,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkApiKey = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const has = await commands.hasApiKey();
|
|
||||||
setHasKey(has);
|
|
||||||
return has;
|
|
||||||
} catch {
|
|
||||||
setHasKey(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [setHasKey]);
|
|
||||||
|
|
||||||
const saveApiKey = useCallback(
|
|
||||||
async (key: string) => {
|
|
||||||
await commands.setApiKey(key);
|
|
||||||
setHasKey(true);
|
|
||||||
},
|
|
||||||
[setHasKey],
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeApiKey = useCallback(async () => {
|
|
||||||
await commands.deleteApiKey();
|
|
||||||
setHasKey(false);
|
|
||||||
}, [setHasKey]);
|
|
||||||
|
|
||||||
const loadSettings = useCallback(async () => {
|
const loadSettings = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const settings = await commands.getSettings();
|
const settings = await commands.getSettings();
|
||||||
@@ -59,10 +33,6 @@ export function useSettings() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasKey,
|
|
||||||
checkApiKey,
|
|
||||||
saveApiKey,
|
|
||||||
removeApiKey,
|
|
||||||
appSettings,
|
appSettings,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ export const rebuildProjectContainer = (projectId: string) =>
|
|||||||
invoke<Project>("rebuild_project_container", { projectId });
|
invoke<Project>("rebuild_project_container", { projectId });
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
export const setApiKey = (key: string) =>
|
|
||||||
invoke<void>("set_api_key", { key });
|
|
||||||
export const hasApiKey = () => invoke<boolean>("has_api_key");
|
|
||||||
export const deleteApiKey = () => invoke<void>("delete_api_key");
|
|
||||||
export const getSettings = () => invoke<AppSettings>("get_settings");
|
export const getSettings = () => invoke<AppSettings>("get_settings");
|
||||||
export const updateSettings = (settings: AppSettings) =>
|
export const updateSettings = (settings: AppSettings) =>
|
||||||
invoke<AppSettings>("update_settings", { settings });
|
invoke<AppSettings>("update_settings", { settings });
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export type ProjectStatus =
|
|||||||
| "stopping"
|
| "stopping"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
export type AuthMode = "login" | "api_key" | "bedrock";
|
export type AuthMode = "anthropic" | "bedrock";
|
||||||
|
|
||||||
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
|
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ interface AppState {
|
|||||||
setDockerAvailable: (available: boolean | null) => void;
|
setDockerAvailable: (available: boolean | null) => void;
|
||||||
imageExists: boolean | null;
|
imageExists: boolean | null;
|
||||||
setImageExists: (exists: boolean | null) => void;
|
setImageExists: (exists: boolean | null) => void;
|
||||||
hasKey: boolean | null;
|
|
||||||
setHasKey: (has: boolean | null) => void;
|
|
||||||
|
|
||||||
// App settings
|
// App settings
|
||||||
appSettings: AppSettings | null;
|
appSettings: AppSettings | null;
|
||||||
setAppSettings: (settings: AppSettings) => void;
|
setAppSettings: (settings: AppSettings) => void;
|
||||||
@@ -85,9 +82,6 @@ export const useAppState = create<AppState>((set) => ({
|
|||||||
setDockerAvailable: (available) => set({ dockerAvailable: available }),
|
setDockerAvailable: (available) => set({ dockerAvailable: available }),
|
||||||
imageExists: null,
|
imageExists: null,
|
||||||
setImageExists: (exists) => set({ imageExists: exists }),
|
setImageExists: (exists) => set({ imageExists: exists }),
|
||||||
hasKey: null,
|
|
||||||
setHasKey: (has) => set({ hasKey: has }),
|
|
||||||
|
|
||||||
// App settings
|
// App settings
|
||||||
appSettings: null,
|
appSettings: null,
|
||||||
setAppSettings: (settings) => set({ appSettings: settings }),
|
setAppSettings: (settings) => set({ appSettings: settings }),
|
||||||
|
|||||||
Reference in New Issue
Block a user