Fix frontend UX: debounce saves, Zustand selectors, init race, dialog
- Debounce project config saves: use local state + save-on-blur instead of firing IPC requests on every keystroke in text inputs - Add Zustand selectors to all store consumers to prevent full-store re-renders on any state change - Fix initialization race: chain checkImage after checkDocker resolves - Fix DockerSettings setTimeout race: await checkImage after save - Add console.error logging to all 11 empty catch blocks in ProjectCard - Add keyboard support to AddProjectDialog: Escape to close, click-outside-to-close, form submit on Enter, auto-focus Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import Sidebar from "./components/layout/Sidebar";
|
import Sidebar from "./components/layout/Sidebar";
|
||||||
import TopBar from "./components/layout/TopBar";
|
import TopBar from "./components/layout/TopBar";
|
||||||
import StatusBar from "./components/layout/StatusBar";
|
import StatusBar from "./components/layout/StatusBar";
|
||||||
@@ -12,13 +13,16 @@ export default function App() {
|
|||||||
const { checkDocker, checkImage } = useDocker();
|
const { checkDocker, checkImage } = useDocker();
|
||||||
const { checkApiKey, loadSettings } = useSettings();
|
const { checkApiKey, loadSettings } = useSettings();
|
||||||
const { refresh } = useProjects();
|
const { refresh } = useProjects();
|
||||||
const { sessions, activeSessionId } = useAppState();
|
const { sessions, activeSessionId } = useAppState(
|
||||||
|
useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId }))
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize on mount
|
// Initialize on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
checkDocker();
|
checkDocker().then((available) => {
|
||||||
checkImage();
|
if (available) checkImage();
|
||||||
|
});
|
||||||
checkApiKey();
|
checkApiKey();
|
||||||
refresh();
|
refresh();
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useAppState } from "../../store/appState";
|
import { useAppState } from "../../store/appState";
|
||||||
import ProjectList from "../projects/ProjectList";
|
import ProjectList from "../projects/ProjectList";
|
||||||
import SettingsPanel from "../settings/SettingsPanel";
|
import SettingsPanel from "../settings/SettingsPanel";
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const { sidebarView, setSidebarView } = useAppState();
|
const { sidebarView, setSidebarView } = useAppState(
|
||||||
|
useShallow(s => ({ sidebarView: s.sidebarView, setSidebarView: s.setSidebarView }))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<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">
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useAppState } from "../../store/appState";
|
import { useAppState } from "../../store/appState";
|
||||||
|
|
||||||
export default function StatusBar() {
|
export default function StatusBar() {
|
||||||
const { projects, sessions } = useAppState();
|
const { projects, sessions } = useAppState(
|
||||||
|
useShallow(s => ({ projects: s.projects, sessions: s.sessions }))
|
||||||
|
);
|
||||||
const running = projects.filter((p) => p.status === "running").length;
|
const running = projects.filter((p) => p.status === "running").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import TerminalTabs from "../terminal/TerminalTabs";
|
import TerminalTabs from "../terminal/TerminalTabs";
|
||||||
import { useAppState } from "../../store/appState";
|
import { useAppState } from "../../store/appState";
|
||||||
|
|
||||||
export default function TopBar() {
|
export default function TopBar() {
|
||||||
const { dockerAvailable, imageExists } = useAppState();
|
const { dockerAvailable, imageExists } = useAppState(
|
||||||
|
useShallow(s => ({ dockerAvailable: s.dockerAvailable, imageExists: s.imageExists }))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center h-10 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg overflow-hidden">
|
<div className="flex items-center h-10 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg overflow-hidden">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
|
|
||||||
@@ -12,6 +12,34 @@ export default function AddProjectDialog({ onClose }: Props) {
|
|||||||
const [path, setPath] = useState("");
|
const [path, setPath] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Auto-focus the first input when the dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
nameInputRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close on Escape key
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
// Close on click outside (click on overlay backdrop)
|
||||||
|
const handleOverlayClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target === overlayRef.current) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
const handleBrowse = async () => {
|
const handleBrowse = async () => {
|
||||||
const selected = await open({ directory: true, multiple: false });
|
const selected = await open({ directory: true, multiple: false });
|
||||||
@@ -24,7 +52,8 @@ export default function AddProjectDialog({ onClose }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async (e?: React.FormEvent) => {
|
||||||
|
if (e) e.preventDefault();
|
||||||
if (!name.trim() || !path.trim()) {
|
if (!name.trim() || !path.trim()) {
|
||||||
setError("Name and path are required");
|
setError("Name and path are required");
|
||||||
return;
|
return;
|
||||||
@@ -34,65 +63,74 @@ export default function AddProjectDialog({ onClose }: Props) {
|
|||||||
try {
|
try {
|
||||||
await add(name.trim(), path.trim());
|
await add(name.trim(), path.trim());
|
||||||
onClose();
|
onClose();
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
setError(String(e));
|
setError(String(err));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
<div
|
||||||
|
ref={overlayRef}
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-6 w-96 shadow-xl">
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-6 w-96 shadow-xl">
|
||||||
<h2 className="text-lg font-semibold mb-4">Add Project</h2>
|
<h2 className="text-lg font-semibold mb-4">Add Project</h2>
|
||||||
|
|
||||||
<label className="block text-sm text-[var(--text-secondary)] mb-1">
|
<form onSubmit={handleSubmit}>
|
||||||
Project Name
|
<label className="block text-sm text-[var(--text-secondary)] mb-1">
|
||||||
</label>
|
Project Name
|
||||||
<input
|
</label>
|
||||||
value={name}
|
|
||||||
onChange={(e) => 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)]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block text-sm text-[var(--text-secondary)] mb-1">
|
|
||||||
Project Path
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2 mb-4">
|
|
||||||
<input
|
<input
|
||||||
value={path}
|
ref={nameInputRef}
|
||||||
onChange={(e) => setPath(e.target.value)}
|
value={name}
|
||||||
placeholder="/path/to/project"
|
onChange={(e) => setName(e.target.value)}
|
||||||
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)]"
|
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)]"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
onClick={handleBrowse}
|
|
||||||
className="px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded text-sm hover:bg-[var(--border-color)] transition-colors"
|
|
||||||
>
|
|
||||||
Browse
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
<label className="block text-sm text-[var(--text-secondary)] mb-1">
|
||||||
<div className="text-xs text-[var(--error)] mb-3">{error}</div>
|
Project Path
|
||||||
)}
|
</label>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<input
|
||||||
|
value={path}
|
||||||
|
onChange={(e) => 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)]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBrowse}
|
||||||
|
className="px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded text-sm hover:bg-[var(--border-color)] transition-colors"
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
{error && (
|
||||||
<button
|
<div className="text-xs text-[var(--error)] mb-3">{error}</div>
|
||||||
onClick={onClose}
|
)}
|
||||||
className="px-4 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
|
||||||
>
|
<div className="flex justify-end gap-2">
|
||||||
Cancel
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
onClick={onClose}
|
||||||
onClick={handleSubmit}
|
className="px-4 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
disabled={loading}
|
>
|
||||||
className="px-4 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
|
Cancel
|
||||||
>
|
</button>
|
||||||
{loading ? "Adding..." : "Add Project"}
|
<button
|
||||||
</button>
|
type="submit"
|
||||||
</div>
|
disabled={loading}
|
||||||
|
className="px-4 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{loading ? "Adding..." : "Add Project"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import type { Project, AuthMode, BedrockConfig, BedrockAuthMethod } from "../../lib/types";
|
import type { Project, AuthMode, BedrockConfig, BedrockAuthMethod } from "../../lib/types";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
@@ -10,7 +10,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectCard({ project }: 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 { start, stop, rebuild, remove, update } = useProjects();
|
||||||
const { open: openTerminal } = useTerminal();
|
const { open: openTerminal } = useTerminal();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -19,6 +20,40 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
const isSelected = selectedProjectId === project.id;
|
const isSelected = selectedProjectId === project.id;
|
||||||
const isStopped = project.status === "stopped" || project.status === "error";
|
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 () => {
|
const handleStart = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -79,7 +114,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
try {
|
try {
|
||||||
const current = project.bedrock_config ?? defaultBedrockConfig;
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
||||||
await update({ ...project, bedrock_config: { ...current, ...patch } });
|
await update({ ...project, bedrock_config: { ...current, ...patch } });
|
||||||
} catch {}
|
} catch (err) {
|
||||||
|
console.error("Failed to update Bedrock config:", err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrowseSSH = async () => {
|
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 = {
|
const statusColor = {
|
||||||
stopped: "bg-[var(--text-secondary)]",
|
stopped: "bg-[var(--text-secondary)]",
|
||||||
starting: "bg-[var(--warning)]",
|
starting: "bg-[var(--warning)]",
|
||||||
@@ -208,10 +357,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">SSH Key Directory</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">SSH Key Directory</label>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<input
|
<input
|
||||||
value={project.ssh_key_path ?? ""}
|
value={sshKeyPath}
|
||||||
onChange={async (e) => {
|
onChange={(e) => setSshKeyPath(e.target.value)}
|
||||||
try { await update({ ...project, ssh_key_path: e.target.value || null }); } catch {}
|
onBlur={handleSshKeyPathBlur}
|
||||||
}}
|
|
||||||
placeholder="~/.ssh"
|
placeholder="~/.ssh"
|
||||||
disabled={!isStopped}
|
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"
|
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) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Name</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Name</label>
|
||||||
<input
|
<input
|
||||||
value={project.git_user_name ?? ""}
|
value={gitName}
|
||||||
onChange={async (e) => {
|
onChange={(e) => setGitName(e.target.value)}
|
||||||
try { await update({ ...project, git_user_name: e.target.value || null }); } catch {}
|
onBlur={handleGitNameBlur}
|
||||||
}}
|
|
||||||
placeholder="Your Name"
|
placeholder="Your Name"
|
||||||
disabled={!isStopped}
|
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"
|
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) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Email</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git Email</label>
|
||||||
<input
|
<input
|
||||||
value={project.git_user_email ?? ""}
|
value={gitEmail}
|
||||||
onChange={async (e) => {
|
onChange={(e) => setGitEmail(e.target.value)}
|
||||||
try { await update({ ...project, git_user_email: e.target.value || null }); } catch {}
|
onBlur={handleGitEmailBlur}
|
||||||
}}
|
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
disabled={!isStopped}
|
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"
|
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) {
|
|||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git HTTPS Token</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Git HTTPS Token</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={project.git_token ?? ""}
|
value={gitToken}
|
||||||
onChange={async (e) => {
|
onChange={(e) => setGitToken(e.target.value)}
|
||||||
try { await update({ ...project, git_token: e.target.value || null }); } catch {}
|
onBlur={handleGitTokenBlur}
|
||||||
}}
|
|
||||||
placeholder="ghp_..."
|
placeholder="ghp_..."
|
||||||
disabled={!isStopped}
|
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"
|
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) {
|
|||||||
<label className="text-xs text-[var(--text-secondary)]">Allow container spawning</label>
|
<label className="text-xs text-[var(--text-secondary)]">Allow container spawning</label>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try { await update({ ...project, allow_docker_access: !project.allow_docker_access }); } catch {}
|
try { await update({ ...project, allow_docker_access: !project.allow_docker_access }); } catch (err) {
|
||||||
|
console.error("Failed to update Docker access setting:", err);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 ${
|
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 ${
|
||||||
@@ -290,34 +437,39 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Environment Variables</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Environment Variables</label>
|
||||||
{(project.custom_env_vars ?? []).map((ev, i) => (
|
{envVars.map((ev, i) => (
|
||||||
<div key={i} className="flex gap-1 mb-1">
|
<div key={i} className="flex gap-1 mb-1">
|
||||||
<input
|
<input
|
||||||
value={ev.key}
|
value={ev.key}
|
||||||
onChange={async (e) => {
|
onChange={(e) => {
|
||||||
const vars = [...(project.custom_env_vars ?? [])];
|
const vars = [...envVars];
|
||||||
vars[i] = { ...vars[i], key: e.target.value };
|
vars[i] = { ...vars[i], key: e.target.value };
|
||||||
try { await update({ ...project, custom_env_vars: vars }); } catch {}
|
setEnvVars(vars);
|
||||||
}}
|
}}
|
||||||
|
onBlur={handleEnvVarBlur}
|
||||||
placeholder="KEY"
|
placeholder="KEY"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className="w-1/3 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"
|
className="w-1/3 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"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
value={ev.value}
|
value={ev.value}
|
||||||
onChange={async (e) => {
|
onChange={(e) => {
|
||||||
const vars = [...(project.custom_env_vars ?? [])];
|
const vars = [...envVars];
|
||||||
vars[i] = { ...vars[i], value: e.target.value };
|
vars[i] = { ...vars[i], value: e.target.value };
|
||||||
try { await update({ ...project, custom_env_vars: vars }); } catch {}
|
setEnvVars(vars);
|
||||||
}}
|
}}
|
||||||
|
onBlur={handleEnvVarBlur}
|
||||||
placeholder="value"
|
placeholder="value"
|
||||||
disabled={!isStopped}
|
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 font-mono"
|
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 font-mono"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const vars = (project.custom_env_vars ?? []).filter((_, j) => j !== i);
|
const vars = envVars.filter((_, j) => j !== i);
|
||||||
try { await update({ ...project, custom_env_vars: vars }); } catch {}
|
setEnvVars(vars);
|
||||||
|
try { await update({ ...project, custom_env_vars: vars }); } catch (err) {
|
||||||
|
console.error("Failed to remove environment variable:", err);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className="px-1.5 py-1 text-xs text-[var(--error)] hover:bg-[var(--bg-primary)] rounded disabled:opacity-50 transition-colors"
|
className="px-1.5 py-1 text-xs text-[var(--error)] hover:bg-[var(--bg-primary)] rounded disabled:opacity-50 transition-colors"
|
||||||
@@ -328,8 +480,11 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const vars = [...(project.custom_env_vars ?? []), { key: "", value: "" }];
|
const vars = [...envVars, { key: "", value: "" }];
|
||||||
try { await update({ ...project, custom_env_vars: vars }); } catch {}
|
setEnvVars(vars);
|
||||||
|
try { await update({ ...project, custom_env_vars: vars }); } catch (err) {
|
||||||
|
console.error("Failed to add environment variable:", err);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className="text-xs text-[var(--accent)] hover:text-[var(--accent-hover)] disabled:opacity-50 transition-colors"
|
className="text-xs text-[var(--accent)] hover:text-[var(--accent-hover)] disabled:opacity-50 transition-colors"
|
||||||
@@ -342,10 +497,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Claude Instructions</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Claude Instructions</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={project.claude_instructions ?? ""}
|
value={claudeInstructions}
|
||||||
onChange={async (e) => {
|
onChange={(e) => setClaudeInstructions(e.target.value)}
|
||||||
try { await update({ ...project, claude_instructions: e.target.value || null }); } catch {}
|
onBlur={handleClaudeInstructionsBlur}
|
||||||
}}
|
|
||||||
placeholder="Per-project instructions for Claude Code (written to ~/.claude/CLAUDE.md in container)"
|
placeholder="Per-project instructions for Claude Code (written to ~/.claude/CLAUDE.md in container)"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -384,8 +538,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Region</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Region</label>
|
||||||
<input
|
<input
|
||||||
value={bc.aws_region}
|
value={bedrockRegion}
|
||||||
onChange={(e) => updateBedrockConfig({ aws_region: e.target.value })}
|
onChange={(e) => setBedrockRegion(e.target.value)}
|
||||||
|
onBlur={handleBedrockRegionBlur}
|
||||||
placeholder="us-east-1"
|
placeholder="us-east-1"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
@@ -398,8 +553,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Access Key ID</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Access Key ID</label>
|
||||||
<input
|
<input
|
||||||
value={bc.aws_access_key_id ?? ""}
|
value={bedrockAccessKeyId}
|
||||||
onChange={(e) => updateBedrockConfig({ aws_access_key_id: e.target.value || null })}
|
onChange={(e) => setBedrockAccessKeyId(e.target.value)}
|
||||||
|
onBlur={handleBedrockAccessKeyIdBlur}
|
||||||
placeholder="AKIA..."
|
placeholder="AKIA..."
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
@@ -409,8 +565,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Secret Access Key</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Secret Access Key</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={bc.aws_secret_access_key ?? ""}
|
value={bedrockSecretKey}
|
||||||
onChange={(e) => updateBedrockConfig({ aws_secret_access_key: e.target.value || null })}
|
onChange={(e) => setBedrockSecretKey(e.target.value)}
|
||||||
|
onBlur={handleBedrockSecretKeyBlur}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
@@ -419,8 +576,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Session Token (optional)</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Session Token (optional)</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={bc.aws_session_token ?? ""}
|
value={bedrockSessionToken}
|
||||||
onChange={(e) => updateBedrockConfig({ aws_session_token: e.target.value || null })}
|
onChange={(e) => setBedrockSessionToken(e.target.value)}
|
||||||
|
onBlur={handleBedrockSessionTokenBlur}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
@@ -433,8 +591,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Profile</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">AWS Profile</label>
|
||||||
<input
|
<input
|
||||||
value={bc.aws_profile ?? ""}
|
value={bedrockProfile}
|
||||||
onChange={(e) => updateBedrockConfig({ aws_profile: e.target.value || null })}
|
onChange={(e) => setBedrockProfile(e.target.value)}
|
||||||
|
onBlur={handleBedrockProfileBlur}
|
||||||
placeholder="default"
|
placeholder="default"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
@@ -448,8 +607,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Bearer Token</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Bearer Token</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={bc.aws_bearer_token ?? ""}
|
value={bedrockBearerToken}
|
||||||
onChange={(e) => updateBedrockConfig({ aws_bearer_token: e.target.value || null })}
|
onChange={(e) => setBedrockBearerToken(e.target.value)}
|
||||||
|
onBlur={handleBedrockBearerTokenBlur}
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
@@ -460,8 +620,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model ID (optional)</label>
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model ID (optional)</label>
|
||||||
<input
|
<input
|
||||||
value={bc.model_id ?? ""}
|
value={bedrockModelId}
|
||||||
onChange={(e) => updateBedrockConfig({ model_id: e.target.value || null })}
|
onChange={(e) => setBedrockModelId(e.target.value)}
|
||||||
|
onBlur={handleBedrockModelIdBlur}
|
||||||
placeholder="anthropic.claude-sonnet-4-20250514-v1:0"
|
placeholder="anthropic.claude-sonnet-4-20250514-v1:0"
|
||||||
disabled={!isStopped}
|
disabled={!isStopped}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ export default function DockerSettings() {
|
|||||||
const handleSourceChange = async (source: ImageSource) => {
|
const handleSourceChange = async (source: ImageSource) => {
|
||||||
if (!appSettings) return;
|
if (!appSettings) return;
|
||||||
await saveSettings({ ...appSettings, image_source: source });
|
await saveSettings({ ...appSettings, image_source: source });
|
||||||
// Re-check image existence after changing source
|
await checkImage();
|
||||||
setTimeout(() => checkImage(), 100);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCustomChange = async (value: string) => {
|
const handleCustomChange = async (value: string) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
import ApiKeyInput from "./ApiKeyInput";
|
import ApiKeyInput from "./ApiKeyInput";
|
||||||
import DockerSettings from "./DockerSettings";
|
import DockerSettings from "./DockerSettings";
|
||||||
import AwsSettings from "./AwsSettings";
|
import AwsSettings from "./AwsSettings";
|
||||||
@@ -5,6 +6,17 @@ import { useSettings } from "../../hooks/useSettings";
|
|||||||
|
|
||||||
export default function SettingsPanel() {
|
export default function SettingsPanel() {
|
||||||
const { appSettings, saveSettings } = useSettings();
|
const { appSettings, saveSettings } = useSettings();
|
||||||
|
const [globalInstructions, setGlobalInstructions] = useState(appSettings?.global_claude_instructions ?? "");
|
||||||
|
|
||||||
|
// Sync local state when appSettings change
|
||||||
|
useEffect(() => {
|
||||||
|
setGlobalInstructions(appSettings?.global_claude_instructions ?? "");
|
||||||
|
}, [appSettings?.global_claude_instructions]);
|
||||||
|
|
||||||
|
const handleInstructionsBlur = async () => {
|
||||||
|
if (!appSettings) return;
|
||||||
|
await saveSettings({ ...appSettings, global_claude_instructions: globalInstructions || null });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-6">
|
||||||
@@ -20,11 +32,9 @@ export default function SettingsPanel() {
|
|||||||
Global instructions applied to all projects (written to ~/.claude/CLAUDE.md in containers)
|
Global instructions applied to all projects (written to ~/.claude/CLAUDE.md in containers)
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
value={appSettings?.global_claude_instructions ?? ""}
|
value={globalInstructions}
|
||||||
onChange={async (e) => {
|
onChange={(e) => setGlobalInstructions(e.target.value)}
|
||||||
if (!appSettings) return;
|
onBlur={handleInstructionsBlur}
|
||||||
await saveSettings({ ...appSettings, global_claude_instructions: e.target.value || null });
|
|
||||||
}}
|
|
||||||
placeholder="Instructions for Claude Code in all project containers..."
|
placeholder="Instructions for Claude Code in all project containers..."
|
||||||
rows={4}
|
rows={4}
|
||||||
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)] resize-y font-mono"
|
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)] resize-y font-mono"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useAppState } from "../store/appState";
|
import { useAppState } from "../store/appState";
|
||||||
import * as commands from "../lib/tauri-commands";
|
import * as commands from "../lib/tauri-commands";
|
||||||
@@ -9,7 +10,14 @@ export function useDocker() {
|
|||||||
setDockerAvailable,
|
setDockerAvailable,
|
||||||
imageExists,
|
imageExists,
|
||||||
setImageExists,
|
setImageExists,
|
||||||
} = useAppState();
|
} = useAppState(
|
||||||
|
useShallow(s => ({
|
||||||
|
dockerAvailable: s.dockerAvailable,
|
||||||
|
setDockerAvailable: s.setDockerAvailable,
|
||||||
|
imageExists: s.imageExists,
|
||||||
|
setImageExists: s.setImageExists,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const checkDocker = useCallback(async () => {
|
const checkDocker = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useAppState } from "../store/appState";
|
import { useAppState } from "../store/appState";
|
||||||
import * as commands from "../lib/tauri-commands";
|
import * as commands from "../lib/tauri-commands";
|
||||||
|
|
||||||
@@ -10,7 +11,16 @@ export function useProjects() {
|
|||||||
setSelectedProject,
|
setSelectedProject,
|
||||||
updateProjectInList,
|
updateProjectInList,
|
||||||
removeProjectFromList,
|
removeProjectFromList,
|
||||||
} = useAppState();
|
} = useAppState(
|
||||||
|
useShallow(s => ({
|
||||||
|
projects: s.projects,
|
||||||
|
selectedProjectId: s.selectedProjectId,
|
||||||
|
setProjects: s.setProjects,
|
||||||
|
setSelectedProject: s.setSelectedProject,
|
||||||
|
updateProjectInList: s.updateProjectInList,
|
||||||
|
removeProjectFromList: s.removeProjectFromList,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const selectedProject = projects.find((p) => p.id === selectedProjectId) ?? null;
|
const selectedProject = projects.find((p) => p.id === selectedProjectId) ?? null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useAppState } from "../store/appState";
|
import { useAppState } from "../store/appState";
|
||||||
import * as commands from "../lib/tauri-commands";
|
import * as commands from "../lib/tauri-commands";
|
||||||
import type { AppSettings } from "../lib/types";
|
import type { AppSettings } from "../lib/types";
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
const { hasKey, setHasKey, appSettings, setAppSettings } = useAppState();
|
const { hasKey, setHasKey, appSettings, setAppSettings } = useAppState(
|
||||||
|
useShallow(s => ({
|
||||||
|
hasKey: s.hasKey,
|
||||||
|
setHasKey: s.setHasKey,
|
||||||
|
appSettings: s.appSettings,
|
||||||
|
setAppSettings: s.setAppSettings,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const checkApiKey = useCallback(async () => {
|
const checkApiKey = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useAppState } from "../store/appState";
|
import { useAppState } from "../store/appState";
|
||||||
import * as commands from "../lib/tauri-commands";
|
import * as commands from "../lib/tauri-commands";
|
||||||
|
|
||||||
export function useTerminal() {
|
export function useTerminal() {
|
||||||
const { sessions, activeSessionId, addSession, removeSession, setActiveSession } =
|
const { sessions, activeSessionId, addSession, removeSession, setActiveSession } =
|
||||||
useAppState();
|
useAppState(
|
||||||
|
useShallow(s => ({
|
||||||
|
sessions: s.sessions,
|
||||||
|
activeSessionId: s.activeSessionId,
|
||||||
|
addSession: s.addSession,
|
||||||
|
removeSession: s.removeSession,
|
||||||
|
setActiveSession: s.setActiveSession,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const open = useCallback(
|
const open = useCallback(
|
||||||
async (projectId: string, projectName: string) => {
|
async (projectId: string, projectName: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user