summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes/mesh.tsx
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 /makima/frontend/src/routes/mesh.tsx
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>
Diffstat (limited to 'makima/frontend/src/routes/mesh.tsx')
-rw-r--r--makima/frontend/src/routes/mesh.tsx179
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>
)}