Compare commits

..

4 Commits

Author SHA1 Message Date
df3d434877 Fix SSH keys, git config, and HTTPS token not applied on container restart
All checks were successful
Build App / build-linux (push) Successful in 2m26s
Build App / build-windows (push) Successful in 3m17s
Recreate the container when SSH key path, git name, git email, or git
HTTPS token change — not just when the docker socket toggle changes.
The claude config named volume persists across recreation so no data
is lost.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:37:06 +00:00
60842befde Fix UI padding and text flush against container edges
All checks were successful
Build App / build-windows (push) Successful in 3m16s
Build App / build-linux (push) Successful in 4m16s
- Remove global * { padding: 0 } reset that was overriding all Tailwind
  padding classes (unlayered CSS beats Tailwind v4 @layer utilities)
- Add color-scheme: dark to fix native form controls (select dropdowns)
  rendering with white backgrounds
- Make sidebar responsive (25% width, min 224px, max 320px)
- Increase internal padding on TopBar, Sidebar, ProjectList, StatusBar
- Add flex-shrink-0 to TopBar status indicators to prevent clipping
- Allow project action buttons to wrap on narrow sidebars
- Increase terminal view padding for breathing room

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 10:31:27 -08:00
1a78378ed7 Fix docker socket not mounting when toggling container spawning
All checks were successful
Build App / build-linux (push) Successful in 2m39s
Build App / build-windows (push) Successful in 3m10s
When "Allow container spawning" was toggled on an existing container,
the docker socket mount was never applied because the container was
simply restarted rather than recreated. Now inspects the existing
container's mounts and recreates it when there's a mismatch, preserving
the named config volume (keyed by project ID) across recreation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 09:56:39 -08:00
0d4ed86f53 adding claude settings 2026-02-27 09:40:19 -08:00
11 changed files with 117 additions and 18 deletions

View File

@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(.:*)"
"Bash(.:*)",
"Bash(git:*)"
]
}
}

View File

@@ -102,9 +102,33 @@ pub async fn start_project_container(
// Check for existing container
let container_id = if let Some(existing_id) = docker::find_existing_container(&project).await? {
// Start existing container
docker::start_container(&existing_id).await?;
existing_id
// Compare the running container's configuration (mounts, env vars)
// against the current project settings. If anything changed (SSH key
// path, git config, docker socket, etc.) we recreate the container.
// Safe to recreate: the claude config named volume is keyed by
// project ID (not container ID) so it persists across recreation.
let needs_recreation = docker::container_needs_recreation(&existing_id, &project)
.await
.unwrap_or(false);
if needs_recreation {
log::info!("Container config changed, recreating container for project {}", project.id);
let _ = docker::stop_container(&existing_id).await;
docker::remove_container(&existing_id).await?;
let new_id = docker::create_container(
&project,
api_key.as_deref(),
&docker_socket,
&image_name,
aws_config_path.as_deref(),
&settings.global_aws,
).await?;
docker::start_container(&new_id).await?;
new_id
} else {
// Start existing container as-is
docker::start_container(&existing_id).await?;
existing_id
}
} else {
// Create new container
let new_id = docker::create_container(

View File

@@ -288,6 +288,84 @@ pub async fn remove_container(container_id: &str) -> Result<(), String> {
.map_err(|e| format!("Failed to remove container: {}", e))
}
/// Check whether the existing container's configuration still matches the
/// current project settings. Returns `true` when the container must be
/// recreated (mounts or env vars differ).
pub async fn container_needs_recreation(container_id: &str, project: &Project) -> Result<bool, String> {
let docker = get_docker()?;
let info = docker
.inspect_container(container_id, None)
.await
.map_err(|e| format!("Failed to inspect container: {}", e))?;
let mounts = info
.host_config
.as_ref()
.and_then(|hc| hc.mounts.as_ref());
// ── Docker socket mount ──────────────────────────────────────────────
let has_socket = mounts
.map(|m| {
m.iter()
.any(|mount| mount.target.as_deref() == Some("/var/run/docker.sock"))
})
.unwrap_or(false);
if has_socket != project.allow_docker_access {
log::info!("Docker socket mismatch (container={}, project={})", has_socket, project.allow_docker_access);
return Ok(true);
}
// ── SSH key path mount ───────────────────────────────────────────────
let ssh_mount_source = mounts
.and_then(|m| {
m.iter()
.find(|mount| mount.target.as_deref() == Some("/tmp/.host-ssh"))
})
.and_then(|mount| mount.source.as_deref());
let project_ssh = project.ssh_key_path.as_deref();
if ssh_mount_source != project_ssh {
log::info!(
"SSH key path mismatch (container={:?}, project={:?})",
ssh_mount_source,
project_ssh
);
return Ok(true);
}
// ── Git environment variables ────────────────────────────────────────
let env_vars = info
.config
.as_ref()
.and_then(|c| c.env.as_ref());
let get_env = |name: &str| -> Option<String> {
env_vars.and_then(|vars| {
vars.iter()
.find(|v| v.starts_with(&format!("{}=", name)))
.map(|v| v[name.len() + 1..].to_string())
})
};
let container_git_name = get_env("GIT_USER_NAME");
let container_git_email = get_env("GIT_USER_EMAIL");
let container_git_token = get_env("GIT_TOKEN");
if container_git_name.as_deref() != project.git_user_name.as_deref() {
log::info!("GIT_USER_NAME mismatch (container={:?}, project={:?})", container_git_name, project.git_user_name);
return Ok(true);
}
if container_git_email.as_deref() != project.git_user_email.as_deref() {
log::info!("GIT_USER_EMAIL mismatch (container={:?}, project={:?})", container_git_email, project.git_user_email);
return Ok(true);
}
if container_git_token.as_deref() != project.git_token.as_deref() {
log::info!("GIT_TOKEN mismatch");
return Ok(true);
}
Ok(false)
}
pub async fn get_container_info(project: &Project) -> Result<Option<ContainerInfo>, String> {
if let Some(ref container_id) = project.container_id {
let docker = get_docker()?;

View File

@@ -6,7 +6,7 @@ export default function Sidebar() {
const { sidebarView, setSidebarView } = useAppState();
return (
<div className="flex flex-col h-full w-64 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg overflow-hidden">
<div className="flex flex-col h-full w-[25%] min-w-56 max-w-80 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg overflow-hidden">
{/* Nav tabs */}
<div className="flex border-b border-[var(--border-color)]">
<button
@@ -32,7 +32,7 @@ export default function Sidebar() {
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto p-1">
{sidebarView === "projects" ? <ProjectList /> : <SettingsPanel />}
</div>
</div>

View File

@@ -5,7 +5,7 @@ export default function StatusBar() {
const running = projects.filter((p) => p.status === "running").length;
return (
<div className="flex items-center h-6 px-3 bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg text-xs text-[var(--text-secondary)]">
<div className="flex items-center h-6 px-4 bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg text-xs text-[var(--text-secondary)]">
<span>
{projects.length} project{projects.length !== 1 ? "s" : ""}
</span>

View File

@@ -6,10 +6,10 @@ export default function TopBar() {
return (
<div className="flex items-center h-10 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg overflow-hidden">
<div className="flex-1 overflow-x-auto">
<div className="flex-1 overflow-x-auto pl-2">
<TerminalTabs />
</div>
<div className="flex items-center gap-2 px-3 text-xs text-[var(--text-secondary)]">
<div className="flex items-center gap-2 px-4 flex-shrink-0 text-xs text-[var(--text-secondary)]">
<StatusDot ok={dockerAvailable === true} label="Docker" />
<StatusDot ok={imageExists === true} label="Image" />
</div>

View File

@@ -159,7 +159,7 @@ export default function ProjectCard({ project }: Props) {
</div>
{/* Action buttons */}
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 flex-wrap">
{isStopped ? (
<>
<ActionButton onClick={handleStart} disabled={loading} label="Start" />

View File

@@ -8,7 +8,7 @@ export default function ProjectList() {
const [showAdd, setShowAdd] = useState(false);
return (
<div className="p-2">
<div className="p-3">
<div className="flex items-center justify-between px-2 py-1 mb-2">
<span className="text-xs font-semibold uppercase text-[var(--text-secondary)]">
Projects

View File

@@ -83,7 +83,7 @@ export default function AwsSettings() {
<select
value={globalAws.aws_profile ?? ""}
onChange={(e) => handleChange("aws_profile", e.target.value)}
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)] text-[var(--text-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
>
<option value="">None (use default)</option>
{profiles.map((p) => (

View File

@@ -170,7 +170,7 @@ export default function TerminalView({ sessionId, active }: Props) {
<div
ref={containerRef}
className={`w-full h-full ${active ? "" : "hidden"}`}
style={{ padding: "4px" }}
style={{ padding: "8px" }}
/>
);
}

View File

@@ -12,13 +12,9 @@
--success: #3fb950;
--warning: #d29922;
--error: #f85149;
color-scheme: dark;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
height: 100%;