Add UX enhancements: modals for env vars and instructions, global env vars, taskbar icon fix
All checks were successful
Build App / build-linux (push) Successful in 2m38s
Build App / build-windows (push) Successful in 5m5s

- Fix Windows taskbar icon by loading icon.ico instead of icon.png (ICO contains
  multiple sizes native to Windows taskbar/title bar/alt-tab)
- Add "Container must be stopped to change settings" warning banner in config panel
- Move per-project Environment Variables and Claude Instructions into modal dialogs
  for more editing space, with buttons in the config panel to open them
- Move global Claude Instructions into a modal in Settings panel
- Add default global Claude instruction recommending git initialization
- Add global environment variables support (full stack: Rust model, TS types,
  container creation with merge logic where project overrides global for same key,
  fingerprinting for recreation checks, and Settings UI with modal)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 01:21:33 +00:00
parent 1ce5151e59
commit 5a59fdb64b
10 changed files with 422 additions and 106 deletions

View File

@@ -170,6 +170,7 @@ pub async fn start_project_container(
&existing_id,
&project,
settings.global_claude_instructions.as_deref(),
&settings.global_custom_env_vars,
)
.await
.unwrap_or(false);
@@ -185,6 +186,7 @@ pub async fn start_project_container(
aws_config_path.as_deref(),
&settings.global_aws,
settings.global_claude_instructions.as_deref(),
&settings.global_custom_env_vars,
).await?;
docker::start_container(&new_id).await?;
new_id
@@ -201,6 +203,7 @@ pub async fn start_project_container(
aws_config_path.as_deref(),
&settings.global_aws,
settings.global_claude_instructions.as_deref(),
&settings.global_custom_env_vars,
).await?;
docker::start_container(&new_id).await?;
new_id

View File

@@ -30,6 +30,25 @@ fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
parts.join(",")
}
/// Merge global and per-project custom environment variables.
/// Per-project variables override global variables with the same key.
fn merge_custom_env_vars(global: &[EnvVar], project: &[EnvVar]) -> Vec<EnvVar> {
let mut merged: std::collections::HashMap<String, EnvVar> = std::collections::HashMap::new();
for ev in global {
let key = ev.key.trim().to_string();
if !key.is_empty() {
merged.insert(key, ev.clone());
}
}
for ev in project {
let key = ev.key.trim().to_string();
if !key.is_empty() {
merged.insert(key, ev.clone());
}
}
merged.into_values().collect()
}
/// Merge global and per-project Claude instructions into a single string.
fn merge_claude_instructions(
global_instructions: Option<&str>,
@@ -114,6 +133,7 @@ pub async fn create_container(
aws_config_path: Option<&str>,
global_aws: &GlobalAwsSettings,
global_claude_instructions: Option<&str>,
global_custom_env_vars: &[EnvVar],
) -> Result<String, String> {
let docker = get_docker()?;
let container_name = project.container_name();
@@ -222,9 +242,10 @@ pub async fn create_container(
}
}
// Custom environment variables
// Custom environment variables (global + per-project, project overrides global for same key)
let merged_env = merge_custom_env_vars(global_custom_env_vars, &project.custom_env_vars);
let reserved_prefixes = ["ANTHROPIC_", "AWS_", "GIT_", "HOST_", "CLAUDE_", "TRIPLE_C_"];
for env_var in &project.custom_env_vars {
for env_var in &merged_env {
let key = env_var.key.trim();
if key.is_empty() {
continue;
@@ -236,7 +257,7 @@ pub async fn create_container(
}
env_vars.push(format!("{}={}", key, env_var.value));
}
let custom_env_fingerprint = compute_env_fingerprint(&project.custom_env_vars);
let custom_env_fingerprint = compute_env_fingerprint(&merged_env);
env_vars.push(format!("TRIPLE_C_CUSTOM_ENV={}", custom_env_fingerprint));
// Claude instructions (global + per-project)
@@ -419,6 +440,7 @@ pub async fn container_needs_recreation(
container_id: &str,
project: &Project,
global_claude_instructions: Option<&str>,
global_custom_env_vars: &[EnvVar],
) -> Result<bool, String> {
let docker = get_docker()?;
let info = docker
@@ -547,7 +569,8 @@ pub async fn container_needs_recreation(
}
// ── Custom environment variables ──────────────────────────────────────
let expected_fingerprint = compute_env_fingerprint(&project.custom_env_vars);
let merged_env = merge_custom_env_vars(global_custom_env_vars, &project.custom_env_vars);
let expected_fingerprint = compute_env_fingerprint(&merged_env);
let container_fingerprint = get_env("TRIPLE_C_CUSTOM_ENV").unwrap_or_default();
if container_fingerprint != expected_fingerprint {
log::info!("Custom env vars mismatch (container={:?}, expected={:?})", container_fingerprint, expected_fingerprint);

View File

@@ -27,7 +27,7 @@ pub fn run() {
exec_manager: ExecSessionManager::new(),
})
.setup(|app| {
let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/icon.png"))
let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/icon.ico"))
.expect("Failed to load window icon");
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_icon(icon);

View File

@@ -1,9 +1,15 @@
use serde::{Deserialize, Serialize};
use super::project::EnvVar;
fn default_true() -> bool {
true
}
fn default_global_instructions() -> Option<String> {
Some("If the project is not initialized with git, recommend to the user to initialize and use git to track changes. This makes it easier to revert should something break.".to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ImageSource {
@@ -54,8 +60,10 @@ pub struct AppSettings {
pub custom_image_name: Option<String>,
#[serde(default)]
pub global_aws: GlobalAwsSettings,
#[serde(default)]
#[serde(default = "default_global_instructions")]
pub global_claude_instructions: Option<String>,
#[serde(default)]
pub global_custom_env_vars: Vec<EnvVar>,
#[serde(default = "default_true")]
pub auto_check_updates: bool,
#[serde(default)]
@@ -72,7 +80,8 @@ impl Default for AppSettings {
image_source: ImageSource::default(),
custom_image_name: None,
global_aws: GlobalAwsSettings::default(),
global_claude_instructions: None,
global_claude_instructions: default_global_instructions(),
global_custom_env_vars: Vec::new(),
auto_check_updates: true,
dismissed_update_version: None,
}