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 | |
| 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>
| -rw-r--r-- | makima/frontend/package-lock.json | 14 | ||||
| -rw-r--r-- | makima/frontend/src/routes/mesh.tsx | 179 |
2 files changed, 180 insertions, 13 deletions
diff --git a/makima/frontend/package-lock.json b/makima/frontend/package-lock.json index 88297c2..cc842b1 100644 --- a/makima/frontend/package-lock.json +++ b/makima/frontend/package-lock.json @@ -54,6 +54,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -960,6 +961,7 @@ "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.11.0.tgz", "integrity": "sha512-g1ou5Zw3r4mCU0L+EXH4vRtAiyt8qz1JOvL1k+PW4rZ4+71h5nBy/fLgD7cg5BnzQZmjRO1PzCgpF5BIrlKYxQ==", "dev": true, + "peer": true, "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", @@ -1857,6 +1859,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1973,6 +1976,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2793,6 +2797,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -2864,6 +2869,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2872,6 +2878,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2889,6 +2896,7 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -2920,6 +2928,7 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", + "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -2979,7 +2988,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -3116,6 +3126,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3207,6 +3218,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", 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> )} |
