From e0da93a20a965125ba4cbb46e3e0e179f06c2a08 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* Task completion checkpoint

* Task completion checkpoint

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
---
 makima/frontend/src/components/mesh/TaskList.tsx | 69 +++++++++++++++++++-----
 1 file changed, 57 insertions(+), 12 deletions(-)

(limited to 'makima/frontend/src/components/mesh/TaskList.tsx')

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<StatusFilter>('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<string | null, GroupedTasks>();
 
-    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 (
     <div className="panel h-full flex flex-col">
       <div className="flex items-center justify-between p-4 pb-2 border-b border-dashed border-[rgba(117,170,252,0.35)]">
         <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
           MESH//TASKS
         </div>
-        <button
-          onClick={onCreate}
-          className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors uppercase"
-        >
-          + New Task
-        </button>
+        <div className="flex items-center gap-2">
+          {/* Status filter toggle */}
+          <div className="flex border border-[rgba(117,170,252,0.25)]">
+            {filterOptions.map((option) => (
+              <button
+                key={option.value}
+                onClick={() => setStatusFilter(option.value)}
+                className={`px-2 py-1 font-mono text-[10px] uppercase transition-colors ${
+                  statusFilter === option.value
+                    ? 'bg-[rgba(117,170,252,0.2)] text-[#dbe7ff] border-r border-[rgba(117,170,252,0.25)] last:border-r-0'
+                    : 'text-[#8b949e] hover:text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)] border-r border-[rgba(117,170,252,0.25)] last:border-r-0'
+                }`}
+              >
+                {option.label}
+              </button>
+            ))}
+          </div>
+          <button
+            onClick={onCreate}
+            className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors uppercase"
+          >
+            + New Task
+          </button>
+        </div>
       </div>
 
       <div className="flex-1 overflow-y-auto">
         {totalTasks === 0 ? (
           <div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8">
-            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.`}
           </div>
         ) : (
           <div>
-- 
cgit v1.2.3

