+
Add Project
-
-
setName(e.target.value)}
- placeholder="my-project"
- className="w-full px-3 py-2 mb-3 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
- />
-
-
-
+
- {error && (
-
{error}
- )}
+
+
+ setPath(e.target.value)}
+ placeholder="/path/to/project"
+ className="flex-1 px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
+ />
+
+
-
-
-
-
+ {error && (
+
{error}
+ )}
+
+
+
+
+
+
);
diff --git a/app/src/components/projects/ProjectCard.tsx b/app/src/components/projects/ProjectCard.tsx
index 33b0787..ef6c56d 100644
--- a/app/src/components/projects/ProjectCard.tsx
+++ b/app/src/components/projects/ProjectCard.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { open } from "@tauri-apps/plugin-dialog";
import type { Project, AuthMode, BedrockConfig, BedrockAuthMethod } from "../../lib/types";
import { useProjects } from "../../hooks/useProjects";
@@ -10,7 +10,8 @@ interface Props {
}
export default function ProjectCard({ project }: Props) {
- const { selectedProjectId, setSelectedProject } = useAppState();
+ const selectedProjectId = useAppState(s => s.selectedProjectId);
+ const setSelectedProject = useAppState(s => s.setSelectedProject);
const { start, stop, rebuild, remove, update } = useProjects();
const { open: openTerminal } = useTerminal();
const [loading, setLoading] = useState(false);
@@ -19,6 +20,40 @@ export default function ProjectCard({ project }: Props) {
const isSelected = selectedProjectId === project.id;
const isStopped = project.status === "stopped" || project.status === "error";
+ // Local state for text fields (save on blur, not on every keystroke)
+ 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 ?? []);
+
+ // 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(() => {
+ 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 ?? []);
+ 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]);
+
const handleStart = async () => {
setLoading(true);
setError(null);
@@ -79,7 +114,9 @@ export default function ProjectCard({ project }: Props) {
try {
const current = project.bedrock_config ?? defaultBedrockConfig;
await update({ ...project, bedrock_config: { ...current, ...patch } });
- } catch {}
+ } catch (err) {
+ console.error("Failed to update Bedrock config:", err);
+ }
};
const handleBrowseSSH = async () => {
@@ -93,6 +130,118 @@ export default function ProjectCard({ project }: Props) {
}
};
+ // 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 handleClaudeInstructionsBlur = async () => {
+ try {
+ await update({ ...project, claude_instructions: claudeInstructions || null });
+ } catch (err) {
+ console.error("Failed to update Claude instructions:", err);
+ }
+ };
+
+ const handleEnvVarBlur = async () => {
+ try {
+ await update({ ...project, custom_env_vars: envVars });
+ } catch (err) {
+ console.error("Failed to update environment variables:", 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);
+ }
+ };
+
const statusColor = {
stopped: "bg-[var(--text-secondary)]",
starting: "bg-[var(--warning)]",
@@ -208,10 +357,9 @@ export default function ProjectCard({ project }: Props) {
{
- try { await update({ ...project, ssh_key_path: e.target.value || null }); } catch {}
- }}
+ value={sshKeyPath}
+ onChange={(e) => setSshKeyPath(e.target.value)}
+ onBlur={handleSshKeyPathBlur}
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"
@@ -230,10 +378,9 @@ export default function ProjectCard({ project }: Props) {
{
- try { await update({ ...project, git_user_name: e.target.value || null }); } catch {}
- }}
+ value={gitName}
+ onChange={(e) => setGitName(e.target.value)}
+ onBlur={handleGitNameBlur}
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"
@@ -244,10 +391,9 @@ export default function ProjectCard({ project }: Props) {
{
- try { await update({ ...project, git_user_email: e.target.value || null }); } catch {}
- }}
+ value={gitEmail}
+ onChange={(e) => setGitEmail(e.target.value)}
+ onBlur={handleGitEmailBlur}
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"
@@ -259,10 +405,9 @@ export default function ProjectCard({ project }: Props) {
{
- try { await update({ ...project, git_token: e.target.value || null }); } catch {}
- }}
+ value={gitToken}
+ onChange={(e) => setGitToken(e.target.value)}
+ onBlur={handleGitTokenBlur}
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"
@@ -274,7 +419,9 @@ export default function ProjectCard({ project }: Props) {