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 ++++++++++++++++++++++++++++++++-- makima/src/db/models.rs | 2 + makima/src/db/repository.rs | 42 ++++++- 3 files changed, 265 insertions(+), 10 deletions(-) (limited to 'makima') 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 */} diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 392d019..e861f1d 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2825,6 +2825,8 @@ pub struct ChainContractDetail { /// Maximum evaluation retry attempts #[sqlx(default)] pub max_evaluation_retries: i32, + /// When the chain contract was created + pub created_at: DateTime, } /// DAG graph structure for visualization diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 9cb653f..7be7bc8 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -5162,7 +5162,8 @@ pub async fn list_chain_contracts( cc.editor_y, cc.evaluation_status, cc.evaluation_retry_count, - cc.max_evaluation_retries + cc.max_evaluation_retries, + cc.created_at FROM chain_contracts cc JOIN contracts c ON c.id = cc.contract_id WHERE cc.chain_id = $1 @@ -6266,6 +6267,45 @@ async fn create_contract_from_definition( .execute(pool) .await?; + // Copy repositories from chain to contract + let chain_repos = list_chain_repositories(pool, chain_id).await.unwrap_or_default(); + for repo in chain_repos { + if let Some(url) = &repo.repository_url { + // Remote repository + if let Err(e) = add_remote_repository(pool, contract.id, &repo.name, url, repo.is_primary).await { + tracing::warn!( + contract_id = %contract.id, + repo_name = %repo.name, + error = %e, + "Failed to copy repository from chain to contract" + ); + } + } else if let Some(path) = &repo.local_path { + // Local repository + if let Err(e) = add_local_repository(pool, contract.id, &repo.name, path, repo.is_primary).await { + tracing::warn!( + contract_id = %contract.id, + repo_name = %repo.name, + error = %e, + "Failed to copy local repository from chain to contract" + ); + } + } + } + + // Activate the contract so it can start + sqlx::query("UPDATE contracts SET status = 'active' WHERE id = $1") + .bind(contract.id) + .execute(pool) + .await?; + + tracing::info!( + contract_id = %contract.id, + contract_name = %def.name, + chain_id = %chain_id, + "Contract created and activated from chain definition" + ); + Ok(contract.id) } -- cgit v1.2.3