-
{k}
-
{asText(val)}
+
+
{k}
+ {asText(val)}
))}
diff --git a/agent-team/web/src/components/TaskList.test.tsx b/agent-team/web/src/components/TaskList.test.tsx
deleted file mode 100644
index a3e0f94..0000000
--- a/agent-team/web/src/components/TaskList.test.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { fireEvent, render, screen } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
-import { TaskList } from "./TaskList";
-import type { StateResponse, Topology } from "../api";
-
-const TOPO: Topology = {
- trees: [
- { id: "core", label: "Core" },
- { id: "sdlc", label: "SDLC" },
- ],
- nodes: [
- { id: "intake", label: "Intake", agent: "coordinator", tree: "core", kind: "phase", gated: false },
- { id: "plan", label: "Plan", agent: "Claude", tree: "sdlc", kind: "phase", gated: false },
- ],
- edges: [],
-};
-
-function makeState(): StateResponse {
- return {
- ok: true,
- error: null,
- generated_at: "now",
- db_path: "x",
- tasks: [
- { thread_id: "aaaa1111", short_id: "aaaa1111", task: "remediate CVE", current_phase: "plan", status: "active", waiting: false, waiting_since: null },
- { thread_id: "bbbb2222", short_id: "bbbb2222", task: "fix login bug", current_phase: "intake", status: "parked", waiting: false, waiting_since: null },
- ],
- waiting: [],
- question_counts: {},
- summary: { active: 1, parked: 1, waiting: 0, open_questions: 0, total: 2 },
- budget: { available: false },
- nodes: {},
- };
-}
-
-describe("TaskList", () => {
- it("renders all tasks", () => {
- render(
-
{}} onClearNodeFilter={() => {}} />,
- );
- expect(screen.getByText("remediate CVE")).toBeInTheDocument();
- expect(screen.getByText("fix login bug")).toBeInTheDocument();
- });
-
- it("filters by status", () => {
- render(
- {}} onClearNodeFilter={() => {}} />,
- );
- fireEvent.change(screen.getByLabelText("Filter by status"), { target: { value: "parked" } });
- expect(screen.queryByText("remediate CVE")).not.toBeInTheDocument();
- expect(screen.getByText("fix login bug")).toBeInTheDocument();
- });
-
- it("honors a node filter (map click)", () => {
- render(
- {}} onClearNodeFilter={() => {}} />,
- );
- expect(screen.getByText("remediate CVE")).toBeInTheDocument();
- expect(screen.queryByText("fix login bug")).not.toBeInTheDocument();
- });
-
- it("fires onSelect when a row is clicked", () => {
- const onSelect = vi.fn();
- render(
- {}} />,
- );
- fireEvent.click(screen.getByText("remediate CVE"));
- expect(onSelect).toHaveBeenCalledWith("aaaa1111");
- });
-});
diff --git a/agent-team/web/src/components/TaskList.tsx b/agent-team/web/src/components/TaskList.tsx
deleted file mode 100644
index 50088c9..0000000
--- a/agent-team/web/src/components/TaskList.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { useMemo, useState } from "react";
-import type { StateResponse, TaskView, Topology } from "../api";
-
-// Mirror of the backend phase->node mapping (topology.PHASE_TO_NODE) so the list
-// can resolve a task's tree and honor a map-node filter. Identity except the P3
-// build/verify vertices.
-const PHASE_TO_NODE: Record = {
- build: "build_node",
- verify: "verify_node",
-};
-const phaseToNode = (phase: string): string => PHASE_TO_NODE[phase] ?? phase;
-
-interface Props {
- state: StateResponse | null;
- topology: Topology | null;
- selectedId: string | null;
- nodeFilter: string | null;
- onSelect: (threadId: string) => void;
- onClearNodeFilter: () => void;
-}
-
-const STATUS_OPTIONS = ["all", "active", "waiting_human", "parked", "done", "failed"];
-
-export function TaskList({
- state,
- topology,
- selectedId,
- nodeFilter,
- onSelect,
- onClearNodeFilter,
-}: Props) {
- const [query, setQuery] = useState("");
- const [status, setStatus] = useState("all");
- const [tree, setTree] = useState("all");
-
- const treeByNode = useMemo(() => {
- const m: Record = {};
- for (const n of topology?.nodes ?? []) m[n.id] = n.tree;
- return m;
- }, [topology]);
-
- const tasks = state?.tasks ?? [];
- const filtered = useMemo(() => {
- const q = query.trim().toLowerCase();
- return tasks.filter((t) => {
- if (nodeFilter && phaseToNode(t.current_phase) !== nodeFilter) return false;
- if (status !== "all" && t.status !== status) return false;
- if (tree !== "all" && treeByNode[phaseToNode(t.current_phase)] !== tree)
- return false;
- if (q && !(`${t.task} ${t.short_id}`.toLowerCase().includes(q))) return false;
- return true;
- });
- }, [tasks, query, status, tree, nodeFilter, treeByNode]);
-
- return (
-
- );
-}
-
-function TaskRow({
- task,
- selected,
- onSelect,
-}: {
- task: TaskView;
- selected: boolean;
- onSelect: (id: string) => void;
-}) {
- return (
- onSelect(task.thread_id)}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- onSelect(task.thread_id);
- }
- }}
- >
-
- {task.status.replace("_", " ")}
- {task.current_phase}
- {task.waiting && ⏳}
-
- {task.task}
- {task.short_id}
-
- );
-}
diff --git a/agent-team/web/src/components/TopBar.tsx b/agent-team/web/src/components/TopBar.tsx
index 8e872dc..2b957f2 100644
--- a/agent-team/web/src/components/TopBar.tsx
+++ b/agent-team/web/src/components/TopBar.tsx
@@ -1,3 +1,4 @@
+import { cn } from "@/lib/utils";
import type { StateResponse } from "../api";
interface Props {
@@ -6,36 +7,118 @@ interface Props {
generatedAt: string | null;
}
-const CHIPS: Array<{ key: keyof StateResponse["summary"]; label: string; cls: string }> =
- [
- { key: "active", label: "Active", cls: "active" },
- { key: "waiting", label: "Awaiting human", cls: "waiting" },
- { key: "parked", label: "Parked", cls: "parked" },
- { key: "open_questions", label: "Open questions", cls: "open" },
- { key: "total", label: "Total", cls: "total" },
- ];
+interface ChipDef {
+ key: keyof StateResponse["summary"];
+ label: string;
+ numCls: string;
+ bgCls: string;
+}
-// Top status bar: summary chips, a connection/health dot, and the snapshot clock.
+const CHIPS: ChipDef[] = [
+ {
+ key: "active",
+ label: "ACTIVE",
+ numCls: "text-status-active",
+ bgCls: "bg-status-active-bg",
+ },
+ {
+ key: "waiting",
+ label: "AWAITING HUMAN",
+ numCls: "text-status-waiting",
+ bgCls: "bg-status-waiting-bg",
+ },
+ {
+ key: "parked",
+ label: "PARKED",
+ numCls: "text-status-parked",
+ bgCls: "bg-status-parked-bg",
+ },
+ {
+ key: "open_questions",
+ label: "OPEN QUESTIONS",
+ numCls: "text-status-waiting",
+ bgCls: "bg-status-waiting-bg",
+ },
+ {
+ key: "total",
+ label: "TOTAL",
+ numCls: "text-muted-foreground",
+ bgCls: "bg-muted",
+ },
+];
+
+// Top status bar: brand, summary chips, connection health, and snapshot clock.
export function TopBar({ state, connected, generatedAt }: Props) {
const summary = state?.summary;
+ const title = connected
+ ? `Live — snapshot at ${generatedAt ?? "—"}`
+ : `Disconnected — last snapshot at ${generatedAt ?? "—"}`;
+
return (
-
-
-
-
agent-team
-
pipeline
+