From 1871ef31b4ca0a4661af8a671b32f1a1de06fd91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=98=9F=E6=9C=88?=
Date: Tue, 12 May 2026 14:58:50 +0800
Subject: [PATCH] refactor(dashboard): replace vertical layout with
side-by-side graph+cards
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Change thread-detail from vertical (graph on top, cards below) to a
side-by-side layout:
- Left panel (280px, sticky): workflow graph, always visible
- Right panel (flex-1, scrollable): record cards
- Remove collapsible GraphPanel wrapper
- Graph acts as navigation (click node → scroll to card)
Refs: workflow thread 06F1NX4C9ET6HPXJAH7CWWF8MR
---
.../src/components/thread-detail.tsx | 162 +++++++++---------
1 file changed, 77 insertions(+), 85 deletions(-)
diff --git a/packages/workflow-dashboard/src/components/thread-detail.tsx b/packages/workflow-dashboard/src/components/thread-detail.tsx
index 819f2f1..b65d91e 100644
--- a/packages/workflow-dashboard/src/components/thread-detail.tsx
+++ b/packages/workflow-dashboard/src/components/thread-detail.tsx
@@ -26,53 +26,6 @@ function extractWorkflowName(records: readonly ThreadRecord[]): string | null {
return null;
}
-type GraphPanelProps = {
- descriptor: WorkflowDescriptor;
- workflowName: string | null;
- nodeStates: Map;
- onNodeClick: ((roleName: string) => void) | null;
-};
-
-function GraphPanel({ descriptor, workflowName, nodeStates, onNodeClick }: GraphPanelProps) {
- const [open, setOpen] = useState(true);
- const edgeCount = descriptor.graph.edges.length;
- return (
-
-
- {open && (
-
-
-
- )}
-
- );
-}
-
function computeNodeStates(records: readonly ThreadRecord[]): Map {
const states = new Map();
const roleRecords = records.filter(
@@ -227,46 +180,85 @@ export function ThreadDetail({ agent, threadId, onBack }: Props) {
)}
- {descriptor !== null && descriptor.graph.edges.length > 0 && (
-
- )}
+
+ {descriptor !== null && descriptor.graph.edges.length > 0 && (
+
+
+
+
+ Workflow graph
+ {workflowName !== null && (
+
+ {workflowName}
+
+ )}
+
+
+ {descriptor.graph.edges.length} edge
+ {descriptor.graph.edges.length === 1 ? "" : "s"}
+
+
+
+
+
+
+
+ )}
- {status === "loading" && !liveActive && records.length === 0 && (
-
Loading...
- )}
- {status === "error" && !liveActive && (
-
Error: {error}
- )}
- {(status === "ok" || liveActive || records.length > 0) && (
-
- {records.map((r, i) => {
- const key = `${threadId}-${i}`;
- if (r.type === "role") {
- const isFirstForRole = firstIndexByRole.get(r.role) === i;
- const flash = highlightedRole === r.role;
- return (
-
{
- if (!isFirstForRole) return;
- if (el !== null) firstCardByRoleRef.current.set(r.role, el);
- else firstCardByRoleRef.current.delete(r.role);
- }}
- >
-
-
- );
- }
- return
;
- })}
-
+
+ {status === "loading" && !liveActive && records.length === 0 && (
+
Loading...
+ )}
+ {status === "error" && !liveActive && (
+
Error: {error}
+ )}
+ {(status === "ok" || liveActive || records.length > 0) && (
+
+ {records.map((r, i) => {
+ const key = `${threadId}-${i}`;
+ if (r.type === "role") {
+ const isFirstForRole = firstIndexByRole.get(r.role) === i;
+ const flash = highlightedRole === r.role;
+ return (
+
{
+ if (!isFirstForRole) return;
+ if (el !== null) firstCardByRoleRef.current.set(r.role, el);
+ else firstCardByRoleRef.current.delete(r.role);
+ }}
+ >
+
+
+ );
+ }
+ return
;
+ })}
+
+
+ )}
- )}
+
);
}