summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-18 18:02:08 +0000
committerGitHub <noreply@github.com>2026-01-18 18:02:08 +0000
commite0da93a20a965125ba4cbb46e3e0e179f06c2a08 (patch)
tree5d127394e1dfa921c5d09fe8f10d716f6548d168
parent869f21ee2efaefed6a5aa4fbd417c25df8dec02a (diff)
downloadsoryu-e0da93a20a965125ba4cbb46e3e0e179f06c2a08.tar.gz
soryu-e0da93a20a965125ba4cbb46e3e0e179f06c2a08.zip
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>
-rw-r--r--makima/frontend/src/components/mesh/TaskDetail.tsx43
-rw-r--r--makima/frontend/src/components/mesh/TaskList.tsx69
-rw-r--r--makima/frontend/src/lib/api.ts2
-rw-r--r--makima/frontend/src/routes/mesh.tsx25
-rw-r--r--makima/src/db/models.rs3
-rw-r--r--makima/src/db/repository.rs7
6 files changed, 120 insertions, 29 deletions
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<void>;
onAutoMerge?: () => Promise<void>;
fetchSubtasks?: (taskId: string) => Promise<TaskSummary[]>;
+ /** 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({
</div>
)}
- {/* Subtasks */}
+ {/* Subtasks / Contract Tasks (for supervisors) */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
- Subtasks ({task.subtasks.length})
+ {isSupervisor ? `Contract Tasks (${displayTasks.length})` : `Subtasks (${displayTasks.length})`}
</div>
- {task.subtasks.length > 0 && (
+ {displayTasks.length > 0 && (
<button
onClick={() => setUseTreeView(!useTreeView)}
className="font-mono text-[9px] text-[#555] hover:text-[#75aafc]"
@@ -661,41 +672,41 @@ export function TaskDetail({
</button>
)}
</div>
- {/* 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 ? (
<button
onClick={onCreateSubtask}
className="px-2 py-1 font-mono text-[10px] text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
>
+ Add Subtask
</button>
- ) : (
+ ) : !isSupervisor ? (
<span className="px-2 py-1 font-mono text-[10px] text-[#555] border border-[#333]" title="Maximum depth reached">
Max depth
</span>
- )}
+ ) : null}
</div>
- {/* Progress bar for subtasks */}
- {task.subtasks.length > 0 && (
+ {/* Progress bar for tasks */}
+ {displayTasks.length > 0 && (
<SubtaskProgressBar stats={subtaskStats} />
)}
- {task.subtasks.length === 0 ? (
+ {displayTasks.length === 0 ? (
<div className="text-[#555] font-mono text-xs py-4 text-center">
- No subtasks yet
+ {isSupervisor ? "No tasks in contract yet" : "No subtasks yet"}
</div>
) : useTreeView ? (
<div className="border border-[rgba(117,170,252,0.15)]">
<SubtaskTree
- subtasks={task.subtasks}
+ subtasks={displayTasks}
onSelect={onSelectSubtask}
fetchSubtasks={fetchSubtasks}
/>
</div>
) : (
<div className="divide-y divide-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.15)]">
- {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<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>
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 9b725bc..4652347 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -532,6 +532,8 @@ export interface TaskSummary {
contractName: string | null;
/** Contract phase (joined from contracts table) */
contractPhase: ContractPhase | null;
+ /** Contract status (joined from contracts table) */
+ contractStatus: ContractStatus | null;
parentTaskId: string | null;
depth: number;
name: string;
diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx
index cc09bca..a8d3574 100644
--- a/makima/frontend/src/routes/mesh.tsx
+++ b/makima/frontend/src/routes/mesh.tsx
@@ -7,7 +7,7 @@ import { TaskOutput } from "../components/mesh/TaskOutput";
import { UnifiedMeshChatInput } from "../components/mesh/UnifiedMeshChatInput";
import { useTasks } from "../hooks/useTasks";
import { useTaskSubscription, type TaskUpdateEvent, type TaskOutputEvent } from "../hooks/useTaskSubscription";
-import type { TaskWithSubtasks, MeshChatContext, ContractSummary, ContractWithRelations, DaemonDirectory } from "../lib/api";
+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 } from "../lib/api";
import { DirectoryInput } from "../components/mesh/DirectoryInput";
import { useAuth } from "../contexts/AuthContext";
@@ -119,6 +119,8 @@ export default function MeshPage() {
// 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);
+ // For supervisor tasks: all tasks in the contract (excluding the supervisor itself)
+ const [contractTasks, setContractTasks] = useState<TaskSummary[]>([]);
// View mode for the split panel layout
const [viewMode, setViewMode] = useState<ViewMode>("split");
// Width of the task panel as a percentage (0-100)
@@ -309,6 +311,26 @@ export default function MeshPage() {
}
}, [id, fetchTask]);
+ // For supervisor tasks: fetch all tasks in the contract (excluding the supervisor itself)
+ useEffect(() => {
+ if (taskDetail?.isSupervisor && taskDetail.contractId) {
+ getContract(taskDetail.contractId)
+ .then((contract) => {
+ // Filter out the supervisor task itself
+ const tasksExcludingSupervisor = contract.tasks.filter(
+ (t) => t.id !== taskDetail.id
+ );
+ setContractTasks(tasksExcludingSupervisor);
+ })
+ .catch((err) => {
+ console.error("Failed to fetch contract tasks for supervisor:", err);
+ setContractTasks([]);
+ });
+ } else {
+ setContractTasks([]);
+ }
+ }, [taskDetail?.isSupervisor, taskDetail?.contractId, taskDetail?.id]);
+
const handleSelectTask = useCallback(
(taskId: string) => {
navigate(`/mesh/${taskId}`);
@@ -711,6 +733,7 @@ export default function MeshPage() {
onToggleSubtaskOutput={handleToggleSubtaskOutput}
viewingSubtaskId={viewingSubtaskId}
onViewContract={(contractId) => navigate(`/contracts/${contractId}`)}
+ contractTasks={taskDetail.isSupervisor ? contractTasks : undefined}
/>
</div>
)}
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 99c8b8e..3e6997f 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -544,6 +544,8 @@ pub struct TaskSummary {
pub contract_name: Option<String>,
/// Contract phase (joined from contracts table)
pub contract_phase: Option<String>,
+ /// Contract status (joined from contracts table): 'active', 'completed', 'archived'
+ pub contract_status: Option<String>,
pub parent_task_id: Option<Uuid>,
/// Depth in task hierarchy: 0=orchestrator (top-level), 1=subtask (max)
pub depth: i32,
@@ -568,6 +570,7 @@ impl From<Task> for TaskSummary {
contract_id: task.contract_id,
contract_name: None, // Not available from Task directly
contract_phase: None, // Not available from Task directly
+ contract_status: None, // Not available from Task directly
parent_task_id: task.parent_task_id,
depth: task.depth,
name: task.name,
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 3d1efd1..36e6bc1 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -735,6 +735,7 @@ pub async fn list_tasks(pool: &PgPool) -> Result<Vec<TaskSummary>, sqlx::Error>
r#"
SELECT
t.id, t.contract_id, c.name as contract_name, c.phase as contract_phase,
+ c.status as contract_status,
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
@@ -755,6 +756,7 @@ pub async fn list_subtasks(pool: &PgPool, parent_id: Uuid) -> Result<Vec<TaskSum
r#"
SELECT
t.id, t.contract_id, c.name as contract_name, c.phase as contract_phase,
+ c.status as contract_status,
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
@@ -1129,6 +1131,7 @@ pub async fn list_tasks_for_owner(
r#"
SELECT
t.id, t.contract_id, c.name as contract_name, c.phase as contract_phase,
+ c.status as contract_status,
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
@@ -1154,6 +1157,7 @@ pub async fn list_subtasks_for_owner(
r#"
SELECT
t.id, t.contract_id, c.name as contract_name, c.phase as contract_phase,
+ c.status as contract_status,
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
@@ -1671,6 +1675,7 @@ pub async fn list_sibling_tasks(
r#"
SELECT
t.id, t.contract_id, c.name as contract_name, c.phase as contract_phase,
+ c.status as contract_status,
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
@@ -1692,6 +1697,7 @@ pub async fn list_sibling_tasks(
r#"
SELECT
t.id, t.contract_id, c.name as contract_name, c.phase as contract_phase,
+ c.status as contract_status,
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
@@ -2669,6 +2675,7 @@ pub async fn list_tasks_in_contract(
r#"
SELECT
t.id, t.contract_id, c.name as contract_name, c.phase as contract_phase,
+ c.status as contract_status,
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,