diff options
Diffstat (limited to 'makima/frontend/src/routes/mesh.tsx')
| -rw-r--r-- | makima/frontend/src/routes/mesh.tsx | 458 |
1 files changed, 352 insertions, 106 deletions
diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx index 453bdff..b53ec20 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, getRepositorySuggestions, continueTask as continueTaskApi, resumeSupervisor, branchTask } from "../lib/api"; import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useAuth } from "../contexts/AuthContext"; import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; @@ -125,6 +125,13 @@ export default function MeshPage() { const [newTaskName, setNewTaskName] = useState(""); const [newTaskRepoUrl, setNewTaskRepoUrl] = useState<string | null>(null); const [newTaskTargetPath, setNewTaskTargetPath] = useState(""); + // Standalone task mode state + const [isStandalone, setIsStandalone] = useState(false); + 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); @@ -496,6 +503,13 @@ export default function MeshPage() { setNewTaskName(""); setNewTaskRepoUrl(null); setNewTaskTargetPath(""); + // Reset standalone state + setIsStandalone(false); + setStandaloneRepoType("remote"); + setStandaloneRepoUrl(""); + setStandaloneRepoPath(""); + setRepoSuggestions([]); + setShowRepoSuggestions(false); setShowContractModal(true); } catch (e) { console.error("Failed to load contracts:", e); @@ -525,18 +539,78 @@ export default function MeshPage() { } }, []); + // Handle standalone task mode selection + const handleSelectStandalone = useCallback(async () => { + setIsStandalone(true); + setNewTaskName("Standalone Task"); + setModalStep(2); + // Fetch initial repository suggestions for remote type + try { + const res = await getRepositorySuggestions("remote", undefined, 10); + setRepoSuggestions(res.entries); + setShowRepoSuggestions(res.entries.length > 0); + } catch { + setRepoSuggestions([]); + setShowRepoSuggestions(false); + } + }, []); + + // Handle standalone repo type change and fetch suggestions + const handleStandaloneRepoTypeChange = useCallback(async (type: "remote" | "local") => { + setStandaloneRepoType(type); + setStandaloneRepoUrl(""); + setStandaloneRepoPath(""); + try { + const res = await getRepositorySuggestions(type, undefined, 10); + setRepoSuggestions(res.entries); + setShowRepoSuggestions(res.entries.length > 0); + } catch { + setRepoSuggestions([]); + setShowRepoSuggestions(false); + } + }, []); + + // Apply a repository suggestion + const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => { + if (suggestion.repositoryUrl) { + setStandaloneRepoUrl(suggestion.repositoryUrl); + } + if (suggestion.localPath) { + setStandaloneRepoPath(suggestion.localPath); + } + setShowRepoSuggestions(false); + }, []); + // Create task with configured options const handleCreateTask = useCallback(async () => { - if (creating || !selectedContract) return; + if (creating) return; + // For contract-based tasks, require a contract + if (!isStandalone && !selectedContract) return; + setShowContractModal(false); setCreating(true); try { + let repositoryUrl: string | undefined; + let targetPath: string | undefined; + + if (isStandalone) { + // Standalone task - use standalone repo configuration + repositoryUrl = standaloneRepoType === "remote" + ? (standaloneRepoUrl || undefined) + : (standaloneRepoPath || undefined); + targetPath = newTaskTargetPath || undefined; + } else { + // Contract-based task + repositoryUrl = newTaskRepoUrl || undefined; + targetPath = newTaskTargetPath || undefined; + } + const newTask = await saveTask({ - contractId: selectedContract.id, - name: newTaskName || `Task for ${selectedContract.name}`, + contractId: isStandalone ? undefined : selectedContract!.id, + name: newTaskName || (isStandalone ? "Standalone Task" : `Task for ${selectedContract!.name}`), plan: "# Plan\n\nDescribe what this task should accomplish...", - repositoryUrl: newTaskRepoUrl || undefined, - targetRepoPath: newTaskTargetPath || undefined, + repositoryUrl, + targetRepoPath: targetPath, }); if (newTask) { navigate(`/mesh/${newTask.id}`); @@ -544,7 +618,7 @@ export default function MeshPage() { } finally { setCreating(false); } - }, [creating, saveTask, navigate, selectedContract, newTaskName, newTaskRepoUrl, newTaskTargetPath]); + }, [creating, saveTask, navigate, isStandalone, selectedContract, newTaskName, newTaskRepoUrl, newTaskTargetPath, standaloneRepoType, standaloneRepoUrl, standaloneRepoPath]); // Close modal and reset state const handleCloseModal = useCallback(() => { @@ -554,6 +628,13 @@ export default function MeshPage() { setNewTaskName(""); setNewTaskRepoUrl(null); setNewTaskTargetPath(""); + // Reset standalone state + setIsStandalone(false); + setStandaloneRepoType("remote"); + setStandaloneRepoUrl(""); + setStandaloneRepoPath(""); + setRepoSuggestions([]); + setShowRepoSuggestions(false); }, []); const handleCreateSubtask = useCallback(async () => { @@ -853,9 +934,13 @@ export default function MeshPage() { <div className="flex items-center gap-2"> {modalStep === 2 && ( <button - onClick={() => setModalStep(1)} + onClick={() => { + setModalStep(1); + setIsStandalone(false); + setSelectedContract(null); + }} className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" - title="Back to contract selection" + title="Back to selection" > <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> @@ -863,7 +948,7 @@ export default function MeshPage() { </button> )} <h2 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff]"> - {modalStep === 1 ? "Select Contract" : "Configure Task"} + {modalStep === 1 ? "Create Task" : isStandalone ? "Standalone Task" : "Configure Task"} </h2> </div> <button @@ -877,121 +962,282 @@ export default function MeshPage() { </div> <div className="p-4 overflow-y-auto flex-1"> {modalStep === 1 ? ( - // Step 1: Select Contract - contracts.length === 0 ? ( - <div className="text-center py-8"> - <p className="text-[#7788aa] font-mono text-xs mb-4">No contracts found.</p> - <button - onClick={() => { - handleCloseModal(); - navigate("/contracts"); - }} - className="px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] transition-colors" - > - Create Contract - </button> + // Step 1: Select Contract or Standalone + <div className="space-y-4"> + {/* Standalone Task Option */} + <button + onClick={handleSelectStandalone} + className="w-full text-left p-3 border border-[rgba(117,170,252,0.35)] bg-[#0a1525] hover:border-[#75aafc] hover:bg-[#0d1b2d] transition-colors" + > + <div className="flex items-center justify-between"> + <span className="text-[#9bc3ff] font-mono text-xs font-semibold">Standalone Task</span> + <span className="text-[10px] font-mono uppercase px-2 py-0.5 border border-[#75aafc] text-[#75aafc] bg-[rgba(117,170,252,0.1)]"> + Quick + </span> + </div> + <p className="text-[10px] font-mono text-[#7788aa] mt-1"> + Create a task without a contract. Configure repository directly. + </p> + </button> + + {/* Divider */} + <div className="flex items-center gap-3"> + <div className="flex-1 border-t border-[rgba(117,170,252,0.15)]" /> + <span className="text-[10px] font-mono text-[#556677] uppercase">Or select contract</span> + <div className="flex-1 border-t border-[rgba(117,170,252,0.15)]" /> </div> - ) : ( + + {/* Contract list */} + {contracts.length === 0 ? ( + <div className="text-center py-4"> + <p className="text-[#7788aa] font-mono text-xs mb-4">No contracts found.</p> + <button + onClick={() => { + handleCloseModal(); + navigate("/contracts"); + }} + className="px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] transition-colors" + > + Create Contract + </button> + </div> + ) : ( + <div className="space-y-2"> + {contracts.map((contract) => ( + <button + key={contract.id} + onClick={() => handleSelectContract(contract)} + className="w-full text-left p-3 border border-[rgba(117,170,252,0.15)] bg-[#0a1525] hover:border-[rgba(117,170,252,0.35)] hover:bg-[#0d1b2d] transition-colors" + > + <div className="flex items-center justify-between"> + <span className="text-[#9bc3ff] font-mono text-xs">{contract.name}</span> + <span className="text-[10px] font-mono uppercase px-2 py-0.5 border border-[rgba(117,170,252,0.25)] text-[#75aafc]"> + {contract.phase} + </span> + </div> + {contract.description && ( + <p className="text-[10px] font-mono text-[#7788aa] mt-1 line-clamp-2">{contract.description}</p> + )} + <div className="flex gap-3 mt-2 text-[10px] font-mono text-[#556677]"> + <span>{contract.taskCount} tasks</span> + <span>{contract.repositoryCount} repos</span> + </div> + </button> + ))} + </div> + )} + </div> + ) : isStandalone ? ( + // Step 2: Configure Standalone Task + <div className="space-y-4"> + {/* Standalone badge */} + <div className="flex items-center gap-2 text-xs font-mono text-[#7788aa]"> + <span className="px-2 py-0.5 border border-[#75aafc] text-[#75aafc] bg-[rgba(117,170,252,0.1)] text-[10px] uppercase"> + Standalone + </span> + <span className="text-[#556677]">No contract</span> + </div> + + {/* Task name */} + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Task Name</label> + <input + type="text" + value={newTaskName} + onChange={(e) => setNewTaskName(e.target.value)} + 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]" + placeholder="Task name" + /> + </div> + + {/* Repository type selector */} <div className="space-y-2"> - {contracts.map((contract) => ( + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Repository Type</label> + <div className="flex gap-2"> <button - key={contract.id} - onClick={() => handleSelectContract(contract)} - className="w-full text-left p-3 border border-[rgba(117,170,252,0.15)] bg-[#0a1525] hover:border-[rgba(117,170,252,0.35)] hover:bg-[#0d1b2d] transition-colors" + type="button" + onClick={() => handleStandaloneRepoTypeChange("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-[#0a1525] text-[#8b949e] border border-[rgba(117,170,252,0.25)] hover:border-[#75aafc]" + }`} > - <div className="flex items-center justify-between"> - <span className="text-[#9bc3ff] font-mono text-xs">{contract.name}</span> - <span className="text-[10px] font-mono uppercase px-2 py-0.5 border border-[rgba(117,170,252,0.25)] text-[#75aafc]"> - {contract.phase} - </span> - </div> - {contract.description && ( - <p className="text-[10px] font-mono text-[#7788aa] mt-1 line-clamp-2">{contract.description}</p> - )} - <div className="flex gap-3 mt-2 text-[10px] font-mono text-[#556677]"> - <span>{contract.taskCount} tasks</span> - <span>{contract.repositoryCount} repos</span> - </div> + Remote </button> - ))} + <button + type="button" + onClick={() => handleStandaloneRepoTypeChange("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-[#0a1525] text-[#8b949e] border border-[rgba(117,170,252,0.25)] hover:border-[#75aafc]" + }`} + > + Local + </button> + </div> </div> - ) - ) : ( - // Step 2: Configure Task - selectedContract && ( - <div className="space-y-4"> - {/* Contract badge */} - <div className="flex items-center gap-2 text-xs font-mono text-[#7788aa]"> - <span>Contract:</span> - <span className="text-[#9bc3ff]">{selectedContract.name}</span> + + {/* 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> + )} - {/* Task name */} + {/* Repository URL/Path input */} + {standaloneRepoType === "remote" ? ( <div className="space-y-1"> - <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Task Name</label> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Repository URL</label> <input type="text" - value={newTaskName} - onChange={(e) => setNewTaskName(e.target.value)} + value={standaloneRepoUrl} + onChange={(e) => setStandaloneRepoUrl(e.target.value)} 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]" - placeholder="Task name" + placeholder="https://github.com/user/repo.git" + /> + <p className="text-[10px] font-mono text-[#556677]"> + GitHub, GitLab, or any Git repository URL. + </p> + </div> + ) : ( + <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/repository" /> + <p className="text-[10px] font-mono text-[#556677]"> + Path to an existing local repository. + </p> </div> + )} + + {/* Target repo path (optional) */} + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Target Repository Path (Optional)</label> + <DirectoryInput + value={newTaskTargetPath} + onChange={setNewTaskTargetPath} + suggestions={daemonDirectories} + placeholder="/path/to/your/local/repo" + repoUrl={standaloneRepoType === "remote" ? standaloneRepoUrl : null} + /> + <p className="text-[10px] font-mono text-[#556677]"> + Path where the task will push/merge changes. Leave empty to configure later. + </p> + </div> - {/* Repository selection */} - {selectedContract.repositories.length > 0 && ( - <div className="space-y-1"> - <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Repository</label> - <select - value={newTaskRepoUrl || ""} - onChange={(e) => setNewTaskRepoUrl(e.target.value || null)} - 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]" - > - <option value="">No repository</option> - {selectedContract.repositories - .filter((r) => r.status === "ready") - .map((repo) => ( - <option key={repo.id} value={repo.repositoryUrl || repo.localPath || ""}> - {repo.name} - {repo.isPrimary && " (primary)"} - </option> - ))} - </select> - <p className="text-[10px] font-mono text-[#556677]"> - The repository this task will work on. - </p> - </div> - )} + {/* Create button */} + <div className="pt-2"> + <button + onClick={handleCreateTask} + disabled={creating} + className="w-full px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] disabled:opacity-50 transition-colors" + > + {creating ? "Creating..." : "Create Task"} + </button> + </div> + </div> + ) : selectedContract && ( + // Step 2: Configure Task (Contract-based) + <div className="space-y-4"> + {/* Contract badge */} + <div className="flex items-center gap-2 text-xs font-mono text-[#7788aa]"> + <span>Contract:</span> + <span className="text-[#9bc3ff]">{selectedContract.name}</span> + </div> - {/* Target repo path with DirectoryInput */} - {newTaskRepoUrl && ( - <div className="space-y-1"> - <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Target Repository Path</label> - <DirectoryInput - value={newTaskTargetPath} - onChange={setNewTaskTargetPath} - suggestions={daemonDirectories} - placeholder="/path/to/your/local/repo" - repoUrl={newTaskRepoUrl} - /> - <p className="text-[10px] font-mono text-[#556677]"> - Path where the task will push/merge changes. Leave empty to configure later. - </p> - </div> - )} + {/* Task name */} + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Task Name</label> + <input + type="text" + value={newTaskName} + onChange={(e) => setNewTaskName(e.target.value)} + 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]" + placeholder="Task name" + /> + </div> - {/* Create button */} - <div className="pt-2"> - <button - onClick={handleCreateTask} - disabled={creating} - className="w-full px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] disabled:opacity-50 transition-colors" + {/* Repository selection */} + {selectedContract.repositories.length > 0 && ( + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Repository</label> + <select + value={newTaskRepoUrl || ""} + onChange={(e) => setNewTaskRepoUrl(e.target.value || null)} + 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]" > - {creating ? "Creating..." : "Create Task"} - </button> + <option value="">No repository</option> + {selectedContract.repositories + .filter((r) => r.status === "ready") + .map((repo) => ( + <option key={repo.id} value={repo.repositoryUrl || repo.localPath || ""}> + {repo.name} + {repo.isPrimary && " (primary)"} + </option> + ))} + </select> + <p className="text-[10px] font-mono text-[#556677]"> + The repository this task will work on. + </p> </div> + )} + + {/* Target repo path with DirectoryInput */} + {newTaskRepoUrl && ( + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">Target Repository Path</label> + <DirectoryInput + value={newTaskTargetPath} + onChange={setNewTaskTargetPath} + suggestions={daemonDirectories} + placeholder="/path/to/your/local/repo" + repoUrl={newTaskRepoUrl} + /> + <p className="text-[10px] font-mono text-[#556677]"> + Path where the task will push/merge changes. Leave empty to configure later. + </p> + </div> + )} + + {/* Create button */} + <div className="pt-2"> + <button + onClick={handleCreateTask} + disabled={creating} + className="w-full px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] disabled:opacity-50 transition-colors" + > + {creating ? "Creating..." : "Create Task"} + </button> </div> - ) + </div> )} </div> </div> |
