-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathindex.tsx
More file actions
137 lines (124 loc) · 4.25 KB
/
Copy pathindex.tsx
File metadata and controls
137 lines (124 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* TasksPanel — left-panel entry point. Org-wide (not scoped to a virtualMCP).
* Renders all open tasks in a single list, sorted by updated_at.
* Automation-triggered tasks are distinguished by a badge on their avatar.
*/
import { Suspense, useState, useTransition } from "react";
import { useParams } from "@tanstack/react-router";
import {
useMCPClient,
useProjectContext,
SELF_MCP_ALIAS_ID,
} from "@decocms/mesh-sdk";
import { useQueryClient } from "@tanstack/react-query";
import { ClipboardCheck } from "@untitledui/icons";
import { ErrorBoundary } from "@/web/components/error-boundary";
import { Chat } from "@/web/components/chat";
import { EmptyState } from "@/web/components/empty-state";
import { useTasks } from "@/web/components/chat/task/use-task-manager";
import { callUpdateTaskTool } from "@/web/components/chat/task/helpers";
import type { Task } from "@/web/components/chat/task/types";
import { useTasksAutoRefresh } from "@/web/hooks/use-tasks-auto-refresh";
import { usePanelActions } from "@/web/layouts/shell-layout";
import { KEYS } from "@/web/lib/query-keys";
import { toast } from "sonner";
import {
TasksSection,
type FilterOption,
type MemberFilter,
} from "./tasks-section";
function TasksPanelContent() {
useTasksAutoRefresh();
const [memberFilter, setMemberFilter] = useState<MemberFilter>("mine");
const [typeFilter, setTypeFilter] = useState<FilterOption>("all");
const [, startFilterTransition] = useTransition();
const taskOwner = memberFilter === "mine" ? "me" : "all";
const { tasks: myTasks } = useTasks({
owner: taskOwner,
status: "open",
hasTrigger: false,
});
const { tasks: automationTasks } = useTasks({
owner: taskOwner,
status: "open",
hasTrigger: true,
});
const { setTaskId, createNewTask } = usePanelActions();
const params = useParams({ strict: false }) as { taskId?: string };
const { locator, org } = useProjectContext();
const queryClient = useQueryClient();
const client = useMCPClient({
connectionId: SELF_MCP_ALIAS_ID,
orgId: org.id,
});
const activeTaskId = params.taskId ?? null;
const taggedAutomationTasks = automationTasks.map((t) => ({
...t,
fromAutomation: true as const,
}));
const allTasks = [
...(typeFilter !== "automation" ? myTasks : []),
...(typeFilter !== "manual" ? taggedAutomationTasks : []),
].sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
const handleSetMemberFilter = (v: MemberFilter) =>
startFilterTransition(() => setMemberFilter(v));
const handleSetTypeFilter = (v: FilterOption) =>
startFilterTransition(() => setTypeFilter(v));
const handleArchive = async (task: Task) => {
try {
await callUpdateTaskTool(client, task.id, { hidden: true });
queryClient.invalidateQueries({
queryKey: KEYS.tasksPrefix(locator),
});
} catch (error) {
const err = error as Error;
toast.error(`Failed to archive task: ${err.message}`);
}
};
if (allTasks.length === 0) {
return (
<div className="h-full flex items-center justify-center p-4">
<EmptyState
image={<ClipboardCheck size={48} className="text-muted-foreground" />}
title="No tasks yet"
description="Start a conversation to create your first task."
/>
</div>
);
}
return (
<div className="flex flex-col h-full min-h-0 overflow-y-auto p-2 gap-3">
<TasksSection
title="Tasks"
tasks={allTasks}
activeTaskId={activeTaskId}
onSelect={(t) => setTaskId(t.id, t.virtual_mcp_id)}
onArchive={handleArchive}
onNew={createNewTask}
showNewButton
filter={typeFilter}
setFilter={handleSetTypeFilter}
memberFilter={memberFilter}
setMemberFilter={handleSetMemberFilter}
/>
</div>
);
}
function TasksPanelSkeleton() {
return (
<div className="flex flex-col h-full p-2 gap-1.5">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-10 rounded-md bg-muted/60 animate-pulse" />
))}
</div>
);
}
export function TasksPanel() {
return (
<ErrorBoundary fallback={<Chat.Skeleton />}>
<Suspense fallback={<TasksPanelSkeleton />}>
<TasksPanelContent />
</Suspense>
</ErrorBoundary>
);
}