@polpo-ai/dashboard 0.1.3 → 0.15.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/LICENSE +3 -1
  2. package/README.md +13 -40
  3. package/dist/components.d.ts +53 -0
  4. package/dist/components.d.ts.map +1 -0
  5. package/dist/components.js +81 -0
  6. package/dist/components.js.map +1 -0
  7. package/dist/host.d.ts +24 -0
  8. package/dist/host.d.ts.map +1 -0
  9. package/dist/host.js +18 -0
  10. package/dist/host.js.map +1 -0
  11. package/dist/index.d.ts +13 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +11 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/test/setup.d.ts +2 -0
  16. package/dist/test/setup.d.ts.map +1 -0
  17. package/dist/test/setup.js +2 -0
  18. package/dist/test/setup.js.map +1 -0
  19. package/dist/views/agent-detail.d.ts +4 -0
  20. package/dist/views/agent-detail.d.ts.map +1 -0
  21. package/dist/views/agent-detail.js +31 -0
  22. package/dist/views/agent-detail.js.map +1 -0
  23. package/dist/views/agents.d.ts +2 -0
  24. package/dist/views/agents.d.ts.map +1 -0
  25. package/dist/views/agents.js +81 -0
  26. package/dist/views/agents.js.map +1 -0
  27. package/dist/views/files.d.ts +2 -0
  28. package/dist/views/files.d.ts.map +1 -0
  29. package/dist/views/files.js +30 -0
  30. package/dist/views/files.js.map +1 -0
  31. package/dist/views/memory.d.ts +2 -0
  32. package/dist/views/memory.d.ts.map +1 -0
  33. package/dist/views/memory.js +19 -0
  34. package/dist/views/memory.js.map +1 -0
  35. package/dist/views/playground.d.ts +4 -0
  36. package/dist/views/playground.d.ts.map +1 -0
  37. package/dist/views/playground.js +43 -0
  38. package/dist/views/playground.js.map +1 -0
  39. package/dist/views/session-detail.d.ts +4 -0
  40. package/dist/views/session-detail.d.ts.map +1 -0
  41. package/dist/views/session-detail.js +23 -0
  42. package/dist/views/session-detail.js.map +1 -0
  43. package/dist/views/sessions.d.ts +2 -0
  44. package/dist/views/sessions.d.ts.map +1 -0
  45. package/dist/views/sessions.js +17 -0
  46. package/dist/views/sessions.js.map +1 -0
  47. package/dist/views/skills.d.ts +2 -0
  48. package/dist/views/skills.d.ts.map +1 -0
  49. package/dist/views/skills.js +16 -0
  50. package/dist/views/skills.js.map +1 -0
  51. package/package.json +47 -83
  52. package/src/styles.css +169 -0
  53. package/app/(dashboard)/error.tsx +0 -47
  54. package/app/(dashboard)/layout.tsx +0 -41
  55. package/app/(dashboard)/loading.tsx +0 -30
  56. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +0 -36
  57. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +0 -30
  58. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +0 -51
  59. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +0 -211
  60. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +0 -84
  61. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +0 -525
  62. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +0 -61
  63. package/app/(dashboard)/projects/[id]/agents/page.tsx +0 -39
  64. package/app/(dashboard)/projects/[id]/agents/view.tsx +0 -283
  65. package/app/(dashboard)/projects/[id]/layout.tsx +0 -11
  66. package/app/(dashboard)/projects/[id]/logs/page.tsx +0 -27
  67. package/app/(dashboard)/projects/[id]/logs/view.tsx +0 -184
  68. package/app/(dashboard)/projects/[id]/memory/page.tsx +0 -40
  69. package/app/(dashboard)/projects/[id]/memory/view.tsx +0 -17
  70. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +0 -88
  71. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +0 -569
  72. package/app/(dashboard)/projects/[id]/missions/page.tsx +0 -32
  73. package/app/(dashboard)/projects/[id]/missions/view.tsx +0 -282
  74. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +0 -359
  75. package/app/(dashboard)/projects/[id]/page.tsx +0 -68
  76. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +0 -5
  77. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +0 -433
  78. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +0 -5
  79. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +0 -222
  80. package/app/(dashboard)/projects/[id]/schedules/page.tsx +0 -44
  81. package/app/(dashboard)/projects/[id]/schedules/view.tsx +0 -385
  82. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +0 -55
  83. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +0 -373
  84. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +0 -124
  85. package/app/(dashboard)/projects/[id]/sessions/page.tsx +0 -31
  86. package/app/(dashboard)/projects/[id]/sessions/view.tsx +0 -242
  87. package/app/(dashboard)/projects/[id]/settings/page.tsx +0 -52
  88. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +0 -65
  89. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +0 -227
  90. package/app/(dashboard)/projects/[id]/skills/page.tsx +0 -31
  91. package/app/(dashboard)/projects/[id]/skills/view.tsx +0 -243
  92. package/app/(dashboard)/projects/[id]/storage/page.tsx +0 -5
  93. package/app/(dashboard)/projects/[id]/storage/view.tsx +0 -385
  94. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +0 -336
  95. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +0 -113
  96. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +0 -72
  97. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +0 -90
  98. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +0 -63
  99. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +0 -137
  100. package/app/(dashboard)/projects/[id]/tasks/page.tsx +0 -40
  101. package/app/(dashboard)/projects/[id]/tasks/view.tsx +0 -421
  102. package/app/(dashboard)/projects/[id]/view.tsx +0 -421
  103. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +0 -156
  104. package/app/(dashboard)/projects/projects-list.tsx +0 -126
  105. package/app/(playground)/error.tsx +0 -42
  106. package/app/(playground)/layout.tsx +0 -27
  107. package/app/(playground)/projects/[id]/playground/page.tsx +0 -57
  108. package/app/(playground)/projects/[id]/playground/view.tsx +0 -167
  109. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +0 -47
  110. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +0 -63
  111. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +0 -237
  112. package/app/favicon.ico +0 -0
  113. package/app/globals.css +0 -385
  114. package/app/icon.svg +0 -6
  115. package/app/layout.tsx +0 -39
  116. package/app/page.tsx +0 -11
  117. package/components/ai-elements/code-block.tsx +0 -562
  118. package/components/dashboard/agent-capabilities.tsx +0 -326
  119. package/components/dashboard/agent-identity-header.tsx +0 -50
  120. package/components/dashboard/agent-model-picker.tsx +0 -195
  121. package/components/dashboard/agent-model-selector.tsx +0 -121
  122. package/components/dashboard/agent-picker-card.tsx +0 -46
  123. package/components/dashboard/agent-studio.tsx +0 -242
  124. package/components/dashboard/agents-table.tsx +0 -48
  125. package/components/dashboard/breadcrumb.tsx +0 -70
  126. package/components/dashboard/builder-chat.tsx +0 -418
  127. package/components/dashboard/chat-shell.tsx +0 -40
  128. package/components/dashboard/client-picker.tsx +0 -63
  129. package/components/dashboard/command-snippet.tsx +0 -77
  130. package/components/dashboard/connect-dialog.tsx +0 -680
  131. package/components/dashboard/copy-button.tsx +0 -45
  132. package/components/dashboard/copy-card.tsx +0 -48
  133. package/components/dashboard/file-browser.tsx +0 -161
  134. package/components/dashboard/hint.tsx +0 -22
  135. package/components/dashboard/inference-mode.tsx +0 -15
  136. package/components/dashboard/manual-refresh-button.tsx +0 -52
  137. package/components/dashboard/markdown.tsx +0 -31
  138. package/components/dashboard/mcp-install-panel.tsx +0 -196
  139. package/components/dashboard/mission-graph.tsx +0 -262
  140. package/components/dashboard/nav-tabs.tsx +0 -86
  141. package/components/dashboard/polpo-chat.tsx +0 -468
  142. package/components/dashboard/project-copilot.tsx +0 -253
  143. package/components/dashboard/sdk-snippet-panel.tsx +0 -194
  144. package/components/dashboard/section-header.tsx +0 -37
  145. package/components/dashboard/settings-form.tsx +0 -2477
  146. package/components/dashboard/sidebar.tsx +0 -385
  147. package/components/dashboard/skeletons.tsx +0 -1054
  148. package/components/dashboard/skills-install-wizard.tsx +0 -294
  149. package/components/dashboard/swarm-runs.tsx +0 -316
  150. package/components/dashboard/tab-toggle.tsx +0 -35
  151. package/components/dashboard/task-studio.tsx +0 -224
  152. package/components/dashboard/top-header.tsx +0 -203
  153. package/components/dashboard/webhook-deliveries.tsx +0 -234
  154. package/components/icons/coding-agents.tsx +0 -151
  155. package/components/json-ld.tsx +0 -8
  156. package/components/providers.tsx +0 -25
  157. package/components/ui/badge.tsx +0 -52
  158. package/components/ui/button.tsx +0 -60
  159. package/components/ui/card.tsx +0 -103
  160. package/components/ui/chart.tsx +0 -356
  161. package/components/ui/command.tsx +0 -196
  162. package/components/ui/dialog.tsx +0 -157
  163. package/components/ui/input-group.tsx +0 -158
  164. package/components/ui/input.tsx +0 -20
  165. package/components/ui/multi-select.tsx +0 -161
  166. package/components/ui/popover.tsx +0 -90
  167. package/components/ui/select.tsx +0 -201
  168. package/components/ui/separator.tsx +0 -25
  169. package/components/ui/sheet.tsx +0 -135
  170. package/components/ui/skeleton.tsx +0 -13
  171. package/components/ui/tabs.tsx +0 -56
  172. package/components/ui/textarea.tsx +0 -18
  173. package/components/ui/tooltip.tsx +0 -66
  174. package/hooks/use-desktop-sidebar.tsx +0 -50
  175. package/hooks/use-mobile-sidebar.tsx +0 -37
  176. package/index.ts +0 -17
  177. package/lib/api.ts +0 -194
  178. package/lib/builder-context.ts +0 -60
  179. package/lib/data-client.ts +0 -68
  180. package/lib/get-query-client.ts +0 -25
  181. package/lib/polpo-client.tsx +0 -49
  182. package/lib/tool-catalog.ts +0 -234
  183. package/lib/use-event-catalog.ts +0 -28
  184. package/lib/utils.ts +0 -6
@@ -1,137 +0,0 @@
1
- import type { Task } from "@polpo-ai/core";
2
- import { ManualRefreshButton } from "../../../../../../components/dashboard/manual-refresh-button";
3
- import type { BlueprintContext } from "./blueprint";
4
-
5
- const statusColor: Record<string, string> = {
6
- done: "bg-brand",
7
- in_progress: "bg-foreground animate-pulse",
8
- pending: "bg-muted-foreground/30",
9
- failed: "bg-destructive",
10
- review: "bg-yellow-500",
11
- assigned: "bg-muted-foreground/50",
12
- draft: "bg-muted-foreground/20",
13
- awaiting_approval: "bg-yellow-500",
14
- };
15
-
16
- export default function TaskOverviewView({
17
- task,
18
- blueprint,
19
- }: {
20
- task: Task | null;
21
- blueprint?: BlueprintContext;
22
- }) {
23
- if (!task) {
24
- return (
25
- <div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
26
- Task not found.
27
- </div>
28
- );
29
- }
30
-
31
- return (
32
- <div>
33
- {blueprint && (
34
- <div className="mt-4 rounded-md border border-border bg-muted/40 px-4 py-3 text-xs">
35
- <p className="font-medium text-foreground">
36
- Blueprint task
37
- {blueprint.missionStatus === "recurring" && " — recurring mission"}
38
- {blueprint.missionStatus === "draft" && " — draft mission"}
39
- </p>
40
- <p className="mt-1 text-muted-foreground">
41
- This is the template definition of a task from mission{" "}
42
- <span className="font-mono">{blueprint.missionName}</span>. It isn&rsquo;t a real run —
43
- a fresh task instance is created each time the mission is executed
44
- {blueprint.missionStatus === "recurring" ? " on its schedule" : ""}. Status,
45
- retries, expectations and outputs only fill in once an instance starts.
46
- </p>
47
- </div>
48
- )}
49
-
50
- {/* Header */}
51
- <div className={blueprint ? "mt-4 flex items-start justify-between gap-4" : "flex items-start justify-between gap-4"}>
52
- <div className="flex items-center gap-3">
53
- <span className={`h-2.5 w-2.5 rounded-full ${statusColor[task.status ?? "pending"]}`} />
54
- <h2 className="text-lg font-extrabold tracking-tight">{task.title}</h2>
55
- </div>
56
- <ManualRefreshButton className="mt-1 shrink-0" />
57
- </div>
58
- <p className="mt-2 text-sm text-muted-foreground">{task.description}</p>
59
-
60
- {/* Meta */}
61
- <div className="mt-6 grid grid-cols-2 gap-3 sm:gap-4 sm:grid-cols-4">
62
- {[
63
- ["Agent", task.assignTo ?? "\u2014"],
64
- ["Status", task.status ?? "\u2014"],
65
- ["Phase", task.phase ?? "\u2014"],
66
- ["Retries", `${task.retries ?? 0}/${task.maxRetries ?? 3}`],
67
- ].map(([label, value]) => (
68
- <div key={label} className="border border-border bg-card p-4">
69
- <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">{label}</p>
70
- <p className="mt-1 text-sm font-medium">{value}</p>
71
- </div>
72
- ))}
73
- </div>
74
-
75
- {/* Expectations */}
76
- {task.expectations && task.expectations.length > 0 && (
77
- <section className="mt-8">
78
- <h3 className="text-sm font-semibold">Expectations</h3>
79
- <div className="mt-3 border border-border overflow-hidden">
80
- {task.expectations.map((exp, i) => (
81
- <div key={i} className="flex items-center justify-between border-b border-border last:border-0 px-4 py-3">
82
- <div>
83
- <span className="font-mono text-xs">{exp.type}</span>
84
- <p className="mt-0.5 text-[11px] text-muted-foreground">
85
- {"command" in exp ? exp.command : "paths" in exp ? (exp.paths as string[]).join(", ") : ""}
86
- </p>
87
- </div>
88
- </div>
89
- ))}
90
- </div>
91
- </section>
92
- )}
93
-
94
- {/* Expected outcomes */}
95
- {task.expectedOutcomes && task.expectedOutcomes.length > 0 && (
96
- <section className="mt-8">
97
- <h3 className="text-sm font-semibold">Expected outcomes</h3>
98
- <div className="mt-3 border border-border overflow-hidden">
99
- {task.expectedOutcomes.map((o, i) => (
100
- <div key={i} className="border-b border-border last:border-0 px-4 py-3">
101
- <div className="flex items-center gap-2">
102
- <span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{o.type}</span>
103
- <span className="text-sm font-medium">{o.label}</span>
104
- </div>
105
- {o.description && <p className="mt-1 text-xs text-muted-foreground">{o.description}</p>}
106
- </div>
107
- ))}
108
- </div>
109
- </section>
110
- )}
111
-
112
- {/* Config */}
113
- <section className="mt-8">
114
- <h3 className="text-sm font-semibold">Details</h3>
115
- <div className="mt-3 border border-border bg-card overflow-hidden overflow-x-auto">
116
- <table className="w-full text-sm min-w-[320px]">
117
- <tbody>
118
- {[
119
- ["Mission", task.missionId ?? "\u2014"],
120
- ["Dependencies", task.dependsOn?.join(", ") || "none"],
121
- ["Side effects", task.sideEffects ? "yes" : "no"],
122
- ["Session", task.sessionId ?? "\u2014"],
123
- ["Created", task.createdAt ?? "\u2014"],
124
- ["Updated", task.updatedAt ?? "\u2014"],
125
- ].map(([label, value]) => (
126
- <tr key={label} className="border-b border-border last:border-0">
127
- <td className="px-4 py-2.5 text-xs text-muted-foreground w-32">{label}</td>
128
- <td className="px-4 py-2.5 text-xs font-mono">{value}</td>
129
- </tr>
130
- ))}
131
- </tbody>
132
- </table>
133
- </div>
134
- </section>
135
- </div>
136
- );
137
- }
@@ -1,40 +0,0 @@
1
- import { Suspense } from "react";
2
- import { dataApi } from "../../../../../lib/api";
3
- import type { Task, Mission } from "@polpo-ai/core";
4
- import { TasksListSkeleton } from "../../../../../components/dashboard/skeletons";
5
- import TasksView from "./view";
6
-
7
- async function TasksData({ id }: { id: string }) {
8
- let tasks: Task[] = [];
9
- let missions: Mission[] = [];
10
-
11
- try {
12
- const [tasksRes, missionsRes] = await Promise.all([
13
- dataApi<{ ok: boolean; data: Task[] }>(id, "/v1/tasks").catch(() => ({
14
- ok: false,
15
- data: [] as Task[],
16
- })),
17
- dataApi<{ ok: boolean; data: Mission[] }>(id, "/v1/missions").catch(
18
- () => ({ ok: false, data: [] as Mission[] }),
19
- ),
20
- ]);
21
- tasks = tasksRes.data ?? [];
22
- missions = missionsRes.data ?? [];
23
- } catch {}
24
-
25
- return <TasksView initialTasks={tasks} initialMissions={missions} />;
26
- }
27
-
28
- export default async function TasksPage({
29
- params,
30
- }: {
31
- params: Promise<{ id: string }>;
32
- }) {
33
- const { id } = await params;
34
-
35
- return (
36
- <Suspense fallback={<TasksListSkeleton />}>
37
- <TasksData id={id} />
38
- </Suspense>
39
- );
40
- }
@@ -1,421 +0,0 @@
1
- "use client";
2
-
3
- import { useQuery } from "@tanstack/react-query";
4
- import { useParams } from "next/navigation";
5
- import { useState, useMemo } from "react";
6
- import Link from "next/link";
7
- import { ChevronRight, RefreshCw, X, ListFilter, Search } from "lucide-react";
8
- // Mixed imports — SDK 0.7.9 Task is missing `missionId` (present in core).
9
- // Mission from SDK is fine. Track in OSS issue: SDK types should re-export from core.
10
- import type { Task } from "@polpo-ai/core";
11
- import type { Mission } from "@polpo-ai/sdk";
12
- import { usePolpoClient } from "../../../../../lib/polpo-client";
13
- import { Button } from "../../../../../components/ui/button";
14
- import { Badge } from "../../../../../components/ui/badge";
15
- import { Popover, PopoverContent, PopoverTrigger } from "../../../../../components/ui/popover";
16
- import {
17
- Command, CommandInput, CommandList, CommandEmpty,
18
- CommandGroup, CommandItem,
19
- } from "../../../../../components/ui/command";
20
-
21
- const ALL_STATUSES = [
22
- "draft", "pending", "awaiting_approval", "assigned",
23
- "in_progress", "review", "done", "failed",
24
- ] as const;
25
-
26
- const statusColor: Record<string, string> = {
27
- done: "bg-brand",
28
- in_progress: "bg-foreground animate-pulse",
29
- pending: "bg-muted-foreground/30",
30
- failed: "bg-destructive",
31
- review: "bg-yellow-500",
32
- assigned: "bg-muted-foreground/50",
33
- draft: "bg-muted-foreground/20",
34
- awaiting_approval: "bg-yellow-500",
35
- };
36
-
37
- const LIVE_STATUSES = new Set(["pending", "assigned", "in_progress", "review", "awaiting_approval"]);
38
-
39
- function timeAgo(date?: string) {
40
- if (!date) return "\u2014";
41
- const diff = Date.now() - new Date(date).getTime();
42
- const min = Math.floor(diff / 60000);
43
- if (min < 60) return `${min}m ago`;
44
- const hours = Math.floor(min / 60);
45
- if (hours < 24) return `${hours}h ago`;
46
- return `${Math.floor(hours / 24)}d ago`;
47
- }
48
-
49
- function duration(ms?: number) {
50
- if (!ms) return "\u2014";
51
- const s = Math.floor(ms / 1000);
52
- if (s < 60) return `${s}s`;
53
- const m = Math.floor(s / 60);
54
- return `${m}m ${s % 60}s`;
55
- }
56
-
57
- export default function TasksView({
58
- initialTasks,
59
- initialMissions,
60
- }: {
61
- initialTasks: Task[];
62
- initialMissions: Mission[];
63
- }) {
64
- const { id } = useParams<{ id: string }>();
65
- const [filterStatus, setFilterStatus] = useState<string | null>(null);
66
- const [filterMission, setFilterMission] = useState<string | null>(null);
67
- const [filterAgent, setFilterAgent] = useState<string | null>(null);
68
- const [search, setSearch] = useState("");
69
- const [page, setPage] = useState(0);
70
- const [statusOpen, setStatusOpen] = useState(false);
71
- const [missionOpen, setMissionOpen] = useState(false);
72
- const [agentOpen, setAgentOpen] = useState(false);
73
- const PER_PAGE = 15;
74
-
75
- const polpo = usePolpoClient(id);
76
- const { data: tasks = [], isFetching, refetch } = useQuery({
77
- queryKey: ["tasks", id],
78
- queryFn: () => polpo.getTasks() as unknown as Promise<Task[]>,
79
- initialData: initialTasks,
80
- refetchInterval: (query) => {
81
- const rows = query.state.data ?? [];
82
- return rows.some((task) => LIVE_STATUSES.has(task.status)) ? 5000 : false;
83
- },
84
- });
85
-
86
- const { data: missions = [] } = useQuery({
87
- queryKey: ["missions", id],
88
- queryFn: () => polpo.getMissions(),
89
- initialData: initialMissions,
90
- });
91
-
92
- const activeStatuses = useMemo(
93
- () => [...new Set(tasks.map(t => t.status).filter(Boolean))].sort(
94
- (a, b) => ALL_STATUSES.indexOf(a as typeof ALL_STATUSES[number]) - ALL_STATUSES.indexOf(b as typeof ALL_STATUSES[number])
95
- ),
96
- [tasks]
97
- );
98
-
99
- const taskMissionIds = useMemo(
100
- () => [...new Set(tasks.map(t => t.missionId).filter(Boolean))] as string[],
101
- [tasks]
102
- );
103
-
104
- const activeAgents = useMemo(
105
- () => [...new Set(tasks.map(t => t.assignTo).filter(Boolean))].sort() as string[],
106
- [tasks]
107
- );
108
-
109
- const missionMap = useMemo(() => {
110
- const map = new Map<string, string>();
111
- for (const m of missions) map.set(m.id, m.name ?? m.id.slice(0, 8));
112
- return map;
113
- }, [missions]);
114
-
115
- const filtered = useMemo(() => {
116
- let result = tasks;
117
- if (filterStatus) result = result.filter(t => t.status === filterStatus);
118
- if (filterMission === "__none__") result = result.filter(t => !t.missionId);
119
- else if (filterMission) result = result.filter(t => t.missionId === filterMission);
120
- if (filterAgent) result = result.filter(t => t.assignTo === filterAgent);
121
- if (search) {
122
- const q = search.toLowerCase();
123
- result = result.filter(t =>
124
- (t.title ?? "").toLowerCase().includes(q) ||
125
- (t.assignTo ?? "").toLowerCase().includes(q) ||
126
- t.id.toLowerCase().includes(q)
127
- );
128
- }
129
- return result;
130
- }, [tasks, filterStatus, filterMission, filterAgent, search]);
131
-
132
- const hasFilters = filterStatus !== null || filterMission !== null || filterAgent !== null;
133
- const paged = filtered.slice(page * PER_PAGE, (page + 1) * PER_PAGE);
134
- const totalPages = Math.ceil(filtered.length / PER_PAGE);
135
-
136
- return (
137
- <div>
138
- {/* Header */}
139
- <div>
140
- <h2 className="text-lg font-semibold tracking-tight">Tasks</h2>
141
- <p className="mt-1 text-xs text-muted-foreground">{tasks.length} total</p>
142
- </div>
143
-
144
- {/* Filters + Search */}
145
- {tasks.length > 0 && (
146
- <div className="mt-3 flex items-center gap-2 flex-wrap">
147
- {/* Search */}
148
- <div className="relative">
149
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
150
- <input
151
- type="text"
152
- value={search}
153
- onChange={(e) => { setSearch(e.target.value); setPage(0); }}
154
- placeholder="Search..."
155
- className="w-48 border border-border bg-transparent pl-9 pr-3 py-1.5 text-xs placeholder:text-muted-foreground/40 focus:border-foreground/30 focus:outline-none transition-colors"
156
- />
157
- </div>
158
-
159
- {/* Status filter */}
160
- <Popover open={statusOpen} onOpenChange={setStatusOpen}>
161
- <PopoverTrigger
162
- render={
163
- <Button variant="outline" size="sm" data-testid="filter-status">
164
- <ListFilter className="h-3 w-3" />
165
- {filterStatus ? (
166
- <>
167
- Status: <Badge variant="secondary" className="ml-1 font-normal">{filterStatus.replace(/_/g, " ")}</Badge>
168
- </>
169
- ) : (
170
- "Status"
171
- )}
172
- </Button>
173
- }
174
- />
175
- <PopoverContent className="w-52 p-0" align="start">
176
- <Command>
177
- <CommandInput placeholder="Search status..." />
178
- <CommandList>
179
- <CommandEmpty>No status found.</CommandEmpty>
180
- <CommandGroup>
181
- {activeStatuses.map(s => (
182
- <CommandItem
183
- key={s}
184
- value={s}
185
- data-checked={filterStatus === s || undefined}
186
- onSelect={() => {
187
- setFilterStatus(filterStatus === s ? null : s);
188
- setStatusOpen(false);
189
- }}
190
- >
191
- <span className={`h-2 w-2 rounded-full shrink-0 ${statusColor[s]}`} />
192
- {s.replace(/_/g, " ")}
193
- </CommandItem>
194
- ))}
195
- </CommandGroup>
196
- </CommandList>
197
- </Command>
198
- </PopoverContent>
199
- </Popover>
200
-
201
- {/* Mission filter */}
202
- {taskMissionIds.length > 0 && (
203
- <Popover open={missionOpen} onOpenChange={setMissionOpen}>
204
- <PopoverTrigger
205
- render={
206
- <Button variant="outline" size="sm" data-testid="filter-mission">
207
- <ListFilter className="h-3 w-3" />
208
- {filterMission ? (
209
- <>
210
- Mission: <Badge variant="secondary" className="ml-1 font-normal">
211
- {filterMission === "__none__" ? "No mission" : (missionMap.get(filterMission) ?? filterMission.slice(0, 8))}
212
- </Badge>
213
- </>
214
- ) : (
215
- "Mission"
216
- )}
217
- </Button>
218
- }
219
- />
220
- <PopoverContent className="w-56 p-0" align="start">
221
- <Command>
222
- <CommandInput placeholder="Search mission..." />
223
- <CommandList>
224
- <CommandEmpty>No mission found.</CommandEmpty>
225
- <CommandGroup>
226
- <CommandItem
227
- value="__none__"
228
- data-checked={filterMission === "__none__" || undefined}
229
- onSelect={() => {
230
- setFilterMission(filterMission === "__none__" ? null : "__none__");
231
- setMissionOpen(false);
232
- }}
233
- >
234
- <span className="text-muted-foreground">No mission</span>
235
- </CommandItem>
236
- {taskMissionIds.map(mid => (
237
- <CommandItem
238
- key={mid}
239
- value={missionMap.get(mid) ?? mid}
240
- data-checked={filterMission === mid || undefined}
241
- onSelect={() => {
242
- setFilterMission(filterMission === mid ? null : mid);
243
- setMissionOpen(false);
244
- }}
245
- >
246
- {missionMap.get(mid) ?? mid.slice(0, 8)}
247
- </CommandItem>
248
- ))}
249
- </CommandGroup>
250
- </CommandList>
251
- </Command>
252
- </PopoverContent>
253
- </Popover>
254
- )}
255
-
256
- {/* Agent filter */}
257
- {activeAgents.length > 1 && (
258
- <Popover open={agentOpen} onOpenChange={setAgentOpen}>
259
- <PopoverTrigger
260
- render={
261
- <Button variant="outline" size="sm" data-testid="filter-agent">
262
- <ListFilter className="h-3 w-3" />
263
- {filterAgent ? (
264
- <>
265
- Agent: <Badge variant="secondary" className="ml-1 font-normal">{filterAgent}</Badge>
266
- </>
267
- ) : (
268
- "Agent"
269
- )}
270
- </Button>
271
- }
272
- />
273
- <PopoverContent className="w-52 p-0" align="start">
274
- <Command>
275
- <CommandInput placeholder="Search agent..." />
276
- <CommandList>
277
- <CommandEmpty>No agent found.</CommandEmpty>
278
- <CommandGroup>
279
- {activeAgents.map(a => (
280
- <CommandItem
281
- key={a}
282
- value={a}
283
- data-checked={filterAgent === a || undefined}
284
- onSelect={() => {
285
- setFilterAgent(filterAgent === a ? null : a);
286
- setAgentOpen(false);
287
- setPage(0);
288
- }}
289
- >
290
- {a}
291
- </CommandItem>
292
- ))}
293
- </CommandGroup>
294
- </CommandList>
295
- </Command>
296
- </PopoverContent>
297
- </Popover>
298
- )}
299
-
300
- {/* Active filter badges + clear */}
301
- {hasFilters && (
302
- <>
303
- <span className="text-xs text-muted-foreground">
304
- {filtered.length} of {tasks.length}
305
- </span>
306
- <Button
307
- variant="ghost"
308
- size="xs"
309
- onClick={() => { setFilterStatus(null); setFilterMission(null); setFilterAgent(null); setSearch(""); setPage(0); }}
310
- data-testid="clear-filters"
311
- >
312
- <X className="h-3 w-3" />
313
- Clear
314
- </Button>
315
- </>
316
- )}
317
-
318
- {/* Refresh */}
319
- <button
320
- onClick={() => refetch()}
321
- disabled={isFetching}
322
- data-testid="refresh-btn"
323
- className="ml-auto inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
324
- >
325
- <RefreshCw className={`h-3 w-3 ${isFetching ? "animate-spin" : ""}`} />
326
- Refresh
327
- </button>
328
- </div>
329
- )}
330
-
331
- {/* Table */}
332
- {filtered.length > 0 ? (
333
- <div data-testid="tasks-table" className="mt-4 border border-border overflow-hidden overflow-x-auto min-w-[600px]">
334
- <div className="flex items-center border-b border-border bg-secondary/50 text-left text-xs font-medium text-muted-foreground">
335
- <span className="px-4 py-2.5 w-28">Status</span>
336
- <span className="px-4 py-2.5 flex-1">Task</span>
337
- <span className="px-4 py-2.5 w-24">Agent</span>
338
- <span className="px-4 py-2.5 w-20">Duration</span>
339
- <span className="px-4 py-2.5 w-16">Retries</span>
340
- <span className="px-4 py-2.5 w-20 text-right">Updated</span>
341
- <span className="w-8" />
342
- </div>
343
- {paged.map((task) => (
344
- <Link
345
- key={task.id}
346
- href={`/projects/${id}/tasks/${task.id}`}
347
- data-testid={`task-row-${task.id}`}
348
- className="flex items-center border-b border-border last:border-0 hover:bg-secondary/30 transition-colors group"
349
- >
350
- <span className="px-4 py-3 w-28">
351
- <span className="inline-flex items-center gap-1.5">
352
- <span className={`h-2 w-2 rounded-full ${statusColor[task.status ?? "pending"]}`} />
353
- <span className="text-xs text-muted-foreground">{task.status}</span>
354
- </span>
355
- </span>
356
- <span className="px-4 py-3 flex-1">
357
- <span className="text-sm font-medium block">{task.title}</span>
358
- {task.missionId && (
359
- <span className="block mt-0.5 text-[11px] text-muted-foreground/50">
360
- mission: {missionMap.get(task.missionId) ?? task.missionId.slice(0, 8)}
361
- </span>
362
- )}
363
- {task.dependsOn && task.dependsOn.length > 0 && (
364
- <span className="block mt-0.5 text-[11px] text-muted-foreground/50">
365
- depends on: {task.dependsOn.join(", ")}
366
- </span>
367
- )}
368
- </span>
369
- <span className="px-4 py-3 w-24 font-mono text-xs text-muted-foreground">
370
- {task.assignTo ?? "\u2014"}
371
- </span>
372
- <span className="px-4 py-3 w-20 font-mono text-xs text-muted-foreground">
373
- {duration(task.result?.duration)}
374
- </span>
375
- <span className="px-4 py-3 w-16 text-xs text-muted-foreground">
376
- {task.retries ?? 0}/{task.maxRetries ?? 3}
377
- </span>
378
- <span className="px-4 py-3 w-20 text-right text-xs text-muted-foreground">
379
- {timeAgo(task.updatedAt)}
380
- </span>
381
- <span className="px-2 py-3 w-8">
382
- <ChevronRight className="h-3.5 w-3.5 text-muted-foreground/30 group-hover:text-muted-foreground transition-colors" />
383
- </span>
384
- </Link>
385
- ))}
386
- {totalPages > 1 && (
387
- <div className="flex items-center justify-between px-4 py-2.5 border-t border-border">
388
- <span className="text-xs text-muted-foreground">
389
- {page * PER_PAGE + 1}–{Math.min((page + 1) * PER_PAGE, filtered.length)} of {filtered.length}
390
- </span>
391
- <div className="flex items-center gap-1">
392
- <button
393
- onClick={() => setPage((p) => Math.max(0, p - 1))}
394
- disabled={page === 0}
395
- className="px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground border border-border transition-colors disabled:opacity-30 disabled:pointer-events-none"
396
- >
397
- Prev
398
- </button>
399
- <button
400
- onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
401
- disabled={page >= totalPages - 1}
402
- className="px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground border border-border transition-colors disabled:opacity-30 disabled:pointer-events-none"
403
- >
404
- Next
405
- </button>
406
- </div>
407
- </div>
408
- )}
409
- </div>
410
- ) : tasks.length > 0 ? (
411
- <div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
412
- {search ? "No tasks matching your search." : "No tasks match the current filters."}
413
- </div>
414
- ) : (
415
- <div data-testid="tasks-empty" className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
416
- No tasks yet.
417
- </div>
418
- )}
419
- </div>
420
- );
421
- }