2026-02-28 20:42:40 +00:00
|
|
|
import { useState, useEffect } from "react";
|
2026-02-27 04:29:51 +00:00
|
|
|
import { open } from "@tauri-apps/plugin-dialog";
|
2026-03-04 07:43:01 -08:00
|
|
|
import { listen } from "@tauri-apps/api/event";
|
2026-02-28 21:18:33 +00:00
|
|
|
import type { Project, ProjectPath, AuthMode, BedrockConfig, BedrockAuthMethod } from "../../lib/types";
|
2026-02-27 04:29:51 +00:00
|
|
|
import { useProjects } from "../../hooks/useProjects";
|
2026-03-04 08:57:12 -08:00
|
|
|
import { useMcpServers } from "../../hooks/useMcpServers";
|
2026-02-27 04:29:51 +00:00
|
|
|
import { useTerminal } from "../../hooks/useTerminal";
|
|
|
|
|
import { useAppState } from "../../store/appState";
|
2026-03-01 01:21:33 +00:00
|
|
|
import EnvVarsModal from "./EnvVarsModal";
|
2026-03-01 14:36:51 +00:00
|
|
|
import PortMappingsModal from "./PortMappingsModal";
|
2026-03-01 01:21:33 +00:00
|
|
|
import ClaudeInstructionsModal from "./ClaudeInstructionsModal";
|
2026-03-04 08:22:35 -08:00
|
|
|
import ContainerProgressModal from "./ContainerProgressModal";
|
2026-02-27 04:29:51 +00:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
project: Project;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ProjectCard({ project }: Props) {
|
2026-02-28 20:42:40 +00:00
|
|
|
const selectedProjectId = useAppState(s => s.selectedProjectId);
|
|
|
|
|
const setSelectedProject = useAppState(s => s.setSelectedProject);
|
2026-02-27 04:29:51 +00:00
|
|
|
const { start, stop, rebuild, remove, update } = useProjects();
|
2026-03-04 08:57:12 -08:00
|
|
|
const { mcpServers } = useMcpServers();
|
2026-02-27 04:29:51 +00:00
|
|
|
const { open: openTerminal } = useTerminal();
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [showConfig, setShowConfig] = useState(false);
|
2026-03-01 01:21:33 +00:00
|
|
|
const [showEnvVarsModal, setShowEnvVarsModal] = useState(false);
|
2026-03-01 14:36:51 +00:00
|
|
|
const [showPortMappingsModal, setShowPortMappingsModal] = useState(false);
|
2026-03-01 01:21:33 +00:00
|
|
|
const [showClaudeInstructionsModal, setShowClaudeInstructionsModal] = useState(false);
|
2026-03-04 07:43:01 -08:00
|
|
|
const [progressMsg, setProgressMsg] = useState<string | null>(null);
|
2026-03-04 08:22:35 -08:00
|
|
|
const [activeOperation, setActiveOperation] = useState<"starting" | "stopping" | "resetting" | null>(null);
|
|
|
|
|
const [operationCompleted, setOperationCompleted] = useState(false);
|
2026-03-05 17:39:34 -08:00
|
|
|
const [showRemoveConfirm, setShowRemoveConfirm] = useState(false);
|
|
|
|
|
const [isEditingName, setIsEditingName] = useState(false);
|
|
|
|
|
const [editName, setEditName] = useState(project.name);
|
2026-02-27 04:29:51 +00:00
|
|
|
const isSelected = selectedProjectId === project.id;
|
|
|
|
|
const isStopped = project.status === "stopped" || project.status === "error";
|
|
|
|
|
|
2026-02-28 20:42:40 +00:00
|
|
|
// Local state for text fields (save on blur, not on every keystroke)
|
2026-02-28 21:18:33 +00:00
|
|
|
const [paths, setPaths] = useState<ProjectPath[]>(project.paths ?? []);
|
2026-02-28 20:42:40 +00:00
|
|
|
const [sshKeyPath, setSshKeyPath] = useState(project.ssh_key_path ?? "");
|
|
|
|
|
const [gitName, setGitName] = useState(project.git_user_name ?? "");
|
|
|
|
|
const [gitEmail, setGitEmail] = useState(project.git_user_email ?? "");
|
|
|
|
|
const [gitToken, setGitToken] = useState(project.git_token ?? "");
|
|
|
|
|
const [claudeInstructions, setClaudeInstructions] = useState(project.claude_instructions ?? "");
|
|
|
|
|
const [envVars, setEnvVars] = useState(project.custom_env_vars ?? []);
|
2026-03-01 14:36:51 +00:00
|
|
|
const [portMappings, setPortMappings] = useState(project.port_mappings ?? []);
|
2026-02-28 20:42:40 +00:00
|
|
|
|
|
|
|
|
// Bedrock local state for text fields
|
|
|
|
|
const [bedrockRegion, setBedrockRegion] = useState(project.bedrock_config?.aws_region ?? "us-east-1");
|
|
|
|
|
const [bedrockAccessKeyId, setBedrockAccessKeyId] = useState(project.bedrock_config?.aws_access_key_id ?? "");
|
|
|
|
|
const [bedrockSecretKey, setBedrockSecretKey] = useState(project.bedrock_config?.aws_secret_access_key ?? "");
|
|
|
|
|
const [bedrockSessionToken, setBedrockSessionToken] = useState(project.bedrock_config?.aws_session_token ?? "");
|
|
|
|
|
const [bedrockProfile, setBedrockProfile] = useState(project.bedrock_config?.aws_profile ?? "");
|
|
|
|
|
const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? "");
|
|
|
|
|
const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? "");
|
|
|
|
|
|
|
|
|
|
// Sync local state when project prop changes (e.g., after save or external update)
|
|
|
|
|
useEffect(() => {
|
2026-03-05 17:39:34 -08:00
|
|
|
setEditName(project.name);
|
2026-02-28 21:18:33 +00:00
|
|
|
setPaths(project.paths ?? []);
|
2026-02-28 20:42:40 +00:00
|
|
|
setSshKeyPath(project.ssh_key_path ?? "");
|
|
|
|
|
setGitName(project.git_user_name ?? "");
|
|
|
|
|
setGitEmail(project.git_user_email ?? "");
|
|
|
|
|
setGitToken(project.git_token ?? "");
|
|
|
|
|
setClaudeInstructions(project.claude_instructions ?? "");
|
|
|
|
|
setEnvVars(project.custom_env_vars ?? []);
|
2026-03-01 14:36:51 +00:00
|
|
|
setPortMappings(project.port_mappings ?? []);
|
2026-02-28 20:42:40 +00:00
|
|
|
setBedrockRegion(project.bedrock_config?.aws_region ?? "us-east-1");
|
|
|
|
|
setBedrockAccessKeyId(project.bedrock_config?.aws_access_key_id ?? "");
|
|
|
|
|
setBedrockSecretKey(project.bedrock_config?.aws_secret_access_key ?? "");
|
|
|
|
|
setBedrockSessionToken(project.bedrock_config?.aws_session_token ?? "");
|
|
|
|
|
setBedrockProfile(project.bedrock_config?.aws_profile ?? "");
|
|
|
|
|
setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? "");
|
|
|
|
|
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
|
|
|
|
}, [project]);
|
|
|
|
|
|
2026-03-04 07:43:01 -08:00
|
|
|
// Listen for container progress events
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const unlisten = listen<{ project_id: string; message: string }>(
|
|
|
|
|
"container-progress",
|
|
|
|
|
(event) => {
|
|
|
|
|
if (event.payload.project_id === project.id) {
|
|
|
|
|
setProgressMsg(event.payload.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
return () => { unlisten.then((f) => f()); };
|
|
|
|
|
}, [project.id]);
|
|
|
|
|
|
2026-03-04 08:22:35 -08:00
|
|
|
// Mark operation completed when status settles
|
2026-03-04 07:43:01 -08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (project.status === "running" || project.status === "stopped" || project.status === "error") {
|
2026-03-04 08:22:35 -08:00
|
|
|
if (activeOperation) {
|
|
|
|
|
setOperationCompleted(true);
|
|
|
|
|
}
|
|
|
|
|
// Clear progress if no modal is managing it
|
|
|
|
|
if (!activeOperation) {
|
|
|
|
|
setProgressMsg(null);
|
|
|
|
|
}
|
2026-03-04 07:43:01 -08:00
|
|
|
}
|
2026-03-04 08:22:35 -08:00
|
|
|
}, [project.status, activeOperation]);
|
2026-03-04 07:43:01 -08:00
|
|
|
|
2026-02-27 04:29:51 +00:00
|
|
|
const handleStart = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
2026-03-04 08:22:35 -08:00
|
|
|
setProgressMsg(null);
|
|
|
|
|
setOperationCompleted(false);
|
|
|
|
|
setActiveOperation("starting");
|
2026-02-27 04:29:51 +00:00
|
|
|
try {
|
|
|
|
|
await start(project.id);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(String(e));
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleStop = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
2026-03-04 08:22:35 -08:00
|
|
|
setProgressMsg(null);
|
|
|
|
|
setOperationCompleted(false);
|
|
|
|
|
setActiveOperation("stopping");
|
2026-02-27 04:29:51 +00:00
|
|
|
try {
|
|
|
|
|
await stop(project.id);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(String(e));
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOpenTerminal = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await openTerminal(project.id, project.name);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(String(e));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-04 08:22:35 -08:00
|
|
|
const handleForceStop = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await stop(project.id);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(String(e));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeModal = () => {
|
|
|
|
|
setActiveOperation(null);
|
|
|
|
|
setOperationCompleted(false);
|
|
|
|
|
setProgressMsg(null);
|
|
|
|
|
setError(null);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-27 14:29:40 +00:00
|
|
|
const defaultBedrockConfig: BedrockConfig = {
|
|
|
|
|
auth_method: "static_credentials",
|
|
|
|
|
aws_region: "us-east-1",
|
|
|
|
|
aws_access_key_id: null,
|
|
|
|
|
aws_secret_access_key: null,
|
|
|
|
|
aws_session_token: null,
|
|
|
|
|
aws_profile: null,
|
|
|
|
|
aws_bearer_token: null,
|
|
|
|
|
model_id: null,
|
|
|
|
|
disable_prompt_caching: false,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-27 04:29:51 +00:00
|
|
|
const handleAuthModeChange = async (mode: AuthMode) => {
|
|
|
|
|
try {
|
2026-02-27 14:29:40 +00:00
|
|
|
const updates: Partial<Project> = { auth_mode: mode };
|
|
|
|
|
if (mode === "bedrock" && !project.bedrock_config) {
|
|
|
|
|
updates.bedrock_config = defaultBedrockConfig;
|
|
|
|
|
}
|
|
|
|
|
await update({ ...project, ...updates });
|
2026-02-27 04:29:51 +00:00
|
|
|
} catch (e) {
|
|
|
|
|
setError(String(e));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-27 14:29:40 +00:00
|
|
|
const updateBedrockConfig = async (patch: Partial<BedrockConfig>) => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, ...patch } });
|
2026-02-28 20:42:40 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock config:", err);
|
|
|
|
|
}
|
2026-02-27 14:29:40 +00:00
|
|
|
};
|
|
|
|
|
|
2026-02-27 04:29:51 +00:00
|
|
|
const handleBrowseSSH = async () => {
|
|
|
|
|
const selected = await open({ directory: true, multiple: false });
|
|
|
|
|
if (selected) {
|
|
|
|
|
try {
|
|
|
|
|
await update({ ...project, ssh_key_path: selected as string });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(String(e));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-28 20:42:40 +00:00
|
|
|
// Blur handlers for text fields
|
|
|
|
|
const handleSshKeyPathBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await update({ ...project, ssh_key_path: sshKeyPath || null });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update SSH key path:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGitNameBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await update({ ...project, git_user_name: gitName || null });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Git name:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGitEmailBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await update({ ...project, git_user_email: gitEmail || null });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Git email:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGitTokenBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await update({ ...project, git_token: gitToken || null });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Git token:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBedrockRegionBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, aws_region: bedrockRegion } });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock region:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBedrockAccessKeyIdBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, aws_access_key_id: bedrockAccessKeyId || null } });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock access key:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBedrockSecretKeyBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, aws_secret_access_key: bedrockSecretKey || null } });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock secret key:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBedrockSessionTokenBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, aws_session_token: bedrockSessionToken || null } });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock session token:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBedrockProfileBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, aws_profile: bedrockProfile || null } });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock profile:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBedrockBearerTokenBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, aws_bearer_token: bedrockBearerToken || null } });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock bearer token:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBedrockModelIdBlur = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
await update({ ...project, bedrock_config: { ...current, model_id: bedrockModelId || null } });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update Bedrock model ID:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-27 04:29:51 +00:00
|
|
|
const statusColor = {
|
|
|
|
|
stopped: "bg-[var(--text-secondary)]",
|
|
|
|
|
starting: "bg-[var(--warning)]",
|
|
|
|
|
running: "bg-[var(--success)]",
|
|
|
|
|
stopping: "bg-[var(--warning)]",
|
|
|
|
|
error: "bg-[var(--error)]",
|
|
|
|
|
}[project.status];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
onClick={() => setSelectedProject(project.id)}
|
2026-02-28 22:53:30 +00:00
|
|
|
className={`px-3 py-2 rounded cursor-pointer transition-colors min-w-0 overflow-hidden ${
|
2026-02-27 04:29:51 +00:00
|
|
|
isSelected
|
|
|
|
|
? "bg-[var(--bg-tertiary)]"
|
|
|
|
|
: "hover:bg-[var(--bg-tertiary)]"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColor}`} />
|
2026-03-05 17:39:34 -08:00
|
|
|
{isEditingName ? (
|
|
|
|
|
<input
|
|
|
|
|
autoFocus
|
|
|
|
|
value={editName}
|
|
|
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
|
|
|
onBlur={async () => {
|
|
|
|
|
setIsEditingName(false);
|
|
|
|
|
const trimmed = editName.trim();
|
|
|
|
|
if (trimmed && trimmed !== project.name) {
|
|
|
|
|
try {
|
|
|
|
|
await update({ ...project, name: trimmed });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to rename project:", err);
|
|
|
|
|
setEditName(project.name);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
setEditName(project.name);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter") (e.target as HTMLInputElement).blur();
|
|
|
|
|
if (e.key === "Escape") { setEditName(project.name); setIsEditingName(false); }
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
className="text-sm font-medium flex-1 min-w-0 px-1 py-0 bg-[var(--bg-primary)] border border-[var(--accent)] rounded text-[var(--text-primary)] focus:outline-none"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span
|
|
|
|
|
className="text-sm font-medium truncate flex-1 cursor-text"
|
|
|
|
|
onDoubleClick={(e) => { e.stopPropagation(); setIsEditingName(true); }}
|
|
|
|
|
>
|
|
|
|
|
{project.name}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-02-27 04:29:51 +00:00
|
|
|
</div>
|
2026-02-28 21:18:33 +00:00
|
|
|
<div className="mt-0.5 ml-4 space-y-0.5">
|
|
|
|
|
{project.paths.map((pp, i) => (
|
|
|
|
|
<div key={i} className="text-xs text-[var(--text-secondary)] truncate">
|
|
|
|
|
<span className="font-mono">/workspace/{pp.mount_name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-02-27 04:29:51 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isSelected && (
|
2026-02-28 22:53:30 +00:00
|
|
|
<div className="mt-2 ml-4 space-y-2 min-w-0 overflow-hidden">
|
2026-02-27 04:29:51 +00:00
|
|
|
{/* Auth mode selector */}
|
|
|
|
|
<div className="flex items-center gap-1 text-xs">
|
|
|
|
|
<span className="text-[var(--text-secondary)] mr-1">Auth:</span>
|
|
|
|
|
<button
|
2026-03-01 03:10:57 +00:00
|
|
|
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("anthropic"); }}
|
2026-02-27 04:29:51 +00:00
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={`px-2 py-0.5 rounded transition-colors ${
|
2026-03-01 03:10:57 +00:00
|
|
|
project.auth_mode === "anthropic"
|
2026-02-27 04:29:51 +00:00
|
|
|
? "bg-[var(--accent)] text-white"
|
|
|
|
|
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
|
|
|
|
|
} disabled:opacity-50`}
|
|
|
|
|
>
|
2026-03-01 03:10:57 +00:00
|
|
|
Anthropic
|
2026-02-27 04:29:51 +00:00
|
|
|
</button>
|
2026-02-27 14:29:40 +00:00
|
|
|
<button
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("bedrock"); }}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={`px-2 py-0.5 rounded transition-colors ${
|
|
|
|
|
project.auth_mode === "bedrock"
|
|
|
|
|
? "bg-[var(--accent)] text-white"
|
|
|
|
|
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
|
|
|
|
|
} disabled:opacity-50`}
|
|
|
|
|
>
|
|
|
|
|
Bedrock
|
|
|
|
|
</button>
|
2026-02-27 04:29:51 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Action buttons */}
|
2026-02-27 10:31:27 -08:00
|
|
|
<div className="flex items-center gap-1 flex-wrap">
|
2026-02-27 04:29:51 +00:00
|
|
|
{isStopped ? (
|
|
|
|
|
<>
|
2026-02-27 14:29:40 +00:00
|
|
|
<ActionButton onClick={handleStart} disabled={loading} label="Start" />
|
2026-02-27 04:29:51 +00:00
|
|
|
<ActionButton
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
setLoading(true);
|
2026-03-04 08:22:35 -08:00
|
|
|
setError(null);
|
|
|
|
|
setProgressMsg(null);
|
|
|
|
|
setOperationCompleted(false);
|
|
|
|
|
setActiveOperation("resetting");
|
2026-02-27 04:29:51 +00:00
|
|
|
try { await rebuild(project.id); } catch (e) { setError(String(e)); }
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
label="Reset"
|
|
|
|
|
/>
|
|
|
|
|
</>
|
2026-02-27 14:29:40 +00:00
|
|
|
) : project.status === "running" ? (
|
|
|
|
|
<>
|
|
|
|
|
<ActionButton onClick={handleStop} disabled={loading} label="Stop" />
|
|
|
|
|
<ActionButton onClick={handleOpenTerminal} disabled={loading} label="Terminal" accent />
|
|
|
|
|
</>
|
2026-02-27 04:29:51 +00:00
|
|
|
) : (
|
2026-03-04 07:12:49 -08:00
|
|
|
<>
|
|
|
|
|
<span className="text-xs text-[var(--text-secondary)]">
|
2026-03-04 07:43:01 -08:00
|
|
|
{progressMsg ?? `${project.status}...`}
|
2026-03-04 07:12:49 -08:00
|
|
|
</span>
|
|
|
|
|
<ActionButton onClick={handleStop} disabled={loading} label="Force Stop" danger />
|
|
|
|
|
</>
|
2026-02-27 04:29:51 +00:00
|
|
|
)}
|
|
|
|
|
<ActionButton
|
|
|
|
|
onClick={(e) => { e?.stopPropagation?.(); setShowConfig(!showConfig); }}
|
|
|
|
|
disabled={false}
|
|
|
|
|
label={showConfig ? "Hide" : "Config"}
|
|
|
|
|
/>
|
2026-03-05 17:39:34 -08:00
|
|
|
{showRemoveConfirm ? (
|
|
|
|
|
<span className="inline-flex items-center gap-1 text-xs">
|
|
|
|
|
<span className="text-[var(--text-secondary)]">Remove?</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={async (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setShowRemoveConfirm(false);
|
|
|
|
|
await remove(project.id);
|
|
|
|
|
}}
|
|
|
|
|
className="px-1.5 py-0.5 rounded text-white bg-[var(--error)] hover:opacity-80 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Yes
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); setShowRemoveConfirm(false); }}
|
|
|
|
|
className="px-1.5 py-0.5 rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors"
|
|
|
|
|
>
|
|
|
|
|
No
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<ActionButton
|
|
|
|
|
onClick={() => setShowRemoveConfirm(true)}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
label="Remove"
|
|
|
|
|
danger
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-02-27 04:29:51 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Config panel */}
|
|
|
|
|
{showConfig && (
|
2026-02-28 22:53:30 +00:00
|
|
|
<div className="space-y-2 pt-1 border-t border-[var(--border-color)] min-w-0 overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
2026-03-01 01:21:33 +00:00
|
|
|
{!isStopped && (
|
|
|
|
|
<div className="px-2 py-1.5 bg-[var(--warning)]/15 border border-[var(--warning)]/30 rounded text-xs text-[var(--warning)]">
|
|
|
|
|
Container must be stopped to change settings.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-28 21:18:33 +00:00
|
|
|
{/* Folder paths */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Folders</label>
|
|
|
|
|
{paths.map((pp, i) => (
|
2026-02-28 22:53:30 +00:00
|
|
|
<div key={i} className="mb-1">
|
|
|
|
|
<div className="flex gap-1 items-center min-w-0">
|
|
|
|
|
<input
|
|
|
|
|
value={pp.host_path}
|
|
|
|
|
onChange={(e) => {
|
2026-02-28 21:18:33 +00:00
|
|
|
const updated = [...paths];
|
2026-02-28 22:53:30 +00:00
|
|
|
updated[i] = { ...updated[i], host_path: e.target.value };
|
2026-02-28 21:18:33 +00:00
|
|
|
setPaths(updated);
|
2026-02-28 22:53:30 +00:00
|
|
|
}}
|
|
|
|
|
onBlur={async () => {
|
|
|
|
|
try { await update({ ...project, paths }); } catch (err) {
|
2026-02-28 21:18:33 +00:00
|
|
|
console.error("Failed to update paths:", err);
|
|
|
|
|
}
|
2026-02-28 22:53:30 +00:00
|
|
|
}}
|
|
|
|
|
placeholder="/path/to/folder"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="flex-1 min-w-0 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
|
|
|
|
/>
|
2026-02-28 21:18:33 +00:00
|
|
|
<button
|
|
|
|
|
onClick={async () => {
|
2026-02-28 22:53:30 +00:00
|
|
|
const selected = await open({ directory: true, multiple: false });
|
|
|
|
|
if (typeof selected === "string") {
|
|
|
|
|
const updated = [...paths];
|
|
|
|
|
const basename = selected.replace(/[/\\]$/, "").split(/[/\\]/).pop() || "";
|
|
|
|
|
updated[i] = { host_path: selected, mount_name: updated[i].mount_name || basename };
|
|
|
|
|
setPaths(updated);
|
|
|
|
|
try { await update({ ...project, paths: updated }); } catch (err) {
|
|
|
|
|
console.error("Failed to update paths:", err);
|
|
|
|
|
}
|
2026-02-28 21:18:33 +00:00
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
disabled={!isStopped}
|
2026-02-28 22:53:30 +00:00
|
|
|
className="flex-shrink-0 px-2 py-1 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] disabled:opacity-50 transition-colors"
|
2026-02-28 21:18:33 +00:00
|
|
|
>
|
2026-02-28 22:53:30 +00:00
|
|
|
...
|
2026-02-28 21:18:33 +00:00
|
|
|
</button>
|
2026-02-28 22:53:30 +00:00
|
|
|
{paths.length > 1 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
const updated = paths.filter((_, j) => j !== i);
|
|
|
|
|
setPaths(updated);
|
|
|
|
|
try { await update({ ...project, paths: updated }); } catch (err) {
|
|
|
|
|
console.error("Failed to remove path:", err);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="flex-shrink-0 px-1.5 py-1 text-xs text-[var(--error)] hover:bg-[var(--bg-primary)] rounded disabled:opacity-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
x
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-1 items-center mt-0.5 min-w-0">
|
|
|
|
|
<span className="text-xs text-[var(--text-secondary)]">/workspace/</span>
|
|
|
|
|
<input
|
|
|
|
|
value={pp.mount_name}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const updated = [...paths];
|
|
|
|
|
updated[i] = { ...updated[i], mount_name: e.target.value };
|
|
|
|
|
setPaths(updated);
|
|
|
|
|
}}
|
|
|
|
|
onBlur={async () => {
|
|
|
|
|
try { await update({ ...project, paths }); } catch (err) {
|
|
|
|
|
console.error("Failed to update paths:", err);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
placeholder="name"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="flex-1 min-w-0 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50 font-mono"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-28 21:18:33 +00:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<button
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
const updated = [...paths, { host_path: "", mount_name: "" }];
|
|
|
|
|
setPaths(updated);
|
|
|
|
|
}}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="text-xs text-[var(--accent)] hover:text-[var(--accent-hover)] disabled:opacity-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
+ Add folder
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-27 04:29:51 +00:00
|
|
|
{/* SSH Key */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">SSH Key Directory</label>
|
|
|
|
|
<div className="flex gap-1">
|
|
|
|
|
<input
|
2026-02-28 20:42:40 +00:00
|
|
|
value={sshKeyPath}
|
|
|
|
|
onChange={(e) => setSshKeyPath(e.target.value)}
|
|
|
|
|
onBlur={handleSshKeyPathBlur}
|
2026-02-27 04:29:51 +00:00
|
|
|
placeholder="~/.ssh"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="flex-1 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleBrowseSSH}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="px-2 py-1 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] disabled:opacity-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
...
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Git Name */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Name</label>
|
|
|
|
|
<input
|
2026-02-28 20:42:40 +00:00
|
|
|
value={gitName}
|
|
|
|
|
onChange={(e) => setGitName(e.target.value)}
|
|
|
|
|
onBlur={handleGitNameBlur}
|
2026-02-27 04:29:51 +00:00
|
|
|
placeholder="Your Name"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Git Email */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Email</label>
|
|
|
|
|
<input
|
2026-02-28 20:42:40 +00:00
|
|
|
value={gitEmail}
|
|
|
|
|
onChange={(e) => setGitEmail(e.target.value)}
|
|
|
|
|
onBlur={handleGitEmailBlur}
|
2026-02-27 04:29:51 +00:00
|
|
|
placeholder="you@example.com"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Git Token (HTTPS) */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git HTTPS Token</label>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
2026-02-28 20:42:40 +00:00
|
|
|
value={gitToken}
|
|
|
|
|
onChange={(e) => setGitToken(e.target.value)}
|
|
|
|
|
onBlur={handleGitTokenBlur}
|
2026-02-27 04:29:51 +00:00
|
|
|
placeholder="ghp_..."
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className="w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Docker access toggle */}
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<label className="text-xs text-[var(--text-secondary)]">Allow container spawning</label>
|
|
|
|
|
<button
|
|
|
|
|
onClick={async () => {
|
2026-02-28 20:42:40 +00:00
|
|
|
try { await update({ ...project, allow_docker_access: !project.allow_docker_access }); } catch (err) {
|
|
|
|
|
console.error("Failed to update Docker access setting:", err);
|
|
|
|
|
}
|
2026-02-27 04:29:51 +00:00
|
|
|
}}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 ${
|
|
|
|
|
project.allow_docker_access
|
|
|
|
|
? "bg-[var(--success)] text-white"
|
|
|
|
|
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{project.allow_docker_access ? "ON" : "OFF"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-02-27 14:29:40 +00:00
|
|
|
|
2026-02-27 18:39:20 -08:00
|
|
|
{/* Environment Variables */}
|
2026-03-01 01:21:33 +00:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<label className="text-xs text-[var(--text-secondary)]">
|
|
|
|
|
Environment Variables{envVars.length > 0 && ` (${envVars.length})`}
|
|
|
|
|
</label>
|
2026-02-27 18:39:20 -08:00
|
|
|
<button
|
2026-03-01 01:21:33 +00:00
|
|
|
onClick={() => setShowEnvVarsModal(true)}
|
|
|
|
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
2026-02-27 18:39:20 -08:00
|
|
|
>
|
2026-03-01 01:21:33 +00:00
|
|
|
Edit
|
2026-02-27 18:39:20 -08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-01 14:36:51 +00:00
|
|
|
{/* Port Mappings */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<label className="text-xs text-[var(--text-secondary)]">
|
|
|
|
|
Port Mappings{portMappings.length > 0 && ` (${portMappings.length})`}
|
|
|
|
|
</label>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowPortMappingsModal(true)}
|
|
|
|
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-27 18:39:20 -08:00
|
|
|
{/* Claude Instructions */}
|
2026-03-01 01:21:33 +00:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<label className="text-xs text-[var(--text-secondary)]">
|
|
|
|
|
Claude Instructions{claudeInstructions ? " (set)" : ""}
|
|
|
|
|
</label>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowClaudeInstructionsModal(true)}
|
|
|
|
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</button>
|
2026-02-27 18:39:20 -08:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-04 08:57:12 -08:00
|
|
|
{/* MCP Servers */}
|
|
|
|
|
{mcpServers.length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-1">MCP Servers</label>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{mcpServers.map((server) => {
|
|
|
|
|
const enabled = project.enabled_mcp_servers.includes(server.id);
|
2026-03-04 10:21:05 -08:00
|
|
|
const isDocker = !!server.docker_image;
|
2026-03-04 08:57:12 -08:00
|
|
|
return (
|
|
|
|
|
<label key={server.id} className="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={enabled}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
onChange={async () => {
|
|
|
|
|
const updated = enabled
|
|
|
|
|
? project.enabled_mcp_servers.filter((id) => id !== server.id)
|
|
|
|
|
: [...project.enabled_mcp_servers, server.id];
|
|
|
|
|
try {
|
|
|
|
|
await update({ ...project, enabled_mcp_servers: updated });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update MCP servers:", err);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="rounded border-[var(--border-color)] disabled:opacity-50"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-xs text-[var(--text-primary)]">{server.name}</span>
|
|
|
|
|
<span className="text-xs text-[var(--text-secondary)]">({server.transport_type})</span>
|
2026-03-04 10:21:05 -08:00
|
|
|
<span className={`text-xs px-1 py-0.5 rounded ${isDocker ? "bg-blue-500/20 text-blue-400" : "bg-[var(--bg-secondary)] text-[var(--text-secondary)]"}`}>
|
|
|
|
|
{isDocker ? "Docker" : "Manual"}
|
|
|
|
|
</span>
|
2026-03-04 08:57:12 -08:00
|
|
|
</label>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
2026-03-04 10:21:05 -08:00
|
|
|
{mcpServers.some((s) => s.docker_image && s.transport_type === "stdio" && project.enabled_mcp_servers.includes(s.id)) && (
|
|
|
|
|
<p className="text-xs text-[var(--text-secondary)] mt-1 opacity-70">
|
|
|
|
|
Docker access will be auto-enabled for stdio+Docker MCP servers.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2026-03-04 08:57:12 -08:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-27 14:29:40 +00:00
|
|
|
{/* Bedrock config */}
|
|
|
|
|
{project.auth_mode === "bedrock" && (() => {
|
|
|
|
|
const bc = project.bedrock_config ?? defaultBedrockConfig;
|
|
|
|
|
const inputCls = "w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50";
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-2 pt-1 border-t border-[var(--border-color)]">
|
|
|
|
|
<label className="block text-xs font-medium text-[var(--text-primary)]">AWS Bedrock</label>
|
|
|
|
|
|
|
|
|
|
{/* Sub-method selector */}
|
|
|
|
|
<div className="flex items-center gap-1 text-xs">
|
|
|
|
|
<span className="text-[var(--text-secondary)] mr-1">Method:</span>
|
|
|
|
|
{(["static_credentials", "profile", "bearer_token"] as BedrockAuthMethod[]).map((m) => (
|
|
|
|
|
<button
|
|
|
|
|
key={m}
|
|
|
|
|
onClick={() => updateBedrockConfig({ auth_method: m })}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={`px-2 py-0.5 rounded transition-colors ${
|
|
|
|
|
bc.auth_method === m
|
|
|
|
|
? "bg-[var(--accent)] text-white"
|
|
|
|
|
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
|
|
|
|
|
} disabled:opacity-50`}
|
|
|
|
|
>
|
|
|
|
|
{m === "static_credentials" ? "Keys" : m === "profile" ? "Profile" : "Token"}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* AWS Region (always shown) */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Region</label>
|
|
|
|
|
<input
|
2026-02-28 20:42:40 +00:00
|
|
|
value={bedrockRegion}
|
|
|
|
|
onChange={(e) => setBedrockRegion(e.target.value)}
|
|
|
|
|
onBlur={handleBedrockRegionBlur}
|
2026-02-27 14:29:40 +00:00
|
|
|
placeholder="us-east-1"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Static credentials fields */}
|
|
|
|
|
{bc.auth_method === "static_credentials" && (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Access Key ID</label>
|
|
|
|
|
<input
|
2026-02-28 20:42:40 +00:00
|
|
|
value={bedrockAccessKeyId}
|
|
|
|
|
onChange={(e) => setBedrockAccessKeyId(e.target.value)}
|
|
|
|
|
onBlur={handleBedrockAccessKeyIdBlur}
|
2026-02-27 14:29:40 +00:00
|
|
|
placeholder="AKIA..."
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Secret Access Key</label>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
2026-02-28 20:42:40 +00:00
|
|
|
value={bedrockSecretKey}
|
|
|
|
|
onChange={(e) => setBedrockSecretKey(e.target.value)}
|
|
|
|
|
onBlur={handleBedrockSecretKeyBlur}
|
2026-02-27 14:29:40 +00:00
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Session Token (optional)</label>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
2026-02-28 20:42:40 +00:00
|
|
|
value={bedrockSessionToken}
|
|
|
|
|
onChange={(e) => setBedrockSessionToken(e.target.value)}
|
|
|
|
|
onBlur={handleBedrockSessionTokenBlur}
|
2026-02-27 14:29:40 +00:00
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Profile field */}
|
|
|
|
|
{bc.auth_method === "profile" && (
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Profile</label>
|
|
|
|
|
<input
|
2026-02-28 20:42:40 +00:00
|
|
|
value={bedrockProfile}
|
|
|
|
|
onChange={(e) => setBedrockProfile(e.target.value)}
|
|
|
|
|
onBlur={handleBedrockProfileBlur}
|
2026-02-27 14:29:40 +00:00
|
|
|
placeholder="default"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Bearer token field */}
|
|
|
|
|
{bc.auth_method === "bearer_token" && (
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Bearer Token</label>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
2026-02-28 20:42:40 +00:00
|
|
|
value={bedrockBearerToken}
|
|
|
|
|
onChange={(e) => setBedrockBearerToken(e.target.value)}
|
|
|
|
|
onBlur={handleBedrockBearerTokenBlur}
|
2026-02-27 14:29:40 +00:00
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Model override */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model ID (optional)</label>
|
|
|
|
|
<input
|
2026-02-28 20:42:40 +00:00
|
|
|
value={bedrockModelId}
|
|
|
|
|
onChange={(e) => setBedrockModelId(e.target.value)}
|
|
|
|
|
onBlur={handleBedrockModelIdBlur}
|
2026-02-27 14:29:40 +00:00
|
|
|
placeholder="anthropic.claude-sonnet-4-20250514-v1:0"
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
className={inputCls}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
2026-02-27 04:29:51 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="text-xs text-[var(--error)] mt-1 ml-4">{error}</div>
|
|
|
|
|
)}
|
2026-03-01 01:21:33 +00:00
|
|
|
|
|
|
|
|
{showEnvVarsModal && (
|
|
|
|
|
<EnvVarsModal
|
|
|
|
|
envVars={envVars}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
onSave={async (vars) => {
|
|
|
|
|
setEnvVars(vars);
|
|
|
|
|
await update({ ...project, custom_env_vars: vars });
|
|
|
|
|
}}
|
|
|
|
|
onClose={() => setShowEnvVarsModal(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-01 14:36:51 +00:00
|
|
|
{showPortMappingsModal && (
|
|
|
|
|
<PortMappingsModal
|
|
|
|
|
portMappings={portMappings}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
onSave={async (mappings) => {
|
|
|
|
|
setPortMappings(mappings);
|
|
|
|
|
await update({ ...project, port_mappings: mappings });
|
|
|
|
|
}}
|
|
|
|
|
onClose={() => setShowPortMappingsModal(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-01 01:21:33 +00:00
|
|
|
{showClaudeInstructionsModal && (
|
|
|
|
|
<ClaudeInstructionsModal
|
|
|
|
|
instructions={claudeInstructions}
|
|
|
|
|
disabled={!isStopped}
|
|
|
|
|
onSave={async (instructions) => {
|
|
|
|
|
setClaudeInstructions(instructions);
|
|
|
|
|
await update({ ...project, claude_instructions: instructions || null });
|
|
|
|
|
}}
|
|
|
|
|
onClose={() => setShowClaudeInstructionsModal(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-03-04 08:22:35 -08:00
|
|
|
|
|
|
|
|
{activeOperation && (
|
|
|
|
|
<ContainerProgressModal
|
|
|
|
|
projectName={project.name}
|
|
|
|
|
operation={activeOperation}
|
|
|
|
|
progressMsg={progressMsg}
|
|
|
|
|
error={error}
|
|
|
|
|
completed={operationCompleted}
|
|
|
|
|
onForceStop={handleForceStop}
|
|
|
|
|
onClose={closeModal}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-02-27 04:29:51 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ActionButton({
|
|
|
|
|
onClick,
|
|
|
|
|
disabled,
|
|
|
|
|
label,
|
|
|
|
|
accent,
|
|
|
|
|
danger,
|
|
|
|
|
}: {
|
|
|
|
|
onClick: (e?: React.MouseEvent) => void;
|
|
|
|
|
disabled: boolean;
|
|
|
|
|
label: string;
|
|
|
|
|
accent?: boolean;
|
|
|
|
|
danger?: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
let color = "text-[var(--text-secondary)] hover:text-[var(--text-primary)]";
|
|
|
|
|
if (accent) color = "text-[var(--accent)] hover:text-[var(--accent-hover)]";
|
|
|
|
|
if (danger) color = "text-[var(--error)] hover:text-[var(--error)]";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); onClick(e); }}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className={`text-xs px-2 py-0.5 rounded transition-colors disabled:opacity-50 ${color} hover:bg-[var(--bg-primary)]`}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-05 06:58:03 -08:00
|
|
|
|