summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-21 19:06:17 +0000
committerGitHub <noreply@github.com>2026-01-21 19:06:17 +0000
commitd7311e40c3326aaeea0918fe870f3cae7163ca15 (patch)
tree9927500e64a0b8b2a2d4907ad879639e8ec3af99
parent9e286c146e29e714b3b209b4d948d75cce179b05 (diff)
downloadsoryu-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.tsx23
-rw-r--r--makima/frontend/src/routes/mesh.tsx248
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>