UX: collapsible sidebar, settings accordion, global backend defaults, tab rename #4

Merged
jknapp merged 5 commits from feature/ux-improvements into main 2026-05-24 16:39:34 +00:00
8 changed files with 258 additions and 26 deletions
Showing only changes of commit 5b1c801cf1 - Show all commits

View File

@@ -193,16 +193,20 @@ pub async fn start_project_container(
if project.backend == Backend::Ollama { if project.backend == Backend::Ollama {
let ollama = project.ollama_config.as_ref() let ollama = project.ollama_config.as_ref()
.ok_or_else(|| "Ollama backend selected but no Ollama configuration found.".to_string())?; .ok_or_else(|| "Ollama backend selected but no Ollama configuration found.".to_string())?;
if ollama.base_url.is_empty() { if ollama.base_url.is_empty()
return Err("Ollama base URL is required.".to_string()); && settings.global_ollama.base_url.as_deref().map(str::trim).unwrap_or("").is_empty()
{
return Err("Ollama base URL is required. Set it per-project or in global Ollama settings.".to_string());
} }
} }
if project.backend == Backend::OpenAiCompatible { if project.backend == Backend::OpenAiCompatible {
let oai_config = project.openai_compatible_config.as_ref() let oai_config = project.openai_compatible_config.as_ref()
.ok_or_else(|| "OpenAI Compatible backend selected but no configuration found.".to_string())?; .ok_or_else(|| "OpenAI Compatible backend selected but no configuration found.".to_string())?;
if oai_config.base_url.is_empty() { if oai_config.base_url.is_empty()
return Err("OpenAI Compatible base URL is required.".to_string()); && settings.global_openai_compatible.base_url.as_deref().map(str::trim).unwrap_or("").is_empty()
{
return Err("OpenAI Compatible base URL is required. Set it per-project or in global settings.".to_string());
} }
} }
@@ -334,6 +338,9 @@ pub async fn start_project_container(
let needs_recreate = docker::container_needs_recreation( let needs_recreate = docker::container_needs_recreation(
&existing_id, &existing_id,
&project, &project,
&settings.global_aws,
&settings.global_ollama,
&settings.global_openai_compatible,
settings.global_claude_instructions.as_deref(), settings.global_claude_instructions.as_deref(),
&settings.global_custom_env_vars, &settings.global_custom_env_vars,
settings.timezone.as_deref(), settings.timezone.as_deref(),
@@ -369,6 +376,8 @@ pub async fn start_project_container(
&create_image, &create_image,
aws_config_path.as_deref(), aws_config_path.as_deref(),
&settings.global_aws, &settings.global_aws,
&settings.global_ollama,
&settings.global_openai_compatible,
settings.global_claude_instructions.as_deref(), settings.global_claude_instructions.as_deref(),
&settings.global_custom_env_vars, &settings.global_custom_env_vars,
settings.timezone.as_deref(), settings.timezone.as_deref(),
@@ -406,6 +415,8 @@ pub async fn start_project_container(
&create_image, &create_image,
aws_config_path.as_deref(), aws_config_path.as_deref(),
&settings.global_aws, &settings.global_aws,
&settings.global_ollama,
&settings.global_openai_compatible,
settings.global_claude_instructions.as_deref(), settings.global_claude_instructions.as_deref(),
&settings.global_custom_env_vars, &settings.global_custom_env_vars,
settings.timezone.as_deref(), settings.timezone.as_deref(),

View File

@@ -8,7 +8,7 @@ use std::collections::HashMap;
use sha2::{Sha256, Digest}; use sha2::{Sha256, Digest};
use super::client::get_docker; use super::client::get_docker;
use crate::models::{Backend, BedrockAuthMethod, ClaudeCodeSettings, ContainerInfo, EnvVar, GlobalAwsSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath}; use crate::models::{Backend, BedrockAuthMethod, ClaudeCodeSettings, ContainerInfo, EnvVar, GlobalAwsSettings, GlobalOllamaSettings, GlobalOpenAiCompatibleSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath};
const SCHEDULER_INSTRUCTIONS: &str = r#"## Scheduled Tasks const SCHEDULER_INSTRUCTIONS: &str = r#"## Scheduled Tasks
@@ -256,9 +256,25 @@ fn sha256_hex(input: &str) -> String {
format!("{:x}", hasher.finalize()) format!("{:x}", hasher.finalize())
} }
/// Resolve a per-project string value with a global fallback. Returns `None`
/// when both are blank, otherwise the per-project value if set, else the global.
fn resolve_with_global<'a>(per_project: Option<&'a str>, global: Option<&'a str>) -> Option<&'a str> {
let project_val = per_project.map(str::trim).filter(|s| !s.is_empty());
if project_val.is_some() {
return project_val;
}
global.map(str::trim).filter(|s| !s.is_empty())
}
/// Compute a fingerprint for the Bedrock configuration so we can detect changes. /// Compute a fingerprint for the Bedrock configuration so we can detect changes.
fn compute_bedrock_fingerprint(project: &Project) -> String { /// Includes the resolved model_id (per-project blank → global default) so that
/// changing the global default forces a container recreation.
fn compute_bedrock_fingerprint(project: &Project, global_aws: &GlobalAwsSettings) -> String {
if let Some(ref bedrock) = project.bedrock_config { if let Some(ref bedrock) = project.bedrock_config {
let effective_model = resolve_with_global(
bedrock.model_id.as_deref(),
global_aws.default_model_id.as_deref(),
).unwrap_or("").to_string();
let parts = vec![ let parts = vec![
format!("{:?}", bedrock.auth_method), format!("{:?}", bedrock.auth_method),
bedrock.aws_region.clone(), bedrock.aws_region.clone(),
@@ -267,7 +283,7 @@ fn compute_bedrock_fingerprint(project: &Project) -> String {
bedrock.aws_session_token.as_deref().unwrap_or("").to_string(), bedrock.aws_session_token.as_deref().unwrap_or("").to_string(),
bedrock.aws_profile.as_deref().unwrap_or("").to_string(), bedrock.aws_profile.as_deref().unwrap_or("").to_string(),
bedrock.aws_bearer_token.as_deref().unwrap_or("").to_string(), bedrock.aws_bearer_token.as_deref().unwrap_or("").to_string(),
bedrock.model_id.as_deref().unwrap_or("").to_string(), effective_model,
format!("{}", bedrock.disable_prompt_caching), format!("{}", bedrock.disable_prompt_caching),
bedrock.service_tier.as_deref().unwrap_or("").to_string(), bedrock.service_tier.as_deref().unwrap_or("").to_string(),
]; ];
@@ -278,12 +294,18 @@ fn compute_bedrock_fingerprint(project: &Project) -> String {
} }
/// Compute a fingerprint for the Ollama configuration so we can detect changes. /// Compute a fingerprint for the Ollama configuration so we can detect changes.
fn compute_ollama_fingerprint(project: &Project) -> String { /// Includes the resolved base_url and model_id (per-project blank → global default).
fn compute_ollama_fingerprint(project: &Project, global_ollama: &GlobalOllamaSettings) -> String {
if let Some(ref ollama) = project.ollama_config { if let Some(ref ollama) = project.ollama_config {
let parts = vec![ let effective_url = resolve_with_global(
ollama.base_url.clone(), Some(&ollama.base_url),
ollama.model_id.as_deref().unwrap_or("").to_string(), global_ollama.base_url.as_deref(),
]; ).unwrap_or("").to_string();
let effective_model = resolve_with_global(
ollama.model_id.as_deref(),
global_ollama.default_model_id.as_deref(),
).unwrap_or("").to_string();
let parts = vec![effective_url, effective_model];
sha256_hex(&parts.join("|")) sha256_hex(&parts.join("|"))
} else { } else {
String::new() String::new()
@@ -291,12 +313,24 @@ fn compute_ollama_fingerprint(project: &Project) -> String {
} }
/// Compute a fingerprint for the OpenAI Compatible configuration so we can detect changes. /// Compute a fingerprint for the OpenAI Compatible configuration so we can detect changes.
fn compute_openai_compatible_fingerprint(project: &Project) -> String { /// Includes the resolved base_url and model_id (per-project blank → global default).
fn compute_openai_compatible_fingerprint(
project: &Project,
global_openai_compatible: &GlobalOpenAiCompatibleSettings,
) -> String {
if let Some(ref config) = project.openai_compatible_config { if let Some(ref config) = project.openai_compatible_config {
let effective_url = resolve_with_global(
Some(&config.base_url),
global_openai_compatible.base_url.as_deref(),
).unwrap_or("").to_string();
let effective_model = resolve_with_global(
config.model_id.as_deref(),
global_openai_compatible.default_model_id.as_deref(),
).unwrap_or("").to_string();
let parts = vec![ let parts = vec![
config.base_url.clone(), effective_url,
config.api_key.as_deref().unwrap_or("").to_string(), config.api_key.as_deref().unwrap_or("").to_string(),
config.model_id.as_deref().unwrap_or("").to_string(), effective_model,
]; ];
sha256_hex(&parts.join("|")) sha256_hex(&parts.join("|"))
} else { } else {
@@ -552,6 +586,8 @@ pub async fn create_container(
image_name: &str, image_name: &str,
aws_config_path: Option<&str>, aws_config_path: Option<&str>,
global_aws: &GlobalAwsSettings, global_aws: &GlobalAwsSettings,
global_ollama: &GlobalOllamaSettings,
global_openai_compatible: &GlobalOpenAiCompatibleSettings,
global_claude_instructions: Option<&str>, global_claude_instructions: Option<&str>,
global_custom_env_vars: &[EnvVar], global_custom_env_vars: &[EnvVar],
timezone: Option<&str>, timezone: Option<&str>,
@@ -659,7 +695,10 @@ pub async fn create_container(
} }
} }
if let Some(ref model) = bedrock.model_id { if let Some(model) = resolve_with_global(
bedrock.model_id.as_deref(),
global_aws.default_model_id.as_deref(),
) {
env_vars.push(format!("ANTHROPIC_MODEL={}", model)); env_vars.push(format!("ANTHROPIC_MODEL={}", model));
} }
@@ -679,9 +718,17 @@ pub async fn create_container(
// Ollama configuration // Ollama configuration
if project.backend == Backend::Ollama { if project.backend == Backend::Ollama {
if let Some(ref ollama) = project.ollama_config { if let Some(ref ollama) = project.ollama_config {
env_vars.push(format!("ANTHROPIC_BASE_URL={}", ollama.base_url)); if let Some(url) = resolve_with_global(
Some(&ollama.base_url),
global_ollama.base_url.as_deref(),
) {
env_vars.push(format!("ANTHROPIC_BASE_URL={}", url));
}
env_vars.push("ANTHROPIC_AUTH_TOKEN=ollama".to_string()); env_vars.push("ANTHROPIC_AUTH_TOKEN=ollama".to_string());
if let Some(ref model) = ollama.model_id { if let Some(model) = resolve_with_global(
ollama.model_id.as_deref(),
global_ollama.default_model_id.as_deref(),
) {
env_vars.push(format!("ANTHROPIC_MODEL={}", model)); env_vars.push(format!("ANTHROPIC_MODEL={}", model));
} }
} }
@@ -690,11 +737,19 @@ pub async fn create_container(
// OpenAI Compatible configuration // OpenAI Compatible configuration
if project.backend == Backend::OpenAiCompatible { if project.backend == Backend::OpenAiCompatible {
if let Some(ref config) = project.openai_compatible_config { if let Some(ref config) = project.openai_compatible_config {
env_vars.push(format!("ANTHROPIC_BASE_URL={}", config.base_url)); if let Some(url) = resolve_with_global(
Some(&config.base_url),
global_openai_compatible.base_url.as_deref(),
) {
env_vars.push(format!("ANTHROPIC_BASE_URL={}", url));
}
if let Some(ref key) = config.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) = config.model_id { if let Some(model) = resolve_with_global(
config.model_id.as_deref(),
global_openai_compatible.default_model_id.as_deref(),
) {
env_vars.push(format!("ANTHROPIC_MODEL={}", model)); env_vars.push(format!("ANTHROPIC_MODEL={}", model));
} }
} }
@@ -904,9 +959,9 @@ pub async fn create_container(
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.backend".to_string(), format!("{:?}", project.backend)); labels.insert("triple-c.backend".to_string(), format!("{:?}", project.backend));
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, global_aws));
labels.insert("triple-c.ollama-fingerprint".to_string(), compute_ollama_fingerprint(project)); labels.insert("triple-c.ollama-fingerprint".to_string(), compute_ollama_fingerprint(project, global_ollama));
labels.insert("triple-c.openai-compatible-fingerprint".to_string(), compute_openai_compatible_fingerprint(project)); labels.insert("triple-c.openai-compatible-fingerprint".to_string(), compute_openai_compatible_fingerprint(project, global_openai_compatible));
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());
@@ -1083,6 +1138,9 @@ pub async fn remove_project_volumes(project: &Project) -> Result<(), String> {
pub async fn container_needs_recreation( pub async fn container_needs_recreation(
container_id: &str, container_id: &str,
project: &Project, project: &Project,
global_aws: &GlobalAwsSettings,
global_ollama: &GlobalOllamaSettings,
global_openai_compatible: &GlobalOpenAiCompatibleSettings,
global_claude_instructions: Option<&str>, global_claude_instructions: Option<&str>,
global_custom_env_vars: &[EnvVar], global_custom_env_vars: &[EnvVar],
timezone: Option<&str>, timezone: Option<&str>,
@@ -1154,7 +1212,7 @@ pub async fn container_needs_recreation(
} }
// ── Bedrock config fingerprint ─────────────────────────────────────── // ── Bedrock config fingerprint ───────────────────────────────────────
let expected_bedrock_fp = compute_bedrock_fingerprint(project); let expected_bedrock_fp = compute_bedrock_fingerprint(project, global_aws);
let container_bedrock_fp = get_label("triple-c.bedrock-fingerprint").unwrap_or_default(); let container_bedrock_fp = get_label("triple-c.bedrock-fingerprint").unwrap_or_default();
if container_bedrock_fp != expected_bedrock_fp { if container_bedrock_fp != expected_bedrock_fp {
log::info!("Bedrock config mismatch"); log::info!("Bedrock config mismatch");
@@ -1162,7 +1220,7 @@ pub async fn container_needs_recreation(
} }
// ── Ollama config fingerprint ──────────────────────────────────────── // ── Ollama config fingerprint ────────────────────────────────────────
let expected_ollama_fp = compute_ollama_fingerprint(project); let expected_ollama_fp = compute_ollama_fingerprint(project, global_ollama);
let container_ollama_fp = get_label("triple-c.ollama-fingerprint").unwrap_or_default(); let container_ollama_fp = get_label("triple-c.ollama-fingerprint").unwrap_or_default();
if container_ollama_fp != expected_ollama_fp { if container_ollama_fp != expected_ollama_fp {
log::info!("Ollama config mismatch"); log::info!("Ollama config mismatch");
@@ -1170,7 +1228,7 @@ pub async fn container_needs_recreation(
} }
// ── OpenAI Compatible config fingerprint ──────────────────────────── // ── OpenAI Compatible config fingerprint ────────────────────────────
let expected_oai_fp = compute_openai_compatible_fingerprint(project); let expected_oai_fp = compute_openai_compatible_fingerprint(project, global_openai_compatible);
let container_oai_fp = get_label("triple-c.openai-compatible-fingerprint").unwrap_or_default(); let container_oai_fp = get_label("triple-c.openai-compatible-fingerprint").unwrap_or_default();
if container_oai_fp != expected_oai_fp { if container_oai_fp != expected_oai_fp {
log::info!("OpenAI Compatible config mismatch"); log::info!("OpenAI Compatible config mismatch");

View File

@@ -32,6 +32,8 @@ pub struct GlobalAwsSettings {
pub aws_profile: Option<String>, pub aws_profile: Option<String>,
#[serde(default)] #[serde(default)]
pub aws_region: Option<String>, pub aws_region: Option<String>,
#[serde(default)]
pub default_model_id: Option<String>,
} }
impl Default for GlobalAwsSettings { impl Default for GlobalAwsSettings {
@@ -40,10 +42,27 @@ impl Default for GlobalAwsSettings {
aws_config_path: None, aws_config_path: None,
aws_profile: None, aws_profile: None,
aws_region: None, aws_region: None,
default_model_id: None,
} }
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GlobalOllamaSettings {
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub default_model_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GlobalOpenAiCompatibleSettings {
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub default_model_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppSettings { pub struct AppSettings {
#[serde(default)] #[serde(default)]
@@ -60,6 +79,10 @@ pub struct AppSettings {
pub custom_image_name: Option<String>, pub custom_image_name: Option<String>,
#[serde(default)] #[serde(default)]
pub global_aws: GlobalAwsSettings, pub global_aws: GlobalAwsSettings,
#[serde(default)]
pub global_ollama: GlobalOllamaSettings,
#[serde(default)]
pub global_openai_compatible: GlobalOpenAiCompatibleSettings,
#[serde(default = "default_global_instructions")] #[serde(default = "default_global_instructions")]
pub global_claude_instructions: Option<String>, pub global_claude_instructions: Option<String>,
#[serde(default)] #[serde(default)]
@@ -156,6 +179,8 @@ impl Default for AppSettings {
image_source: ImageSource::default(), image_source: ImageSource::default(),
custom_image_name: None, custom_image_name: None,
global_aws: GlobalAwsSettings::default(), global_aws: GlobalAwsSettings::default(),
global_ollama: GlobalOllamaSettings::default(),
global_openai_compatible: GlobalOpenAiCompatibleSettings::default(),
global_claude_instructions: default_global_instructions(), global_claude_instructions: default_global_instructions(),
global_custom_env_vars: Vec::new(), global_custom_env_vars: Vec::new(),
auto_check_updates: true, auto_check_updates: true,

View File

@@ -12,6 +12,7 @@ export default function AwsSettings() {
aws_config_path: null, aws_config_path: null,
aws_profile: null, aws_profile: null,
aws_region: null, aws_region: null,
default_model_id: null,
}; };
// Load profiles when component mounts or aws_config_path changes // Load profiles when component mounts or aws_config_path changes
@@ -105,6 +106,18 @@ export default function AwsSettings() {
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]" className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
/> />
</div> </div>
{/* Default Model ID */}
<div>
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Model ID<Tooltip text="Default Bedrock model ID. Used when a Bedrock project doesn't set its own Model ID." /></span>
<input
type="text"
value={globalAws.default_model_id ?? ""}
onChange={(e) => handleChange("default_model_id", e.target.value)}
placeholder="anthropic.claude-sonnet-4-20250514-v1:0"
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
/>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,53 @@
import { useSettings } from "../../hooks/useSettings";
import Tooltip from "../ui/Tooltip";
export default function OllamaSettings() {
const { appSettings, saveSettings } = useSettings();
const globalOllama = appSettings?.global_ollama ?? {
base_url: null,
default_model_id: null,
};
const handleChange = async (field: "base_url" | "default_model_id", value: string) => {
if (!appSettings) return;
await saveSettings({
...appSettings,
global_ollama: { ...globalOllama, [field]: value || null },
});
};
return (
<div>
<label className="block text-sm font-medium mb-2">Ollama Configuration</label>
<div className="space-y-3 text-sm">
<p className="text-xs text-[var(--text-secondary)]">
Global Ollama defaults. Used when a per-project field is blank.
Changes here require a container rebuild to take effect.
</p>
<div>
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Base URL<Tooltip text="URL of your Ollama server. Used when a per-project Ollama base URL is blank." /></span>
<input
type="text"
value={globalOllama.base_url ?? ""}
onChange={(e) => handleChange("base_url", e.target.value)}
placeholder="http://host.docker.internal:11434"
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
/>
</div>
<div>
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Model<Tooltip text="Default Ollama model name. Used when a per-project Ollama model is blank." /></span>
<input
type="text"
value={globalOllama.default_model_id ?? ""}
onChange={(e) => handleChange("default_model_id", e.target.value)}
placeholder="qwen3.5:27b"
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { useSettings } from "../../hooks/useSettings";
import Tooltip from "../ui/Tooltip";
export default function OpenAiCompatibleSettings() {
const { appSettings, saveSettings } = useSettings();
const globalOai = appSettings?.global_openai_compatible ?? {
base_url: null,
default_model_id: null,
};
const handleChange = async (field: "base_url" | "default_model_id", value: string) => {
if (!appSettings) return;
await saveSettings({
...appSettings,
global_openai_compatible: { ...globalOai, [field]: value || null },
});
};
return (
<div>
<label className="block text-sm font-medium mb-2">OpenAI Compatible Configuration</label>
<div className="space-y-3 text-sm">
<p className="text-xs text-[var(--text-secondary)]">
Global defaults for any OpenAI-compatible endpoint (LiteLLM, OpenRouter, vLLM, etc.).
Used when a per-project field is blank. Changes require a container rebuild.
</p>
<div>
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Base URL<Tooltip text="Default OpenAI-compatible endpoint URL. Used when a per-project base URL is blank." /></span>
<input
type="text"
value={globalOai.base_url ?? ""}
onChange={(e) => handleChange("base_url", e.target.value)}
placeholder="http://host.docker.internal:4000"
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
/>
</div>
<div>
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Model<Tooltip text="Default model identifier. Used when a per-project model is blank." /></span>
<input
type="text"
value={globalOai.default_model_id ?? ""}
onChange={(e) => handleChange("default_model_id", e.target.value)}
placeholder="gpt-4o / gemini-pro / etc."
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
/>
</div>
</div>
</div>
);
}

View File

@@ -1,6 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import DockerSettings from "./DockerSettings"; import DockerSettings from "./DockerSettings";
import AwsSettings from "./AwsSettings"; import AwsSettings from "./AwsSettings";
import OllamaSettings from "./OllamaSettings";
import OpenAiCompatibleSettings from "./OpenAiCompatibleSettings";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import { useUpdates } from "../../hooks/useUpdates"; import { useUpdates } from "../../hooks/useUpdates";
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal"; import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
@@ -148,6 +150,10 @@ export default function SettingsPanel() {
<AccordionSection id="backends" title="Backends" defaultOpen={false}> <AccordionSection id="backends" title="Backends" defaultOpen={false}>
<AwsSettings /> <AwsSettings />
<div className="pt-3 border-t border-[var(--border-color)]" />
<OllamaSettings />
<div className="pt-3 border-t border-[var(--border-color)]" />
<OpenAiCompatibleSettings />
</AccordionSection> </AccordionSection>
<AccordionSection id="container" title="Container" defaultOpen={false}> <AccordionSection id="container" title="Container" defaultOpen={false}>

View File

@@ -116,6 +116,17 @@ export interface GlobalAwsSettings {
aws_config_path: string | null; aws_config_path: string | null;
aws_profile: string | null; aws_profile: string | null;
aws_region: string | null; aws_region: string | null;
default_model_id: string | null;
}
export interface GlobalOllamaSettings {
base_url: string | null;
default_model_id: string | null;
}
export interface GlobalOpenAiCompatibleSettings {
base_url: string | null;
default_model_id: string | null;
} }
export interface AppSettings { export interface AppSettings {
@@ -126,6 +137,8 @@ export interface AppSettings {
image_source: ImageSource; image_source: ImageSource;
custom_image_name: string | null; custom_image_name: string | null;
global_aws: GlobalAwsSettings; global_aws: GlobalAwsSettings;
global_ollama: GlobalOllamaSettings;
global_openai_compatible: GlobalOpenAiCompatibleSettings;
global_claude_instructions: string | null; global_claude_instructions: string | null;
global_custom_env_vars: EnvVar[]; global_custom_env_vars: EnvVar[];
auto_check_updates: boolean; auto_check_updates: boolean;