From aadec0b96c3aae434c78a20d0c3c07c4207e5c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Tue, 12 May 2026 10:53:43 +0800 Subject: [PATCH] feat: WorkflowList expandable cards with static graph (#198) - Workflow cards click to expand/collapse - Lazy-load descriptor on first expand - Static WorkflowGraph (all nodes default state, no highlighting) - Show description, version count, hash - Fix WorkflowSummary type to match actual API response --- packages/workflow-dashboard/src/api.ts | 20 +- .../src/components/workflow-list.tsx | 228 ++++++++++++++++-- 2 files changed, 221 insertions(+), 27 deletions(-) diff --git a/packages/workflow-dashboard/src/api.ts b/packages/workflow-dashboard/src/api.ts index 68beda0..ab910b4 100644 --- a/packages/workflow-dashboard/src/api.ts +++ b/packages/workflow-dashboard/src/api.ts @@ -66,8 +66,13 @@ export type AgentEndpoint = { export type WorkflowSummary = { name: string; - currentHash: string; - versions: number; + hash: string | null; + timestamp: number | null; +}; + +export type WorkflowHistoryEntry = { + hash: string; + timestamp: number; }; export type ThreadSummary = { @@ -130,7 +135,7 @@ export type WorkflowDetail = { name: string; hash: string; timestamp: number; - history: unknown[]; + history: readonly WorkflowHistoryEntry[]; descriptor: WorkflowDescriptor | null; }; @@ -147,14 +152,15 @@ export function listWorkflows(agent: string): Promise<{ workflows: WorkflowSumma return fetchJson(agentBase(agent), "/workflows"); } +export async function getWorkflowDetail(agent: string, name: string): Promise { + return fetchJson(agentBase(agent), `/workflows/${encodeURIComponent(name)}`); +} + export async function getWorkflowDescriptor( agent: string, name: string, ): Promise { - const res = await fetchJson( - agentBase(agent), - `/workflows/${encodeURIComponent(name)}`, - ); + const res = await getWorkflowDetail(agent, name); return res.descriptor; } diff --git a/packages/workflow-dashboard/src/components/workflow-list.tsx b/packages/workflow-dashboard/src/components/workflow-list.tsx index 0c32d08..0a49de3 100644 --- a/packages/workflow-dashboard/src/components/workflow-list.tsx +++ b/packages/workflow-dashboard/src/components/workflow-list.tsx @@ -1,12 +1,168 @@ -import { listWorkflows } from "../api.ts"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import type { WorkflowDetail } from "../api.ts"; +import { getWorkflowDetail, listWorkflows } from "../api.ts"; import { useFetch } from "../hooks.ts"; +import { type NodeState, WorkflowGraph } from "./workflow-graph/index.ts"; type Props = { agent: string; }; +type DetailCacheEntry = + | { status: "loading" } + | { status: "error"; message: string } + | { status: "ok"; detail: WorkflowDetail }; + +function versionCount(detail: WorkflowDetail): number { + return detail.history.length + 1; +} + +function ExpandedWorkflowBody({ + cacheEntry, + staticNodeStates, +}: { + cacheEntry: DetailCacheEntry | undefined; + staticNodeStates: Map; +}) { + if (cacheEntry === undefined || cacheEntry.status === "loading") { + return ( +

+ Loading workflow details... +

+ ); + } + + if (cacheEntry.status === "error") { + return ( +

+ {cacheEntry.message} +

+ ); + } + + const { detail } = cacheEntry; + const descriptor = detail.descriptor; + const edgeCount = descriptor !== null ? descriptor.graph.edges.length : 0; + const vc = versionCount(detail); + + return ( +
+
+

+ {detail.name} +

+

+ Hash +

+ + {detail.hash} + +
+

+ {vc} version{vc !== 1 ? "s" : ""} +

+
+

+ Description +

+

+ {descriptor !== null && descriptor.description !== "" + ? descriptor.description + : descriptor !== null + ? "—" + : "No descriptor available for this workflow version."} +

+
+ {descriptor !== null && edgeCount > 0 ? ( +
+
+ Workflow graph + + {edgeCount} edge{edgeCount === 1 ? "" : "s"} + +
+
+ +
+
+ ) : null} +
+ ); +} + export function WorkflowList({ agent }: Props) { const { status, data, error } = useFetch(() => listWorkflows(agent), [agent]); + const [expanded, setExpanded] = useState>(() => new Set()); + const [detailsByName, setDetailsByName] = useState>( + () => new Map(), + ); + + const staticNodeStates = useMemo(() => new Map(), []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: reset expansion when switching agents + useEffect(() => { + setExpanded(new Set()); + setDetailsByName(new Map()); + }, [agent]); + + const ensureDetailLoaded = useCallback( + (name: string) => { + setDetailsByName((prev) => { + const cur = prev.get(name); + if (cur !== undefined && (cur.status === "ok" || cur.status === "loading")) { + return prev; + } + return new Map(prev).set(name, { status: "loading" }); + }); + + void (async () => { + try { + const detail = await getWorkflowDetail(agent, name); + setDetailsByName((prev) => { + const next = new Map(prev); + next.set(name, { status: "ok", detail }); + return next; + }); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + setDetailsByName((prev) => { + const next = new Map(prev); + next.set(name, { status: "error", message }); + return next; + }); + } + })(); + }, + [agent], + ); + + function toggleExpanded(name: string) { + let shouldLoad = false; + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(name)) { + next.delete(name); + return next; + } + next.add(name); + shouldLoad = true; + return next; + }); + if (shouldLoad) { + ensureDetailLoaded(name); + } + } if (status === "loading") return

Loading workflows...

; @@ -21,26 +177,58 @@ export function WorkflowList({ agent }: Props) {

No workflows registered.

) : (
- {workflows.map((w) => ( -
-
- {w.name} - - {w.versions} version{w.versions !== 1 ? "s" : ""} - -
- { + const isOpen = expanded.has(w.name); + return ( +
- {w.currentHash} - -
- ))} + + {isOpen ? ( +
+ +
+ ) : null} +
+ ); + })}
)}