summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-22 00:46:06 +0000
committerGitHub <noreply@github.com>2026-01-22 00:46:06 +0000
commit0a30c9d3a9227660860abcd48ea1e9bd5cc2350c (patch)
tree49c14a8410a36fbf1fee8d4159379331f9d77005
parenta6f91232285ad2db0ac58a7d0bc196e47da1ca8c (diff)
downloadsoryu-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.json14
-rw-r--r--makima/frontend/src/routes/mesh.tsx179
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>
)}