diff options
| author | soryu <soryu@soryu.co> | 2026-01-21 19:06:17 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-21 19:06:17 +0000 |
| commit | d7311e40c3326aaeea0918fe870f3cae7163ca15 (patch) | |
| tree | 9927500e64a0b8b2a2d4907ad879639e8ec3af99 | |
| parent | 9e286c146e29e714b3b209b4d948d75cce179b05 (diff) | |
| download | soryu-d7311e40c3326aaeea0918fe870f3cae7163ca15.tar.gz soryu-d7311e40c3326aaeea0918fe870f3cae7163ca15.zip | |
Add standalone task creation from mesh page (#16)
Allow creating anonymous tasks (tasks without a contract_id) directly
from the mesh page:
- Add 'Create Standalone Task' option in task creation modal step 1
- Update step 2 to handle null selectedContract for standalone tasks
- Show working directory input for standalone tasks
- Update TaskList to show standalone tasks (filter: !parentTaskId && (isSupervisor || !contractId))
- Display 'Standalone Tasks' group header with lightning bolt icon
- Update empty state message to mention standalone tasks
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | makima/frontend/src/components/mesh/TaskList.tsx | 23 | ||||
| -rw-r--r-- | makima/frontend/src/routes/mesh.tsx | 248 |
2 files changed, 163 insertions, 108 deletions
diff --git a/makima/frontend/src/components/mesh/TaskList.tsx b/makima/frontend/src/components/mesh/TaskList.tsx index 80077b6..e3f2862 100644 --- a/makima/frontend/src/components/mesh/TaskList.tsx +++ b/makima/frontend/src/components/mesh/TaskList.tsx @@ -95,8 +95,9 @@ export function TaskList({ // Group tasks by contract and filter by status const groupedTasks = useMemo(() => { - // Separate root tasks (no parent) from subtasks, and only show supervisor tasks - const rootTasks = tasks.filter((t) => !t.parentTaskId && t.isSupervisor); + // Separate root tasks (no parent) from subtasks + // Show supervisor tasks AND standalone tasks (tasks without a contract) + const rootTasks = tasks.filter((t) => !t.parentTaskId && (t.isSupervisor || !t.contractId)); // Filter tasks based on contract status const filteredTasks = statusFilter === 'all' @@ -205,8 +206,8 @@ export function TaskList({ {totalTasks === 0 ? ( <div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8"> {statusFilter === 'all' - ? 'No supervisor tasks yet. Create a contract to start orchestrating tasks.' - : `No ${statusFilter} supervisor tasks found.`} + ? 'No tasks yet. Create a contract or a standalone task to get started.' + : `No ${statusFilter} tasks found.`} </div> ) : ( <div> @@ -229,9 +230,17 @@ export function TaskList({ </span> </> ) : ( - <span className="font-mono text-xs text-[#8b949e] italic"> - Unassigned Tasks ({group.tasks.length}) - </span> + <> + <svg className="w-4 h-4 text-[#9bc3ff]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> + </svg> + <span className="font-mono text-xs text-[#9bc3ff]"> + Standalone Tasks + </span> + <span className="font-mono text-[10px] text-[#8b949e]"> + ({group.tasks.length}) + </span> + </> )} </div> diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx index 453bdff..77e9d7e 100644 --- a/makima/frontend/src/routes/mesh.tsx +++ b/makima/frontend/src/routes/mesh.tsx @@ -525,15 +525,24 @@ export default function MeshPage() { } }, []); + // Handle creating a standalone task (no contract) + const handleCreateStandaloneTask = useCallback(() => { + setSelectedContract(null); + setNewTaskName("Standalone Task"); + setNewTaskRepoUrl(null); + setNewTaskTargetPath(""); + setModalStep(2); + }, []); + // Create task with configured options const handleCreateTask = useCallback(async () => { - if (creating || !selectedContract) return; + if (creating) return; setShowContractModal(false); setCreating(true); try { const newTask = await saveTask({ - contractId: selectedContract.id, - name: newTaskName || `Task for ${selectedContract.name}`, + 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, @@ -878,120 +887,157 @@ export default function MeshPage() { <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> + <div className="space-y-4"> + {/* Standalone task option */} + <div className="pb-3 border-b border-[rgba(117,170,252,0.25)]"> <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" + onClick={handleCreateStandaloneTask} + className="w-full text-left p-3 border border-[rgba(117,170,252,0.25)] bg-[#0a1525] hover:border-[#3f6fb3] hover:bg-[#0d1b2d] transition-colors" > - Create Contract + <div className="flex items-center gap-2"> + <svg className="w-4 h-4 text-[#9bc3ff]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> + </svg> + <span className="text-[#9bc3ff] font-mono text-xs font-medium">Create Standalone Task</span> + </div> + <p className="text-[10px] font-mono text-[#7788aa] mt-1 ml-6"> + Quick task without a contract. Good for one-off tasks. + </p> </button> </div> - ) : ( - <div className="space-y-2"> - {contracts.map((contract) => ( + + {/* Contract selection */} + {contracts.length === 0 ? ( + <div className="text-center py-4"> + <p className="text-[#7788aa] font-mono text-xs mb-4">No contracts found.</p> <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" + 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" > - <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> + Create Contract </button> - ))} - </div> - ) + </div> + ) : ( + <div className="space-y-2"> + <div className="text-[10px] font-mono uppercase tracking-wide text-[#7788aa] px-1"> + Or select a contract: + </div> + {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> ) : ( // 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> - </div> + <div className="space-y-4"> + {/* Context badge */} + <div className="flex items-center gap-2 text-xs font-mono text-[#7788aa]"> + {selectedContract ? ( + <> + <span>Contract:</span> + <span className="text-[#9bc3ff]">{selectedContract.name}</span> + </> + ) : ( + <> + <svg className="w-4 h-4 text-[#9bc3ff]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> + </svg> + <span className="text-[#9bc3ff]">Standalone Task</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> - {/* Task name */} + {/* Repository selection - only 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]">Task Name</label> - <input - type="text" - value={newTaskName} - onChange={(e) => setNewTaskName(e.target.value)} + <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]" - placeholder="Task name" - /> + > + <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> + )} - {/* 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> - )} - - {/* 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> + {/* Target repo path with DirectoryInput - for standalone tasks or when repo is selected */} + {(newTaskRepoUrl || !selectedContract) && ( + <div className="space-y-1"> + <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> + {selectedContract ? "Target Repository Path" : "Working Directory (optional)"} + </label> + <DirectoryInput + value={newTaskTargetPath} + onChange={setNewTaskTargetPath} + suggestions={daemonDirectories} + placeholder="/path/to/your/local/repo" + 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."} + </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> |
