From f1a50b80f3969d150bd1c31edde0aff05369157e Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Feb 2026 11:10:23 +0000 Subject: Add repository selection to chain creation modal - Update CreateChainModal to include repository input fields - Add repository suggestions from history using getRepositorySuggestions - Support both remote (URL) and local (path) repositories - First repository added becomes primary automatically - Pass repositories to CreateChainRequest Also includes backend changes: - Copy chain repositories to contracts when created from definitions - Add created_at field to ChainContractDetail Co-Authored-By: Claude Opus 4.5 --- makima/frontend/src/routes/chains.tsx | 231 ++++++++++++++++++++++++++++++++-- 1 file changed, 222 insertions(+), 9 deletions(-) (limited to 'makima/frontend') diff --git a/makima/frontend/src/routes/chains.tsx b/makima/frontend/src/routes/chains.tsx index 23484b4..9b33304 100644 --- a/makima/frontend/src/routes/chains.tsx +++ b/makima/frontend/src/routes/chains.tsx @@ -10,7 +10,10 @@ import type { ChainWithContracts, ChainGraphResponse, CreateChainRequest, + AddChainRepositoryRequest, + RepositoryHistoryEntry, } from "../lib/api"; +import { getRepositorySuggestions } from "../lib/api"; export default function ChainsPage() { const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); @@ -92,10 +95,11 @@ function ChainsPageContent() { }, []); const handleCreateSubmit = useCallback( - async (name: string, description: string) => { + async (name: string, description: string, repositories: AddChainRepositoryRequest[]) => { const data: CreateChainRequest = { name: name.trim(), description: description.trim() || undefined, + repositories: repositories.length > 0 ? repositories : undefined, }; try { @@ -203,23 +207,89 @@ function ChainsPageContent() { } interface CreateChainModalProps { - onSubmit: (name: string, description: string) => void; + onSubmit: (name: string, description: string, repositories: AddChainRepositoryRequest[]) => void; onCancel: () => void; } +type RepoMode = "remote" | "local" | null; + function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) { const [name, setName] = useState(""); const [description, setDescription] = useState(""); + const [repositories, setRepositories] = useState([]); + + // Repository input state + const [repoMode, setRepoMode] = useState(null); + const [repoName, setRepoName] = useState(""); + const [repoUrl, setRepoUrl] = useState(""); + const [repoPath, setRepoPath] = useState(""); + + // Suggestions + const [suggestions, setSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); + + // Load suggestions when mode changes + useEffect(() => { + if (repoMode) { + getRepositorySuggestions(repoMode, undefined, 10) + .then((res) => { + setSuggestions(res.entries); + setShowSuggestions(res.entries.length > 0); + }) + .catch(() => { + setSuggestions([]); + setShowSuggestions(false); + }); + } else { + setSuggestions([]); + setShowSuggestions(false); + } + }, [repoMode]); + + const applySuggestion = (suggestion: RepositoryHistoryEntry) => { + setRepoName(suggestion.name); + if (suggestion.repositoryUrl) setRepoUrl(suggestion.repositoryUrl); + if (suggestion.localPath) setRepoPath(suggestion.localPath); + setShowSuggestions(false); + }; + + const handleAddRepo = () => { + if (!repoName.trim()) return; + if (repoMode === "remote" && !repoUrl.trim()) return; + if (repoMode === "local" && !repoPath.trim()) return; + + const newRepo: AddChainRepositoryRequest = { + name: repoName.trim(), + sourceType: repoMode || "remote", + isPrimary: repositories.length === 0, // First one is primary + ...(repoMode === "remote" ? { repositoryUrl: repoUrl.trim() } : { localPath: repoPath.trim() }), + }; + + setRepositories([...repositories, newRepo]); + setRepoMode(null); + setRepoName(""); + setRepoUrl(""); + setRepoPath(""); + }; + + const handleRemoveRepo = (index: number) => { + const newRepos = repositories.filter((_, i) => i !== index); + // If we removed the primary, make the first one primary + if (newRepos.length > 0 && repositories[index]?.isPrimary) { + newRepos[0].isPrimary = true; + } + setRepositories(newRepos); + }; const handleSubmit = () => { if (name.trim()) { - onSubmit(name.trim(), description.trim()); + onSubmit(name.trim(), description.trim(), repositories); } }; return (
-
+

Create Chain

@@ -228,7 +298,7 @@ function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) { {/* Chain name */}
setDescription(e.target.value)} placeholder="Describe what this chain accomplishes..." - rows={3} + rows={2} className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none" />
+ {/* Repositories */} +
+ + + {/* Added repositories */} + {repositories.length > 0 && ( +
+ {repositories.map((repo, index) => ( +
+ + {repo.sourceType === "remote" ? "URL" : "Local"} + + + {repo.name} + + {repo.isPrimary && ( + + primary + + )} + +
+ ))} +
+ )} + + {/* Add repository form */} + {repoMode ? ( +
+
+ + Add {repoMode === "remote" ? "Remote" : "Local"} Repository + + {suggestions.length > 0 && ( + + )} +
+ + {/* Suggestions dropdown */} + {showSuggestions && suggestions.length > 0 && ( +
+ {suggestions.map((s) => ( + + ))} +
+ )} + + setRepoName(e.target.value)} + placeholder="Repository name" + className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" + /> + + {repoMode === "remote" ? ( + setRepoUrl(e.target.value)} + placeholder="https://github.com/owner/repo" + className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" + /> + ) : ( + setRepoPath(e.target.value)} + placeholder="/path/to/repository" + className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]" + /> + )} + +
+ + +
+
+ ) : ( +
+ + +
+ )} + + {repositories.length === 0 && !repoMode && ( +

+ Add repositories that contracts in this chain will work with +

+ )} +
+

- A chain links multiple contracts together in a directed acyclic graph (DAG). - Contracts can depend on each other, and dependent contracts start automatically - when their dependencies complete. + A chain links multiple contracts together in a DAG. Contracts depend on each + other and start automatically when dependencies complete.

{/* Actions */} -- cgit v1.2.3