From 0a30c9d3a9227660860abcd48ea1e9bd5cc2350c Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 22 Jan 2026 00:46:06 +0000 Subject: 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 * Task completion checkpoint --------- Co-authored-by: Claude Opus 4.5 --- makima/frontend/src/routes/mesh.tsx | 179 +++++++++++++++++++++++++++++++++--- 1 file changed, 167 insertions(+), 12 deletions(-) (limited to 'makima/frontend/src') 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(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([]); + const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); // Track which subtask's output we're viewing (null = parent task) const [viewingSubtaskId, setViewingSubtaskId] = useState(null); const [viewingSubtaskName, setViewingSubtaskName] = useState(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() { /> - {/* Repository selection - only for contract tasks */} + {/* Repository selection - for contract tasks */} {selectedContract && selectedContract.repositories.length > 0 && (
@@ -1006,11 +1063,111 @@ export default function MeshPage() {
)} - {/* Target repo path with DirectoryInput - for standalone tasks or when repo is selected */} - {(newTaskRepoUrl || !selectedContract) && ( + {/* Repository selection - for standalone tasks */} + {!selectedContract && ( +
+ {/* Repository type selector */} +
+ +
+ + +
+
+ + {/* Repository suggestions */} + {showRepoSuggestions && repoSuggestions.length > 0 && ( +
+ +
+ {repoSuggestions.map((suggestion) => ( + + ))} +
+
+ )} + + {/* Repository URL (for remote) */} + {standaloneRepoType === "remote" && ( +
+ + 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]" + /> +

+ The remote repository this task will clone and work on. +

+
+ )} + + {/* Repository path (for local) */} + {standaloneRepoType === "local" && ( +
+ + +

+ The local directory this task will work in. +

+
+ )} +
+ )} + + {/* Target repo path with DirectoryInput - only for contract tasks when repo is selected */} + {selectedContract && newTaskRepoUrl && (

- {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.

)} -- cgit v1.2.3