diff options
| author | soryu <soryu@soryu.co> | 2026-02-07 19:17:49 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-07 19:17:49 +0000 |
| commit | 6b9b62404489fb55a2c52d7b75730be16efbec3e (patch) | |
| tree | 1d750a2b09e518702ff7c63a9799e34ad481340f | |
| parent | 27a91409a623347d148bc8d0f0fa81a955a038bd (diff) | |
| download | soryu-6b9b62404489fb55a2c52d7b75730be16efbec3e.tar.gz soryu-6b9b62404489fb55a2c52d7b75730be16efbec3e.zip | |
Add repo suggestions for directives
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 198 |
1 files changed, 170 insertions, 28 deletions
diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index 939961c..fd3808b 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -3,11 +3,19 @@ import { useParams, useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { DirectiveList } from "../components/directives/DirectiveList"; import { DirectiveDetail } from "../components/directives/DirectiveDetail"; +import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useDirectives } from "../hooks/useDirectives"; import { useAuth } from "../contexts/AuthContext"; +import { + getDaemonDirectories, + getRepositorySuggestions, +} from "../lib/api"; import type { DirectiveWithChains, CreateDirectiveRequest, + RepositorySourceType, + DaemonDirectory, + RepositoryHistoryEntry, } from "../lib/api"; export default function DirectivesPage() { @@ -58,7 +66,52 @@ function DirectivesContent() { const [showCreateForm, setShowCreateForm] = useState(false); const [createTitle, setCreateTitle] = useState(""); const [createGoal, setCreateGoal] = useState(""); - const [createRepoUrl, setCreateRepoUrl] = useState(""); + + // Repository state + const [repoType, setRepoType] = useState<RepositorySourceType>("remote"); + const [repoUrl, setRepoUrl] = useState(""); + const [repoPath, setRepoPath] = useState(""); + const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]); + const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]); + const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); + + // Fetch repository suggestions when modal opens and repo type changes + useEffect(() => { + if (showCreateForm && (repoType === "remote" || repoType === "local")) { + getRepositorySuggestions(repoType, undefined, 10) + .then((res) => { + setRepoSuggestions(res.entries); + setShowRepoSuggestions(res.entries.length > 0); + }) + .catch(() => { + setRepoSuggestions([]); + setShowRepoSuggestions(false); + }); + } else { + setRepoSuggestions([]); + setShowRepoSuggestions(false); + } + }, [showCreateForm, repoType]); + + // Fetch daemon directories when "local" repo type is selected + useEffect(() => { + if (repoType === "local" && showCreateForm) { + getDaemonDirectories() + .then((res) => setSuggestedDirectories(res.directories)) + .catch(() => setSuggestedDirectories([])); + } + }, [repoType, showCreateForm]); + + // Apply a repository suggestion + const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => { + if (suggestion.repositoryUrl) { + setRepoUrl(suggestion.repositoryUrl); + } + if (suggestion.localPath) { + setRepoPath(suggestion.localPath); + } + setShowRepoSuggestions(false); + }, []); // Load directive when ID changes useEffect(() => { @@ -84,6 +137,15 @@ function DirectivesContent() { navigate("/directives"); }, [navigate]); + const resetCreateForm = useCallback(() => { + setShowCreateForm(false); + setCreateTitle(""); + setCreateGoal(""); + setRepoType("remote"); + setRepoUrl(""); + setRepoPath(""); + }, []); + const handleCreate = useCallback(async () => { if (!createTitle.trim() || !createGoal.trim()) return; @@ -91,18 +153,17 @@ function DirectivesContent() { title: createTitle.trim(), goal: createGoal.trim(), }; - if (createRepoUrl.trim()) { - data.repositoryUrl = createRepoUrl.trim(); + if (repoType === "remote" && repoUrl.trim()) { + data.repositoryUrl = repoUrl.trim(); + } else if (repoType === "local" && repoPath.trim()) { + data.localPath = repoPath.trim(); } const result = await saveDirective(data); if (result) { - setShowCreateForm(false); - setCreateTitle(""); - setCreateGoal(""); - setCreateRepoUrl(""); + resetCreateForm(); } - }, [createTitle, createGoal, createRepoUrl, saveDirective]); + }, [createTitle, createGoal, repoType, repoUrl, repoPath, saveDirective, resetCreateForm]); const handleDelete = useCallback( async (directiveId: string) => { @@ -118,7 +179,6 @@ function DirectivesContent() { async (directiveId: string) => { const ok = await startDirective(directiveId); if (ok) { - // Refresh the detail view const updated = await fetchDirective(directiveId); if (updated) { setSelectedDirective(updated); @@ -145,27 +205,30 @@ function DirectivesContent() { </div> )} - {/* Create directive form */} + {/* Create directive modal */} {showCreateForm && ( <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> <div className="w-full max-w-lg max-h-[90vh] overflow-y-auto p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]"> <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> New Directive </h3> - <div className="space-y-3"> + <div className="space-y-4"> + {/* Title */} <div> <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> Title </label> <input type="text" - placeholder="Title" + placeholder="Directive title" value={createTitle} onChange={(e) => setCreateTitle(e.target.value)} className="w-full px-3 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] focus:border-[#75aafc] outline-none" autoFocus /> </div> + + {/* Goal */} <div> <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> Goal @@ -178,26 +241,105 @@ function DirectivesContent() { className="w-full px-3 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] focus:border-[#75aafc] outline-none resize-none" /> </div> - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Repository URL (optional) + + {/* Repository Configuration */} + <div className="border-t border-[rgba(117,170,252,0.2)] pt-4"> + <label className="block font-mono text-xs text-[#75aafc] uppercase mb-3"> + Repository (optional) </label> - <input - type="text" - placeholder="https://github.com/user/repo.git" - value={createRepoUrl} - onChange={(e) => setCreateRepoUrl(e.target.value)} - className="w-full px-3 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] focus:border-[#75aafc] outline-none" - /> + + {/* Repository type selector */} + <div className="flex gap-2 mb-3"> + <button + type="button" + onClick={() => setRepoType("remote")} + className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ + repoType === "remote" + ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" + : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" + }`} + > + Remote + </button> + <button + type="button" + onClick={() => setRepoType("local")} + className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ + repoType === "local" + ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" + : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" + }`} + > + Local + </button> + </div> + + {/* Repository suggestions */} + {showRepoSuggestions && repoSuggestions.length > 0 && ( + <div className="mb-3"> + <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> + Recent Repositories + </label> + <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto"> + {repoSuggestions.map((suggestion) => ( + <button + key={suggestion.id} + type="button" + onClick={() => applyRepoSuggestion(suggestion)} + 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">{suggestion.name}</span> + <span className="text-[10px] text-[#556677] ml-2"> + {suggestion.useCount}× + </span> + </div> + <div className="text-[10px] text-[#556677] truncate"> + {repoType === "local" ? suggestion.localPath : suggestion.repositoryUrl} + </div> + </button> + ))} + </div> + </div> + )} + + {/* Repository URL (for remote) */} + {repoType === "remote" && ( + <div> + <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> + Repository URL + </label> + <input + type="text" + value={repoUrl} + onChange={(e) => setRepoUrl(e.target.value)} + placeholder="https://github.com/user/repo.git" + className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" + /> + </div> + )} + + {/* Repository path (for local) */} + {repoType === "local" && ( + <div> + <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> + Local Path + </label> + <DirectoryInput + value={repoPath} + onChange={setRepoPath} + suggestions={suggestedDirectories} + placeholder="/path/to/repository" + repoUrl={repoUrl || null} + /> + </div> + )} </div> + + {/* Actions */} <div className="flex gap-2 justify-end pt-2"> <button - onClick={() => { - setShowCreateForm(false); - setCreateTitle(""); - setCreateGoal(""); - setCreateRepoUrl(""); - }} + onClick={resetCreateForm} className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" > Cancel |
