diff options
| author | soryu <soryu@soryu.co> | 2026-01-22 00:46:06 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-22 00:46:06 +0000 |
| commit | 0a30c9d3a9227660860abcd48ea1e9bd5cc2350c (patch) | |
| tree | 49c14a8410a36fbf1fee8d4159379331f9d77005 /makima/frontend/src/routes | |
| parent | a6f91232285ad2db0ac58a7d0bc196e47da1ca8c (diff) | |
| download | soryu-0a30c9d3a9227660860abcd48ea1e9bd5cc2350c.tar.gz soryu-0a30c9d3a9227660860abcd48ea1e9bd5cc2350c.zip | |
Add repository selection with suggestions to standalone tasks (#17)
* Add repository selection with suggestions to standalone task creation
Enhance the standalone task creation modal with repository selection:
- Add Remote/Local repository type selector buttons
- Show recent repository suggestions dropdown (fetched via API)
- Add repository URL input for remote repositories
- Add local path input with DirectoryInput for local repositories
- Auto-fill form fields when clicking a suggestion
- Wire up handleCreateTask to use standalone repo fields
This builds on PR #16's standalone task feature by adding the same
repository suggestion capabilities that exist in contract creation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Task completion checkpoint
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/routes')
| -rw-r--r-- | makima/frontend/src/routes/mesh.tsx | 179 |
1 files changed, 167 insertions, 12 deletions
diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx index 77e9d7e..314be7b 100644 --- a/makima/frontend/src/routes/mesh.tsx +++ b/makima/frontend/src/routes/mesh.tsx @@ -8,8 +8,8 @@ import { UnifiedMeshChatInput } from "../components/mesh/UnifiedMeshChatInput"; import { ContractCompleteQuestion } from "../components/mesh/ContractCompleteQuestion"; import { useTasks } from "../hooks/useTasks"; import { useTaskSubscription, type TaskUpdateEvent, type TaskOutputEvent } from "../hooks/useTaskSubscription"; -import type { TaskWithSubtasks, MeshChatContext, ContractSummary, ContractWithRelations, DaemonDirectory, TaskSummary } from "../lib/api"; -import { startTask as startTaskApi, stopTask as stopTaskApi, getTaskOutput, listContracts, getContract, getDaemonDirectories, continueTask as continueTaskApi, resumeSupervisor, branchTask } from "../lib/api"; +import type { TaskWithSubtasks, MeshChatContext, ContractSummary, ContractWithRelations, DaemonDirectory, TaskSummary, RepositoryHistoryEntry } from "../lib/api"; +import { startTask as startTaskApi, stopTask as stopTaskApi, getTaskOutput, listContracts, getContract, getDaemonDirectories, continueTask as continueTaskApi, resumeSupervisor, branchTask, getRepositorySuggestions } from "../lib/api"; import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useAuth } from "../contexts/AuthContext"; import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; @@ -125,6 +125,12 @@ export default function MeshPage() { const [newTaskName, setNewTaskName] = useState(""); const [newTaskRepoUrl, setNewTaskRepoUrl] = useState<string | null>(null); const [newTaskTargetPath, setNewTaskTargetPath] = useState(""); + // Standalone task repository selection state + const [standaloneRepoType, setStandaloneRepoType] = useState<"remote" | "local">("remote"); + const [standaloneRepoUrl, setStandaloneRepoUrl] = useState(""); + const [standaloneRepoPath, setStandaloneRepoPath] = useState(""); + const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]); + const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); // Track which subtask's output we're viewing (null = parent task) const [viewingSubtaskId, setViewingSubtaskId] = useState<string | null>(null); const [viewingSubtaskName, setViewingSubtaskName] = useState<string | null>(null); @@ -340,6 +346,35 @@ export default function MeshPage() { } }, [taskDetail?.isSupervisor, taskDetail?.contractId, taskDetail?.id]); + // Fetch repository suggestions when standalone task modal is open + useEffect(() => { + if (showContractModal && modalStep === 2 && !selectedContract) { + getRepositorySuggestions(standaloneRepoType, undefined, 10) + .then((res) => { + setRepoSuggestions(res.entries); + setShowRepoSuggestions(res.entries.length > 0); + }) + .catch(() => { + setRepoSuggestions([]); + setShowRepoSuggestions(false); + }); + } else if (!showContractModal) { + setRepoSuggestions([]); + setShowRepoSuggestions(false); + } + }, [showContractModal, modalStep, selectedContract, standaloneRepoType]); + + // Apply a repository suggestion + const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => { + if (suggestion.repositoryUrl) { + setStandaloneRepoUrl(suggestion.repositoryUrl); + } + if (suggestion.localPath) { + setStandaloneRepoPath(suggestion.localPath); + } + setShowRepoSuggestions(false); + }, []); + const handleSelectTask = useCallback( (taskId: string) => { navigate(`/mesh/${taskId}`); @@ -531,6 +566,9 @@ export default function MeshPage() { setNewTaskName("Standalone Task"); setNewTaskRepoUrl(null); setNewTaskTargetPath(""); + setStandaloneRepoType("remote"); + setStandaloneRepoUrl(""); + setStandaloneRepoPath(""); setModalStep(2); }, []); @@ -540,12 +578,26 @@ export default function MeshPage() { setShowContractModal(false); setCreating(true); try { + // For standalone tasks, use the standalone repo URL/path based on type + let repoUrl = newTaskRepoUrl; + let targetPath = newTaskTargetPath; + + if (!selectedContract) { + // Standalone task - use the standalone repo fields + if (standaloneRepoType === "remote" && standaloneRepoUrl) { + repoUrl = standaloneRepoUrl; + } else if (standaloneRepoType === "local" && standaloneRepoPath) { + // For local paths, use targetRepoPath instead + targetPath = standaloneRepoPath; + } + } + const newTask = await saveTask({ contractId: selectedContract?.id, name: newTaskName || (selectedContract ? `Task for ${selectedContract.name}` : "Standalone Task"), plan: "# Plan\n\nDescribe what this task should accomplish...", - repositoryUrl: newTaskRepoUrl || undefined, - targetRepoPath: newTaskTargetPath || undefined, + repositoryUrl: repoUrl || undefined, + targetRepoPath: targetPath || undefined, }); if (newTask) { navigate(`/mesh/${newTask.id}`); @@ -553,7 +605,7 @@ export default function MeshPage() { } finally { setCreating(false); } - }, [creating, saveTask, navigate, selectedContract, newTaskName, newTaskRepoUrl, newTaskTargetPath]); + }, [creating, saveTask, navigate, selectedContract, newTaskName, newTaskRepoUrl, newTaskTargetPath, standaloneRepoType, standaloneRepoUrl, standaloneRepoPath]); // Close modal and reset state const handleCloseModal = useCallback(() => { @@ -563,6 +615,11 @@ export default function MeshPage() { setNewTaskName(""); setNewTaskRepoUrl(null); setNewTaskTargetPath(""); + setStandaloneRepoType("remote"); + setStandaloneRepoUrl(""); + setStandaloneRepoPath(""); + setRepoSuggestions([]); + setShowRepoSuggestions(false); }, []); const handleCreateSubtask = useCallback(async () => { @@ -981,7 +1038,7 @@ export default function MeshPage() { /> </div> - {/* Repository selection - only for contract tasks */} + {/* Repository selection - for contract tasks */} {selectedContract && selectedContract.repositories.length > 0 && ( <div className="space-y-1"> <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Repository</label> @@ -1006,11 +1063,111 @@ export default function MeshPage() { </div> )} - {/* Target repo path with DirectoryInput - for standalone tasks or when repo is selected */} - {(newTaskRepoUrl || !selectedContract) && ( + {/* Repository selection - for standalone tasks */} + {!selectedContract && ( + <div className="space-y-3"> + {/* Repository type selector */} + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Repository Type (optional)</label> + <div className="flex gap-2"> + <button + type="button" + onClick={() => setStandaloneRepoType("remote")} + className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ + standaloneRepoType === "remote" + ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" + : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" + }`} + > + Remote + </button> + <button + type="button" + onClick={() => setStandaloneRepoType("local")} + className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ + standaloneRepoType === "local" + ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" + : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" + }`} + > + Local + </button> + </div> + </div> + + {/* Repository suggestions */} + {showRepoSuggestions && repoSuggestions.length > 0 && ( + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> + 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"> + {standaloneRepoType === "local" ? suggestion.localPath : suggestion.repositoryUrl} + </div> + </button> + ))} + </div> + </div> + )} + + {/* Repository URL (for remote) */} + {standaloneRepoType === "remote" && ( + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> + Repository URL + </label> + <input + type="text" + value={standaloneRepoUrl} + onChange={(e) => setStandaloneRepoUrl(e.target.value)} + placeholder="https://github.com/user/repo.git" + className="w-full bg-[#0a1525] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-3 py-2 outline-none focus:border-[#3f6fb3]" + /> + <p className="text-[10px] font-mono text-[#556677]"> + The remote repository this task will clone and work on. + </p> + </div> + )} + + {/* Repository path (for local) */} + {standaloneRepoType === "local" && ( + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> + Local Path + </label> + <DirectoryInput + value={standaloneRepoPath} + onChange={setStandaloneRepoPath} + suggestions={daemonDirectories} + placeholder="/path/to/your/local/repo" + /> + <p className="text-[10px] font-mono text-[#556677]"> + The local directory this task will work in. + </p> + </div> + )} + </div> + )} + + {/* Target repo path with DirectoryInput - only for contract tasks when repo is selected */} + {selectedContract && newTaskRepoUrl && ( <div className="space-y-1"> <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> - {selectedContract ? "Target Repository Path" : "Working Directory (optional)"} + Target Repository Path </label> <DirectoryInput value={newTaskTargetPath} @@ -1020,9 +1177,7 @@ export default function MeshPage() { repoUrl={newTaskRepoUrl || undefined} /> <p className="text-[10px] font-mono text-[#556677]"> - {selectedContract - ? "Path where the task will push/merge changes. Leave empty to configure later." - : "Directory for the task to work in. Leave empty to configure later."} + Path where the task will push/merge changes. Leave empty to configure later. </p> </div> )} |
