Add port mappings feature, update app icon, and enhance default instructions
All checks were successful
Build App / build-linux (push) Successful in 2m49s
Build App / build-windows (push) Successful in 4m57s

- Add per-project port mapping configuration (host:container port pairs with
  TCP/UDP protocol) stored in project config and applied as Docker port
  bindings at container creation. Port changes trigger automatic container
  recreation via fingerprint detection.
- Create PortMappingsModal UI component following the same pattern as
  EnvVarsModal, integrated into ProjectCard config panel.
- Inject port mapping details into CLAUDE_INSTRUCTIONS so Claude inside the
  container knows which ports are available for testing services.
- Update default global instructions for new installs to encourage use of
  subagents for long-running and parallel tasks.
- Replace app icons with new v2 sun logo design for better visibility at
  small sizes (taskbar/dock).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 14:36:51 +00:00
parent da078af73f
commit 06be613e36
12 changed files with 269 additions and 5 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -2,13 +2,13 @@ use bollard::container::{
Config, CreateContainerOptions, ListContainersOptions, RemoveContainerOptions,
StartContainerOptions, StopContainerOptions,
};
use bollard::models::{ContainerSummary, HostConfig, Mount, MountTypeEnum};
use bollard::models::{ContainerSummary, HostConfig, Mount, MountTypeEnum, PortBinding};
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use super::client::get_docker;
use crate::models::{AuthMode, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, Project, ProjectPath};
use crate::models::{AuthMode, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, PortMapping, Project, ProjectPath};
/// Compute a fingerprint string for the custom environment variables.
/// Sorted alphabetically so order changes do not cause spurious recreation.
@@ -95,6 +95,20 @@ fn compute_paths_fingerprint(paths: &[ProjectPath]) -> String {
format!("{:x}", hasher.finish())
}
/// Compute a fingerprint for port mappings so we can detect changes.
/// Sorted so order changes don't cause spurious recreation.
fn compute_ports_fingerprint(port_mappings: &[PortMapping]) -> String {
let mut parts: Vec<String> = port_mappings
.iter()
.map(|p| format!("{}:{}:{}", p.host_port, p.container_port, p.protocol))
.collect();
parts.sort();
let joined = parts.join(",");
let mut hasher = DefaultHasher::new();
joined.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
pub async fn find_existing_container(project: &Project) -> Result<Option<String>, String> {
let docker = get_docker()?;
let container_name = project.container_name();
@@ -255,11 +269,27 @@ pub async fn create_container(
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)
let combined_instructions = merge_claude_instructions(
// Claude instructions (global + per-project, plus port mapping info)
let mut combined_instructions = merge_claude_instructions(
global_claude_instructions,
project.claude_instructions.as_deref(),
);
if !project.port_mappings.is_empty() {
let mut port_lines: Vec<String> = Vec::new();
port_lines.push("## Available Port Mappings".to_string());
port_lines.push("The following ports are mapped from the host to this container. Use these container ports when starting services that need to be accessible from the host:".to_string());
for pm in &project.port_mappings {
port_lines.push(format!(
"- Host port {} -> Container port {} ({})",
pm.host_port, pm.container_port, pm.protocol
));
}
let port_info = port_lines.join("\n");
combined_instructions = Some(match combined_instructions {
Some(existing) => format!("{}\n\n{}", existing, port_info),
None => port_info,
});
}
if let Some(ref instructions) = combined_instructions {
env_vars.push(format!("CLAUDE_INSTRUCTIONS={}", instructions));
}
@@ -346,6 +376,21 @@ pub async fn create_container(
});
}
// Port mappings
let mut exposed_ports: HashMap<String, HashMap<(), ()>> = HashMap::new();
let mut port_bindings: HashMap<String, Option<Vec<PortBinding>>> = HashMap::new();
for pm in &project.port_mappings {
let container_key = format!("{}/{}", pm.container_port, pm.protocol);
exposed_ports.insert(container_key.clone(), HashMap::new());
port_bindings.insert(
container_key,
Some(vec![PortBinding {
host_ip: Some("0.0.0.0".to_string()),
host_port: Some(pm.host_port.to_string()),
}]),
);
}
let mut labels = HashMap::new();
labels.insert("triple-c.managed".to_string(), "true".to_string());
labels.insert("triple-c.project-id".to_string(), project.id.clone());
@@ -353,10 +398,12 @@ pub async fn create_container(
labels.insert("triple-c.auth-mode".to_string(), format!("{:?}", project.auth_mode));
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.ports-fingerprint".to_string(), compute_ports_fingerprint(&project.port_mappings));
labels.insert("triple-c.image".to_string(), image_name.to_string());
let host_config = HostConfig {
mounts: Some(mounts),
port_bindings: if port_bindings.is_empty() { None } else { Some(port_bindings) },
..Default::default()
};
@@ -373,6 +420,7 @@ pub async fn create_container(
labels: Some(labels),
working_dir: Some(working_dir),
host_config: Some(host_config),
exposed_ports: if exposed_ports.is_empty() { None } else { Some(exposed_ports) },
tty: Some(true),
..Default::default()
};
@@ -488,6 +536,14 @@ pub async fn container_needs_recreation(
}
}
// ── Port mappings fingerprint ──────────────────────────────────────────
let expected_ports_fp = compute_ports_fingerprint(&project.port_mappings);
let container_ports_fp = get_label("triple-c.ports-fingerprint").unwrap_or_default();
if container_ports_fp != expected_ports_fp {
log::info!("Port mappings fingerprint mismatch (container={:?}, expected={:?})", container_ports_fp, expected_ports_fp);
return Ok(true);
}
// ── Bedrock config fingerprint ───────────────────────────────────────
let expected_bedrock_fp = compute_bedrock_fingerprint(project);
let container_bedrock_fp = get_label("triple-c.bedrock-fingerprint").unwrap_or_default();

View File

@@ -7,7 +7,7 @@ fn default_true() -> bool {
}
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())
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.\n\nUse subagents frequently. For long-running tasks, break the work into parallel subagents where possible. When handling multiple separate tasks, delegate each to its own subagent so they can run concurrently.".to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]

View File

@@ -12,6 +12,18 @@ pub struct ProjectPath {
pub mount_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PortMapping {
pub host_port: u16,
pub container_port: u16,
#[serde(default = "default_protocol")]
pub protocol: String,
}
fn default_protocol() -> String {
"tcp".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
pub id: String,
@@ -30,6 +42,8 @@ pub struct Project {
#[serde(default)]
pub custom_env_vars: Vec<EnvVar>,
#[serde(default)]
pub port_mappings: Vec<PortMapping>,
#[serde(default)]
pub claude_instructions: Option<String>,
pub created_at: String,
pub updated_at: String,
@@ -114,6 +128,7 @@ impl Project {
git_user_name: None,
git_user_email: None,
custom_env_vars: Vec::new(),
port_mappings: Vec::new(),
claude_instructions: None,
created_at: now.clone(),
updated_at: now,