summaryrefslogtreecommitdiff
path: root/makima
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-05 11:10:23 +0000
committersoryu <soryu@soryu.co>2026-02-05 11:10:23 +0000
commitf1a50b80f3969d150bd1c31edde0aff05369157e (patch)
treeeef4a1e8ba4012d5ee67cd5dd01d3a7380f215ec /makima
parent5205db1f26cff0b59c567915966ed1dd892ab472 (diff)
downloadsoryu-f1a50b80f3969d150bd1c31edde0aff05369157e.tar.gz
soryu-f1a50b80f3969d150bd1c31edde0aff05369157e.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'makima')
-rw-r--r--makima/frontend/src/routes/chains.tsx231
-rw-r--r--makima/src/db/models.rs2
-rw-r--r--makima/src/db/repository.rs42
3 files changed, 265 insertions, 10 deletions
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<AddChainRepositoryRequest[]>([]);
+
+ // Repository input state
+ const [repoMode, setRepoMode] = useState<RepoMode>(null);
+ const [repoName, setRepoName] = useState("");
+ const [repoUrl, setRepoUrl] = useState("");
+ const [repoPath, setRepoPath] = useState("");
+
+ // Suggestions
+ const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]);
+ 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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
- <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]">
+ <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto">
<h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
Create Chain
</h3>
@@ -228,7 +298,7 @@ function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) {
{/* Chain name */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Chain Name
+ Chain Name *
</label>
<input
type="text"
@@ -249,15 +319,158 @@ function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) {
value={description}
onChange={(e) => 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"
/>
</div>
+ {/* Repositories */}
+ <div>
+ <label className="block font-mono text-xs text-[#8b949e] uppercase mb-2">
+ Repositories
+ </label>
+
+ {/* Added repositories */}
+ {repositories.length > 0 && (
+ <div className="space-y-2 mb-3">
+ {repositories.map((repo, index) => (
+ <div
+ key={index}
+ className="flex items-center gap-2 px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)]"
+ >
+ <span className="font-mono text-[10px] text-[#556677] uppercase">
+ {repo.sourceType === "remote" ? "URL" : "Local"}
+ </span>
+ <span className="font-mono text-xs text-[#dbe7ff] flex-1 truncate">
+ {repo.name}
+ </span>
+ {repo.isPrimary && (
+ <span className="font-mono text-[8px] text-[#75aafc] uppercase px-1 border border-[#75aafc]/30">
+ primary
+ </span>
+ )}
+ <button
+ onClick={() => handleRemoveRepo(index)}
+ className="font-mono text-xs text-[#556677] hover:text-red-400"
+ >
+ ✕
+ </button>
+ </div>
+ ))}
+ </div>
+ )}
+
+ {/* Add repository form */}
+ {repoMode ? (
+ <div className="p-3 border border-[rgba(117,170,252,0.3)] space-y-3">
+ <div className="flex items-center justify-between">
+ <span className="font-mono text-[10px] text-[#75aafc] uppercase">
+ Add {repoMode === "remote" ? "Remote" : "Local"} Repository
+ </span>
+ {suggestions.length > 0 && (
+ <button
+ onClick={() => setShowSuggestions(!showSuggestions)}
+ className="font-mono text-[10px] text-[#556677] hover:text-[#9bc3ff]"
+ >
+ {showSuggestions ? "Hide" : `${suggestions.length} suggestions`}
+ </button>
+ )}
+ </div>
+
+ {/* Suggestions dropdown */}
+ {showSuggestions && suggestions.length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-28 overflow-y-auto">
+ {suggestions.map((s) => (
+ <button
+ key={s.id}
+ onClick={() => applySuggestion(s)}
+ className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
+ >
+ <div className="flex items-center justify-between">
+ <span className="text-[#9bc3ff] truncate">{s.name}</span>
+ <span className="text-[10px] text-[#556677]">{s.useCount}×</span>
+ </div>
+ <div className="text-[10px] text-[#556677] truncate">
+ {repoMode === "local" ? s.localPath : s.repositoryUrl}
+ </div>
+ </button>
+ ))}
+ </div>
+ )}
+
+ <input
+ type="text"
+ value={repoName}
+ onChange={(e) => 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" ? (
+ <input
+ type="text"
+ value={repoUrl}
+ onChange={(e) => 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]"
+ />
+ ) : (
+ <input
+ type="text"
+ value={repoPath}
+ onChange={(e) => 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]"
+ />
+ )}
+
+ <div className="flex gap-2">
+ <button
+ onClick={() => setRepoMode(null)}
+ className="px-3 py-1.5 font-mono text-xs text-[#556677] hover:text-[#9bc3ff]"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleAddRepo}
+ disabled={
+ !repoName.trim() ||
+ (repoMode === "remote" && !repoUrl.trim()) ||
+ (repoMode === "local" && !repoPath.trim())
+ }
+ className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] disabled:opacity-50"
+ >
+ Add
+ </button>
+ </div>
+ </div>
+ ) : (
+ <div className="flex gap-2">
+ <button
+ onClick={() => setRepoMode("remote")}
+ className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]"
+ >
+ + Remote
+ </button>
+ <button
+ onClick={() => setRepoMode("local")}
+ className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]"
+ >
+ + Local
+ </button>
+ </div>
+ )}
+
+ {repositories.length === 0 && !repoMode && (
+ <p className="font-mono text-[10px] text-[#556677] mt-2">
+ Add repositories that contracts in this chain will work with
+ </p>
+ )}
+ </div>
+
<p className="font-mono text-xs text-[#8b949e]">
- 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.
</p>
{/* 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<Utc>,
}
/// 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)
}