From e0da93a20a965125ba4cbb46e3e0e179f06c2a08 Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 18 Jan 2026 18:02:08 +0000 Subject: Improve Mesh Tab: Organize by contract with status filter (#5) * Add status filter toggle to Mesh Tab TaskList component Add a filter toggle at the top of the TaskList that allows filtering by contract status (All, Active, Completed, Archive) with Active as the default. Changes: - Backend: Add contract_status field to TaskSummary struct in models.rs - Backend: Update all SQL queries returning TaskSummary to include c.status as contract_status from the contracts table join - Frontend: Add contractStatus to TaskSummary TypeScript type - Frontend: Add useState for statusFilter with 'active' as default - Frontend: Add filter button group in header area next to '+ New Task' - Frontend: Update groupedTasks useMemo to filter based on contract status - Frontend: Update empty state message to reflect current filter Co-Authored-By: Claude Opus 4.5 * Task completion checkpoint * feat(mesh): show all contract tasks for supervisor tasks When viewing a supervisor task (task.isSupervisor === true), the TaskDetail component now shows all tasks in the contract instead of showing the subtasks tree. Changes: - Add contractTasks prop to TaskDetailProps for passing contract tasks - Add displayTasks computed value that uses contractTasks for supervisors - Change section header from "Subtasks" to "Contract Tasks" for supervisors - Hide the "+ Add Subtask" button for supervisor tasks - Update empty state message for supervisors - Fetch contract tasks in mesh.tsx when viewing a supervisor task - Filter out the supervisor itself from the contract tasks list Co-Authored-By: Claude Opus 4.5 * Task completion checkpoint * Task completion checkpoint --------- Co-authored-by: Claude Opus 4.5 --- makima/frontend/src/components/mesh/TaskDetail.tsx | 43 +++++++++----- makima/frontend/src/components/mesh/TaskList.tsx | 69 ++++++++++++++++++---- 2 files changed, 84 insertions(+), 28 deletions(-) (limited to 'makima/frontend/src/components/mesh') diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx index 8e853e7..efe26a8 100644 --- a/makima/frontend/src/components/mesh/TaskDetail.tsx +++ b/makima/frontend/src/components/mesh/TaskDetail.tsx @@ -32,6 +32,8 @@ interface TaskDetailProps { onCreatePR?: (title: string, body: string, draft: boolean) => Promise; onAutoMerge?: () => Promise; fetchSubtasks?: (taskId: string) => Promise; + /** For supervisor tasks: all tasks in the contract (excluding the supervisor itself) */ + contractTasks?: TaskSummary[]; } function formatDate(dateStr: string): string { @@ -114,6 +116,7 @@ export function TaskDetail({ onCreatePR, onAutoMerge, fetchSubtasks, + contractTasks, }: TaskDetailProps) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(task.name); @@ -149,10 +152,18 @@ export function TaskDetail({ // Show continue for supervisors (always) or terminal states for other tasks const canContinue = isSupervisor || isTaskTerminal; - // Calculate subtask statistics + // Determine which tasks to show: for supervisors, show contractTasks; for regular tasks, show subtasks + const displayTasks = useMemo(() => { + if (isSupervisor && contractTasks) { + return contractTasks; + } + return task.subtasks; + }, [isSupervisor, contractTasks, task.subtasks]); + + // Calculate task statistics for progress bar const subtaskStats = useMemo( - () => calculateTreeStats(task.subtasks), - [task.subtasks] + () => calculateTreeStats(displayTasks), + [displayTasks] ); // Check if task can create PR @@ -645,14 +656,14 @@ export function TaskDetail({ )} - {/* Subtasks */} + {/* Subtasks / Contract Tasks (for supervisors) */}
- Subtasks ({task.subtasks.length}) + {isSupervisor ? `Contract Tasks (${displayTasks.length})` : `Subtasks (${displayTasks.length})`}
- {task.subtasks.length > 0 && ( + {displayTasks.length > 0 && ( )}
- {/* Disable adding subtasks at max depth (2 = sub-subtask, cannot have children) */} - {task.depth < 2 ? ( + {/* Disable adding subtasks for supervisors and at max depth (2 = sub-subtask, cannot have children) */} + {!isSupervisor && task.depth < 2 ? ( - ) : ( + ) : !isSupervisor ? ( Max depth - )} + ) : null}
- {/* Progress bar for subtasks */} - {task.subtasks.length > 0 && ( + {/* Progress bar for tasks */} + {displayTasks.length > 0 && ( )} - {task.subtasks.length === 0 ? ( + {displayTasks.length === 0 ? (
- No subtasks yet + {isSupervisor ? "No tasks in contract yet" : "No subtasks yet"}
) : useTreeView ? (
) : (
- {task.subtasks.map((subtask: TaskSummary) => { + {displayTasks.map((subtask: TaskSummary) => { const isRunning = subtask.status === "running" || subtask.status === "initializing" || subtask.status === "starting"; const isViewingOutput = viewingSubtaskId === subtask.id; const isExpanded = expandedSubtaskId === subtask.id; diff --git a/makima/frontend/src/components/mesh/TaskList.tsx b/makima/frontend/src/components/mesh/TaskList.tsx index d013782..f829b29 100644 --- a/makima/frontend/src/components/mesh/TaskList.tsx +++ b/makima/frontend/src/components/mesh/TaskList.tsx @@ -1,5 +1,5 @@ -import { useMemo } from "react"; -import type { TaskSummary, TaskStatus, ContractPhase } from "../../lib/api"; +import { useMemo, useState } from "react"; +import type { TaskSummary, TaskStatus, ContractPhase, ContractStatus } from "../../lib/api"; interface TaskListProps { tasks: TaskSummary[]; @@ -13,9 +13,12 @@ interface GroupedTasks { contractId: string | null; contractName: string | null; contractPhase: ContractPhase | null; + contractStatus: ContractStatus | null; tasks: TaskSummary[]; } +type StatusFilter = 'all' | 'active' | 'completed' | 'archived'; + function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString("en-US", { @@ -87,21 +90,36 @@ export function TaskList({ onDelete, onCreate, }: TaskListProps) { - // Group tasks by contract + // Filter state - default to 'active' to show only active contracts + const [statusFilter, setStatusFilter] = useState('active'); + + // Group tasks by contract and filter by status const groupedTasks = useMemo(() => { // Separate root tasks (no parent) from subtasks const rootTasks = tasks.filter((t) => !t.parentTaskId); + // Filter tasks based on contract status + const filteredTasks = statusFilter === 'all' + ? rootTasks + : rootTasks.filter((task) => { + // Tasks without a contract go in 'all' only, or treat them as 'active' + if (!task.contractId) { + return statusFilter === 'active'; + } + return task.contractStatus === statusFilter; + }); + // Group by contractId const groups = new Map(); - for (const task of rootTasks) { + for (const task of filteredTasks) { const key = task.contractId; if (!groups.has(key)) { groups.set(key, { contractId: task.contractId, contractName: task.contractName, contractPhase: task.contractPhase, + contractStatus: task.contractStatus, tasks: [], }); } @@ -132,7 +150,7 @@ export function TaskList({ }); return sorted; - }, [tasks]); + }, [tasks, statusFilter]); if (loading) { return ( @@ -144,24 +162,51 @@ export function TaskList({ const totalTasks = groupedTasks.reduce((sum, g) => sum + g.tasks.length, 0); + const filterOptions: { value: StatusFilter; label: string }[] = [ + { value: 'all', label: 'All' }, + { value: 'active', label: 'Active' }, + { value: 'completed', label: 'Completed' }, + { value: 'archived', label: 'Archive' }, + ]; + return (
MESH//TASKS
- +
+ {/* Status filter toggle */} +
+ {filterOptions.map((option) => ( + + ))} +
+ +
{totalTasks === 0 ? (
- No tasks yet. Create one to start orchestrating Claude Code instances. + {statusFilter === 'all' + ? 'No tasks yet. Create one to start orchestrating Claude Code instances.' + : `No ${statusFilter} tasks found.`}
) : (
-- cgit v1.2.3