fleetlens 0.13.1 → 0.13.2
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.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/build-manifest.json +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/middleware-manifest.json +5 -5
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/{0vz4~a~~hq399.js → 02xxpexzk3rf-.js} +1 -1
- package/app/apps/web/.next/static/chunks/0uyctl-m8iydb.js +1 -0
- package/app/apps/web/.next/static/chunks/0~qdw1b2uz._5.js +10 -0
- package/app/apps/web/app/parallelism/gantt-chart.tsx +8 -1
- package/app/apps/web/app/sessions/[id]/session-view/minimap.tsx +1587 -0
- package/app/apps/web/app/sessions/[id]/session-view/workflows-panel.tsx +944 -0
- package/app/apps/web/app/sessions/[id]/session-view.tsx +909 -3223
- package/app/apps/web/components/tail-mode.tsx +7 -1
- package/app/apps/web/lib/changelog-data.generated.ts +1 -1
- package/app/apps/web/lib/format.ts +17 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +4 -4
- package/dist/index.js +11 -11
- package/package.json +1 -1
- package/app/apps/web/.next/static/chunks/02bv_ejzhhzvp.js +0 -10
- package/app/apps/web/.next/static/chunks/0q8dg4txlzno0.js +0 -1
- /package/app/apps/web/.next/static/{VEfemGVAQ0xCHjj15orZU → bZs5UyzsYBqC6MFY5n-M_}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{VEfemGVAQ0xCHjj15orZU → bZs5UyzsYBqC6MFY5n-M_}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{VEfemGVAQ0xCHjj15orZU → bZs5UyzsYBqC6MFY5n-M_}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/* ------------------------------------------------------------------ */
|
|
4
|
+
/* Workflows panel */
|
|
5
|
+
/* */
|
|
6
|
+
/* A single `Workflow` tool call collapses a whole dynamic-workflow */
|
|
7
|
+
/* fan-out into one transcript row. This panel surfaces what that row */
|
|
8
|
+
/* actually did: each run's spawned-agent count, tool calls, tokens, */
|
|
9
|
+
/* phases, and progress log — the fleet work the transcript hides. */
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
|
|
12
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
13
|
+
import {
|
|
14
|
+
ChevronDown,
|
|
15
|
+
ChevronRight,
|
|
16
|
+
Clock,
|
|
17
|
+
Workflow as WorkflowIcon,
|
|
18
|
+
Wrench,
|
|
19
|
+
X,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import type { WorkflowRun } from "@claude-lens/parser";
|
|
22
|
+
import { formatGap, formatOffset, formatTokens } from "@/lib/format";
|
|
23
|
+
import { shortenToolName } from "../turn-steps";
|
|
24
|
+
import { workflowColor } from "./colors";
|
|
25
|
+
import { WfMiniStat, SectionLabel } from "./workflows-shared";
|
|
26
|
+
|
|
27
|
+
export function WorkflowsPanel({
|
|
28
|
+
workflows,
|
|
29
|
+
spawnedAgentCount,
|
|
30
|
+
focusRunId,
|
|
31
|
+
selectedAgentKey,
|
|
32
|
+
onOpenAgent,
|
|
33
|
+
onJumpToParent,
|
|
34
|
+
onClearFocus,
|
|
35
|
+
}: {
|
|
36
|
+
workflows: WorkflowRun[];
|
|
37
|
+
spawnedAgentCount: number;
|
|
38
|
+
focusRunId?: string | null;
|
|
39
|
+
selectedAgentKey?: string | null;
|
|
40
|
+
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
41
|
+
onJumpToParent: (toolUseId?: string) => void;
|
|
42
|
+
onClearFocus?: () => void;
|
|
43
|
+
}) {
|
|
44
|
+
const totalTokens = workflows.reduce((n, w) => n + w.totalTokens, 0);
|
|
45
|
+
const totalTools = workflows.reduce((n, w) => n + w.toolCallCount, 0);
|
|
46
|
+
return (
|
|
47
|
+
<section
|
|
48
|
+
style={{
|
|
49
|
+
marginBottom: 16,
|
|
50
|
+
border: "1px solid var(--af-border-subtle)",
|
|
51
|
+
borderRadius: 10,
|
|
52
|
+
background: "var(--af-surface)",
|
|
53
|
+
overflow: "hidden",
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
display: "flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
gap: 10,
|
|
61
|
+
flexWrap: "wrap",
|
|
62
|
+
padding: "10px 14px",
|
|
63
|
+
borderBottom: "1px solid var(--af-border-subtle)",
|
|
64
|
+
background: "var(--af-surface-subtle, rgba(234,88,12,0.04))",
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<span
|
|
68
|
+
style={{
|
|
69
|
+
display: "inline-flex",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
gap: 7,
|
|
72
|
+
fontSize: 13,
|
|
73
|
+
fontWeight: 600,
|
|
74
|
+
color: "var(--af-text)",
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<WorkflowIcon size={14} style={{ color: "#EA580C" }} />
|
|
78
|
+
Workflows
|
|
79
|
+
</span>
|
|
80
|
+
<span
|
|
81
|
+
style={{
|
|
82
|
+
fontSize: 11.5,
|
|
83
|
+
color: "var(--af-text-tertiary)",
|
|
84
|
+
fontFamily: "var(--font-mono)",
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{workflows.length} run{workflows.length === 1 ? "" : "s"} ·{" "}
|
|
88
|
+
{spawnedAgentCount.toLocaleString()} agents · {totalTools.toLocaleString()} tool calls ·{" "}
|
|
89
|
+
{formatTokens(totalTokens)} tok orchestrated
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
93
|
+
{workflows.map((w) => (
|
|
94
|
+
<WorkflowCard
|
|
95
|
+
key={w.runId}
|
|
96
|
+
w={w}
|
|
97
|
+
focused={focusRunId === w.runId}
|
|
98
|
+
selectedAgentKey={selectedAgentKey}
|
|
99
|
+
onOpenAgent={onOpenAgent}
|
|
100
|
+
onJumpToParent={onJumpToParent}
|
|
101
|
+
onClearFocus={onClearFocus}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</section>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function WorkflowCard({
|
|
110
|
+
w,
|
|
111
|
+
focused = false,
|
|
112
|
+
selectedAgentKey,
|
|
113
|
+
onOpenAgent,
|
|
114
|
+
onJumpToParent,
|
|
115
|
+
onClearFocus,
|
|
116
|
+
}: {
|
|
117
|
+
w: WorkflowRun;
|
|
118
|
+
focused?: boolean;
|
|
119
|
+
selectedAgentKey?: string | null;
|
|
120
|
+
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
121
|
+
onJumpToParent: (toolUseId?: string) => void;
|
|
122
|
+
onClearFocus?: () => void;
|
|
123
|
+
}) {
|
|
124
|
+
const [open, setOpen] = useState(focused);
|
|
125
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
126
|
+
// Focused via a minimap lane click — expand and scroll this run into view.
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (focused) {
|
|
129
|
+
setOpen(true);
|
|
130
|
+
cardRef.current?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
131
|
+
}
|
|
132
|
+
}, [focused]);
|
|
133
|
+
const color = workflowColor(w.status);
|
|
134
|
+
const dur = w.durationMs !== undefined ? formatGap(w.durationMs) : "—";
|
|
135
|
+
const startOff = formatOffset(w.startTOffsetMs);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
ref={cardRef}
|
|
140
|
+
style={{
|
|
141
|
+
borderTop: "1px solid var(--af-border-subtle)",
|
|
142
|
+
scrollMarginTop: 120,
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
{/* Collapsed header — click to expand */}
|
|
146
|
+
<button
|
|
147
|
+
onClick={() => {
|
|
148
|
+
setOpen((v) => !v);
|
|
149
|
+
// Any toggle is the user taking over from the timeline jump — drop the
|
|
150
|
+
// focus highlight so it doesn't linger on a card they've moved past.
|
|
151
|
+
onClearFocus?.();
|
|
152
|
+
}}
|
|
153
|
+
style={{
|
|
154
|
+
width: "100%",
|
|
155
|
+
display: "flex",
|
|
156
|
+
alignItems: "center",
|
|
157
|
+
gap: 10,
|
|
158
|
+
padding: "10px 14px",
|
|
159
|
+
background: "transparent",
|
|
160
|
+
border: "none",
|
|
161
|
+
cursor: "pointer",
|
|
162
|
+
textAlign: "left",
|
|
163
|
+
color: "inherit",
|
|
164
|
+
flexWrap: "wrap",
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>
|
|
168
|
+
{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
169
|
+
</span>
|
|
170
|
+
<span
|
|
171
|
+
style={{
|
|
172
|
+
fontFamily: "var(--font-mono)",
|
|
173
|
+
fontSize: 13,
|
|
174
|
+
fontWeight: 600,
|
|
175
|
+
color: focused ? "#EA580C" : "var(--af-text)",
|
|
176
|
+
background: focused ? "rgba(234,88,12,0.16)" : "transparent",
|
|
177
|
+
padding: "2px 8px",
|
|
178
|
+
borderRadius: 6,
|
|
179
|
+
transition: "background 0.4s ease, color 0.4s ease",
|
|
180
|
+
minWidth: 0,
|
|
181
|
+
overflow: "hidden",
|
|
182
|
+
textOverflow: "ellipsis",
|
|
183
|
+
whiteSpace: "nowrap",
|
|
184
|
+
}}
|
|
185
|
+
title={w.name}
|
|
186
|
+
>
|
|
187
|
+
{w.name}
|
|
188
|
+
</span>
|
|
189
|
+
<span
|
|
190
|
+
style={{
|
|
191
|
+
fontSize: 9.5,
|
|
192
|
+
fontWeight: 600,
|
|
193
|
+
padding: "2px 7px",
|
|
194
|
+
borderRadius: 100,
|
|
195
|
+
background: color,
|
|
196
|
+
color: "#fff",
|
|
197
|
+
textTransform: "uppercase",
|
|
198
|
+
letterSpacing: "0.04em",
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{w.status}
|
|
202
|
+
</span>
|
|
203
|
+
<span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 12 }}>
|
|
204
|
+
<span
|
|
205
|
+
style={{
|
|
206
|
+
fontSize: 12,
|
|
207
|
+
fontWeight: 700,
|
|
208
|
+
padding: "2px 10px",
|
|
209
|
+
borderRadius: 100,
|
|
210
|
+
background: "rgba(234,88,12,0.14)",
|
|
211
|
+
color: "#EA580C",
|
|
212
|
+
fontFamily: "var(--font-mono)",
|
|
213
|
+
whiteSpace: "nowrap",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{w.agentCount} agents
|
|
217
|
+
</span>
|
|
218
|
+
<WfMiniStat icon={<Wrench size={11} />} value={`${w.toolCallCount.toLocaleString()}`} />
|
|
219
|
+
<WfMiniStat value={`${formatTokens(w.totalTokens)} tok`} />
|
|
220
|
+
<WfMiniStat icon={<Clock size={11} />} value={dur} />
|
|
221
|
+
</span>
|
|
222
|
+
</button>
|
|
223
|
+
|
|
224
|
+
{/* Expanded detail */}
|
|
225
|
+
{open && (
|
|
226
|
+
<div style={{ padding: "0 14px 14px 38px", display: "flex", flexDirection: "column", gap: 12 }}>
|
|
227
|
+
{w.description && (
|
|
228
|
+
<div style={{ fontSize: 12.5, color: "var(--af-text-secondary)", lineHeight: 1.5 }}>
|
|
229
|
+
{w.description}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
<div
|
|
234
|
+
style={{
|
|
235
|
+
display: "flex",
|
|
236
|
+
gap: 14,
|
|
237
|
+
flexWrap: "wrap",
|
|
238
|
+
fontSize: 11,
|
|
239
|
+
fontFamily: "var(--font-mono)",
|
|
240
|
+
color: "var(--af-text-tertiary)",
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<span>starts at {startOff}</span>
|
|
244
|
+
{w.model && <span>· {w.model}</span>}
|
|
245
|
+
<span>· runId {w.runId}</span>
|
|
246
|
+
{w.parentToolUseId && (
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={() => onJumpToParent(w.parentToolUseId)}
|
|
250
|
+
style={{
|
|
251
|
+
marginLeft: "auto",
|
|
252
|
+
fontSize: 11,
|
|
253
|
+
color: "var(--af-accent)",
|
|
254
|
+
background: "transparent",
|
|
255
|
+
border: "none",
|
|
256
|
+
cursor: "pointer",
|
|
257
|
+
padding: 0,
|
|
258
|
+
fontFamily: "var(--font-mono)",
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
Jump to timeline →
|
|
262
|
+
</button>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{w.agents.length > 0 ? (
|
|
267
|
+
<WorkflowPhaseTabs
|
|
268
|
+
w={w}
|
|
269
|
+
selectedAgentKey={selectedAgentKey}
|
|
270
|
+
onOpenAgent={onOpenAgent}
|
|
271
|
+
/>
|
|
272
|
+
) : (
|
|
273
|
+
w.phases.length > 0 && (
|
|
274
|
+
<div>
|
|
275
|
+
<SectionLabel>Phases</SectionLabel>
|
|
276
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
|
277
|
+
{w.phases.map((p, i) => (
|
|
278
|
+
<span
|
|
279
|
+
key={`${p.title}-${i}`}
|
|
280
|
+
title={p.detail}
|
|
281
|
+
style={{
|
|
282
|
+
display: "inline-flex",
|
|
283
|
+
alignItems: "center",
|
|
284
|
+
gap: 5,
|
|
285
|
+
fontSize: 11,
|
|
286
|
+
padding: "3px 9px",
|
|
287
|
+
borderRadius: 4,
|
|
288
|
+
background: "var(--af-border-subtle)",
|
|
289
|
+
color: "var(--af-text)",
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
<b style={{ fontWeight: 600, fontFamily: "var(--font-mono)" }}>{p.title}</b>
|
|
293
|
+
{p.detail && (
|
|
294
|
+
<span style={{ color: "var(--af-text-tertiary)", maxWidth: 360, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
295
|
+
{p.detail}
|
|
296
|
+
</span>
|
|
297
|
+
)}
|
|
298
|
+
</span>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
{w.logs.length > 0 && <WorkflowRunLog logs={w.logs} />}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Compact model label: "claude-opus-4-8[1m]" → "opus-4-8", "claude-haiku-4-5-20251001" → "haiku-4-5". */
|
|
313
|
+
function shortenModel(model: string): string {
|
|
314
|
+
return model
|
|
315
|
+
.replace(/^claude-/, "")
|
|
316
|
+
.replace(/\[1m\]$/, "")
|
|
317
|
+
.replace(/-\d{6,}$/, "")
|
|
318
|
+
.trim();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Status dot color for a single workflow-spawned agent. */
|
|
322
|
+
function agentStateColor(state?: string): string {
|
|
323
|
+
const s = (state ?? "").toLowerCase();
|
|
324
|
+
if (s === "done" || s === "completed" || s === "success") return "#10B981";
|
|
325
|
+
if (s === "error" || s === "failed") return "#EF4444";
|
|
326
|
+
if (s === "running" || s === "active" || s === "in_progress") return "#F59E0B";
|
|
327
|
+
return "#8A8580";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Per-phase tabs for a workflow run. Each tab lists the agents (actions)
|
|
331
|
+
* that ran in that phase so you can review what actually happened. */
|
|
332
|
+
function WorkflowPhaseTabs({
|
|
333
|
+
w,
|
|
334
|
+
selectedAgentKey,
|
|
335
|
+
onOpenAgent,
|
|
336
|
+
}: {
|
|
337
|
+
w: WorkflowRun;
|
|
338
|
+
selectedAgentKey?: string | null;
|
|
339
|
+
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
340
|
+
}) {
|
|
341
|
+
// Build the phase list from meta.phases (ordered), bucketing agents by
|
|
342
|
+
// phaseIndex. Agents whose phaseIndex isn't in meta.phases fall into an
|
|
343
|
+
// appended bucket keyed by their phaseTitle so nothing is dropped.
|
|
344
|
+
const groups = useMemo(() => {
|
|
345
|
+
const byIndex = new Map<number, WorkflowRun["agents"]>();
|
|
346
|
+
const extras = new Map<string, WorkflowRun["agents"]>();
|
|
347
|
+
for (const a of w.agents) {
|
|
348
|
+
if (a.phaseIndex && a.phaseIndex >= 1 && a.phaseIndex <= w.phases.length) {
|
|
349
|
+
const arr = byIndex.get(a.phaseIndex) ?? [];
|
|
350
|
+
arr.push(a);
|
|
351
|
+
byIndex.set(a.phaseIndex, arr);
|
|
352
|
+
} else {
|
|
353
|
+
const key = a.phaseTitle ?? "Other";
|
|
354
|
+
const arr = extras.get(key) ?? [];
|
|
355
|
+
arr.push(a);
|
|
356
|
+
extras.set(key, arr);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const out = w.phases.map((p, i) => ({
|
|
360
|
+
key: `p${i + 1}`,
|
|
361
|
+
title: p.title,
|
|
362
|
+
detail: p.detail,
|
|
363
|
+
agents: byIndex.get(i + 1) ?? [],
|
|
364
|
+
}));
|
|
365
|
+
for (const [title, agents] of extras) {
|
|
366
|
+
out.push({ key: `x-${title}`, title, detail: undefined, agents });
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}, [w]);
|
|
370
|
+
|
|
371
|
+
// The first phase that actually has agents. For a live workflow this only
|
|
372
|
+
// resolves AFTER agents stream in, so it's recomputed every render.
|
|
373
|
+
const firstWithAgents = groups.find((g) => g.agents.length > 0)?.key ?? groups[0]?.key ?? "";
|
|
374
|
+
// Bug fix: the active tab used to be seeded once via useState(firstWithAgents),
|
|
375
|
+
// so when a live run's phases populated after first mount the selection stayed
|
|
376
|
+
// stuck on the initial (often empty) default. We now track only an explicit
|
|
377
|
+
// user pick; until then the active tab derives from firstWithAgents and follows
|
|
378
|
+
// it reactively. Once the user clicks a tab, `picked` pins the choice.
|
|
379
|
+
const [picked, setPicked] = useState<string | null>(null);
|
|
380
|
+
const active = picked ?? firstWithAgents;
|
|
381
|
+
const activeGroup = groups.find((g) => g.key === active) ?? groups[0];
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div>
|
|
385
|
+
<SectionLabel>Phases & actions</SectionLabel>
|
|
386
|
+
{/* Phase tab strip */}
|
|
387
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 10 }}>
|
|
388
|
+
{groups.map((g) => {
|
|
389
|
+
const isActive = g.key === active;
|
|
390
|
+
return (
|
|
391
|
+
<button
|
|
392
|
+
key={g.key}
|
|
393
|
+
onClick={() => setPicked(g.key)}
|
|
394
|
+
style={{
|
|
395
|
+
display: "inline-flex",
|
|
396
|
+
alignItems: "center",
|
|
397
|
+
gap: 6,
|
|
398
|
+
fontSize: 11.5,
|
|
399
|
+
fontWeight: isActive ? 600 : 500,
|
|
400
|
+
padding: "4px 10px",
|
|
401
|
+
borderRadius: 6,
|
|
402
|
+
border: "1px solid",
|
|
403
|
+
borderColor: isActive ? "#EA580C" : "var(--af-border-subtle)",
|
|
404
|
+
background: isActive ? "rgba(234,88,12,0.12)" : "transparent",
|
|
405
|
+
color: isActive ? "#EA580C" : "var(--af-text-secondary)",
|
|
406
|
+
cursor: "pointer",
|
|
407
|
+
fontFamily: "var(--font-mono)",
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
{g.title}
|
|
411
|
+
<span
|
|
412
|
+
style={{
|
|
413
|
+
fontSize: 10,
|
|
414
|
+
fontWeight: 700,
|
|
415
|
+
padding: "0px 5px",
|
|
416
|
+
borderRadius: 100,
|
|
417
|
+
background: isActive ? "#EA580C" : "var(--af-border-subtle)",
|
|
418
|
+
color: isActive ? "#fff" : "var(--af-text-tertiary)",
|
|
419
|
+
}}
|
|
420
|
+
>
|
|
421
|
+
{g.agents.length}
|
|
422
|
+
</span>
|
|
423
|
+
</button>
|
|
424
|
+
);
|
|
425
|
+
})}
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
{/* Active phase body */}
|
|
429
|
+
{activeGroup && (
|
|
430
|
+
<div>
|
|
431
|
+
{activeGroup.detail && (
|
|
432
|
+
<div
|
|
433
|
+
style={{
|
|
434
|
+
fontSize: 11.5,
|
|
435
|
+
color: "var(--af-text-tertiary)",
|
|
436
|
+
marginBottom: 8,
|
|
437
|
+
fontStyle: "italic",
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
{activeGroup.detail}
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
{activeGroup.agents.length === 0 ? (
|
|
444
|
+
<div style={{ fontSize: 12, color: "var(--af-text-tertiary)", padding: "8px 0" }}>
|
|
445
|
+
No agents recorded for this phase.
|
|
446
|
+
</div>
|
|
447
|
+
) : (
|
|
448
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
449
|
+
{activeGroup.agents.map((a) => (
|
|
450
|
+
<WorkflowAgentRow
|
|
451
|
+
key={`${a.index}-${a.agentId ?? a.label}`}
|
|
452
|
+
a={a}
|
|
453
|
+
selected={selectedAgentKey === `${w.runId}:${a.agentId ?? a.index}`}
|
|
454
|
+
onOpen={() => onOpenAgent(w.runId, a)}
|
|
455
|
+
/>
|
|
456
|
+
))}
|
|
457
|
+
</div>
|
|
458
|
+
)}
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** One agent (action) inside a phase — collapsed shows label + metering;
|
|
466
|
+
* expanding reveals the task prompt and the returned result. */
|
|
467
|
+
/** Full transcript detail for one workflow agent — fetched on demand from
|
|
468
|
+
* /api/workflow-agent. Mirrors the parser's WorkflowAgentDetail. */
|
|
469
|
+
type WfAgentDetail = {
|
|
470
|
+
prompt?: string;
|
|
471
|
+
finalText?: string;
|
|
472
|
+
steps: { index: number; tool: string; preview: string; full?: string; isError?: boolean }[];
|
|
473
|
+
toolCalls: { name: string; count: number }[];
|
|
474
|
+
toolCallCount: number;
|
|
475
|
+
assistantMessageCount: number;
|
|
476
|
+
eventCount: number;
|
|
477
|
+
totalUsage: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
478
|
+
model?: string;
|
|
479
|
+
durationMs?: number;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
function WorkflowAgentRow({
|
|
483
|
+
a,
|
|
484
|
+
selected,
|
|
485
|
+
onOpen,
|
|
486
|
+
}: {
|
|
487
|
+
a: WorkflowRun["agents"][number];
|
|
488
|
+
selected: boolean;
|
|
489
|
+
onOpen: () => void;
|
|
490
|
+
}) {
|
|
491
|
+
const dur = a.durationMs !== undefined ? formatGap(a.durationMs) : undefined;
|
|
492
|
+
return (
|
|
493
|
+
<button
|
|
494
|
+
onClick={onOpen}
|
|
495
|
+
style={{
|
|
496
|
+
width: "100%",
|
|
497
|
+
display: "flex",
|
|
498
|
+
alignItems: "center",
|
|
499
|
+
gap: 8,
|
|
500
|
+
padding: "8px 10px",
|
|
501
|
+
textAlign: "left",
|
|
502
|
+
cursor: "pointer",
|
|
503
|
+
border: "1px solid",
|
|
504
|
+
borderColor: selected ? "#EA580C" : "var(--af-border-subtle)",
|
|
505
|
+
borderRadius: 6,
|
|
506
|
+
background: selected
|
|
507
|
+
? "rgba(234,88,12,0.08)"
|
|
508
|
+
: "var(--af-surface-subtle, rgba(120,115,108,0.04))",
|
|
509
|
+
color: "inherit",
|
|
510
|
+
flexWrap: "wrap",
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
513
|
+
<span
|
|
514
|
+
title={a.state}
|
|
515
|
+
style={{ width: 7, height: 7, borderRadius: "50%", background: agentStateColor(a.state), flexShrink: 0 }}
|
|
516
|
+
/>
|
|
517
|
+
<span
|
|
518
|
+
style={{
|
|
519
|
+
fontFamily: "var(--font-mono)",
|
|
520
|
+
fontSize: 12,
|
|
521
|
+
fontWeight: 600,
|
|
522
|
+
color: "var(--af-text)",
|
|
523
|
+
minWidth: 0,
|
|
524
|
+
overflow: "hidden",
|
|
525
|
+
textOverflow: "ellipsis",
|
|
526
|
+
whiteSpace: "nowrap",
|
|
527
|
+
maxWidth: 280,
|
|
528
|
+
}}
|
|
529
|
+
title={a.label}
|
|
530
|
+
>
|
|
531
|
+
{a.label}
|
|
532
|
+
</span>
|
|
533
|
+
{a.model && (
|
|
534
|
+
<span style={{ fontSize: 10, color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)" }}>
|
|
535
|
+
{shortenModel(a.model)}
|
|
536
|
+
</span>
|
|
537
|
+
)}
|
|
538
|
+
{a.lastToolSummary && (
|
|
539
|
+
<span
|
|
540
|
+
style={{
|
|
541
|
+
fontSize: 11,
|
|
542
|
+
color: "var(--af-text-tertiary)",
|
|
543
|
+
overflow: "hidden",
|
|
544
|
+
textOverflow: "ellipsis",
|
|
545
|
+
whiteSpace: "nowrap",
|
|
546
|
+
maxWidth: 240,
|
|
547
|
+
}}
|
|
548
|
+
title={a.lastToolSummary}
|
|
549
|
+
>
|
|
550
|
+
→ {a.lastToolSummary}
|
|
551
|
+
</span>
|
|
552
|
+
)}
|
|
553
|
+
<span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
|
|
554
|
+
{dur && <WfMiniStat icon={<Clock size={10} />} value={dur} />}
|
|
555
|
+
{a.tokens !== undefined && <WfMiniStat value={`${formatTokens(a.tokens)} tok`} />}
|
|
556
|
+
{a.toolCalls !== undefined && <WfMiniStat icon={<Wrench size={10} />} value={`${a.toolCalls}`} />}
|
|
557
|
+
<span style={{ color: selected ? "#EA580C" : "var(--af-text-tertiary)", display: "inline-flex" }}>
|
|
558
|
+
<ChevronRight size={14} />
|
|
559
|
+
</span>
|
|
560
|
+
</span>
|
|
561
|
+
</button>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Bug fix: the parent unmounts this drawer when the agent is deselected, so its
|
|
566
|
+
// fetched detail used to be lost on close — reopening the same agent refetched
|
|
567
|
+
// from scratch and flashed empty. This module-level cache outlives the component
|
|
568
|
+
// so a reopen paints the cached detail synchronously. (Stale-while-revalidate: we
|
|
569
|
+
// still refetch in the background so a live agent's transcript stays fresh.)
|
|
570
|
+
const wfAgentDetailCache = new Map<string, WfAgentDetail>();
|
|
571
|
+
|
|
572
|
+
/** Right side-sheet showing one workflow agent's full transcript — Task,
|
|
573
|
+
* ordered Steps, and Result — fetched on demand. Kept out of the card so the
|
|
574
|
+
* Workflows tab stays short even for 100+ step agents. */
|
|
575
|
+
export function WorkflowAgentDrawer({
|
|
576
|
+
sessionId,
|
|
577
|
+
runId,
|
|
578
|
+
agent,
|
|
579
|
+
onClose,
|
|
580
|
+
}: {
|
|
581
|
+
sessionId: string;
|
|
582
|
+
runId: string;
|
|
583
|
+
agent: WorkflowRun["agents"][number];
|
|
584
|
+
onClose: () => void;
|
|
585
|
+
}) {
|
|
586
|
+
// Seed from the cache on mount so a close+reopen of the same agent paints
|
|
587
|
+
// instantly instead of flashing empty while the refetch runs.
|
|
588
|
+
const [detail, setDetail] = useState<WfAgentDetail | null>(() =>
|
|
589
|
+
agent.agentId
|
|
590
|
+
? wfAgentDetailCache.get(`${sessionId}:${runId}:${agent.agentId}`) ?? null
|
|
591
|
+
: null,
|
|
592
|
+
);
|
|
593
|
+
const [loading, setLoading] = useState(false);
|
|
594
|
+
const [failed, setFailed] = useState(false);
|
|
595
|
+
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
setFailed(false);
|
|
598
|
+
if (!agent.agentId) {
|
|
599
|
+
setDetail(null);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const key = `${sessionId}:${runId}:${agent.agentId}`;
|
|
603
|
+
const cached = wfAgentDetailCache.get(key);
|
|
604
|
+
// Stale-while-revalidate: show the cached detail immediately (no empty flash),
|
|
605
|
+
// and only surface the loading state on a true cache miss.
|
|
606
|
+
if (cached) setDetail(cached);
|
|
607
|
+
else {
|
|
608
|
+
setDetail(null);
|
|
609
|
+
setLoading(true);
|
|
610
|
+
}
|
|
611
|
+
const ctrl = new AbortController();
|
|
612
|
+
const url = `/api/workflow-agent?session=${encodeURIComponent(sessionId)}&run=${encodeURIComponent(runId)}&agent=${encodeURIComponent(agent.agentId)}`;
|
|
613
|
+
fetch(url, { signal: ctrl.signal })
|
|
614
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
|
|
615
|
+
.then((d: WfAgentDetail) => {
|
|
616
|
+
wfAgentDetailCache.set(key, d);
|
|
617
|
+
setDetail(d);
|
|
618
|
+
})
|
|
619
|
+
.catch((e) => {
|
|
620
|
+
// Don't blank out a shown cached detail if only the revalidation failed.
|
|
621
|
+
if (e?.name !== "AbortError" && !cached) setFailed(true);
|
|
622
|
+
})
|
|
623
|
+
.finally(() => setLoading(false));
|
|
624
|
+
return () => ctrl.abort();
|
|
625
|
+
}, [sessionId, runId, agent.agentId]);
|
|
626
|
+
|
|
627
|
+
const prompt = detail?.prompt ?? agent.promptPreview;
|
|
628
|
+
const finalText = detail?.finalText ?? agent.resultPreview;
|
|
629
|
+
const dur = detail?.durationMs ?? agent.durationMs;
|
|
630
|
+
const model = detail?.model ?? agent.model;
|
|
631
|
+
const toolCount = detail?.toolCallCount ?? agent.toolCalls;
|
|
632
|
+
const tok = detail ? detail.totalUsage.input + detail.totalUsage.output : agent.tokens;
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<div>
|
|
636
|
+
{/* Sticky title bar */}
|
|
637
|
+
<div
|
|
638
|
+
style={{
|
|
639
|
+
padding: "14px 18px",
|
|
640
|
+
borderBottom: "1px solid var(--af-border-subtle)",
|
|
641
|
+
display: "flex",
|
|
642
|
+
alignItems: "center",
|
|
643
|
+
gap: 10,
|
|
644
|
+
background: "var(--af-surface)",
|
|
645
|
+
position: "sticky",
|
|
646
|
+
top: 0,
|
|
647
|
+
zIndex: 1,
|
|
648
|
+
}}
|
|
649
|
+
>
|
|
650
|
+
<span
|
|
651
|
+
title={agent.state}
|
|
652
|
+
style={{ width: 8, height: 8, borderRadius: "50%", background: agentStateColor(agent.state), flexShrink: 0 }}
|
|
653
|
+
/>
|
|
654
|
+
<span
|
|
655
|
+
style={{
|
|
656
|
+
fontSize: 13.5,
|
|
657
|
+
fontWeight: 600,
|
|
658
|
+
fontFamily: "var(--font-mono)",
|
|
659
|
+
color: "var(--af-text)",
|
|
660
|
+
flex: 1,
|
|
661
|
+
minWidth: 0,
|
|
662
|
+
overflow: "hidden",
|
|
663
|
+
textOverflow: "ellipsis",
|
|
664
|
+
whiteSpace: "nowrap",
|
|
665
|
+
}}
|
|
666
|
+
title={agent.label}
|
|
667
|
+
>
|
|
668
|
+
{agent.label}
|
|
669
|
+
</span>
|
|
670
|
+
<button
|
|
671
|
+
onClick={onClose}
|
|
672
|
+
style={{ background: "transparent", border: "none", cursor: "pointer", color: "var(--af-text-tertiary)", padding: 4 }}
|
|
673
|
+
aria-label="Close"
|
|
674
|
+
>
|
|
675
|
+
<X size={16} />
|
|
676
|
+
</button>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
{/* Meta strip */}
|
|
680
|
+
<div
|
|
681
|
+
style={{
|
|
682
|
+
padding: "9px 18px",
|
|
683
|
+
fontFamily: "var(--font-mono)",
|
|
684
|
+
fontSize: 11,
|
|
685
|
+
color: "var(--af-text-tertiary)",
|
|
686
|
+
borderBottom: "1px solid var(--af-border-subtle)",
|
|
687
|
+
display: "flex",
|
|
688
|
+
gap: 12,
|
|
689
|
+
flexWrap: "wrap",
|
|
690
|
+
}}
|
|
691
|
+
>
|
|
692
|
+
{agent.phaseTitle && <span>phase {agent.phaseTitle}</span>}
|
|
693
|
+
{agent.state && <span>· {agent.state}</span>}
|
|
694
|
+
{model && <span>· {shortenModel(model)}</span>}
|
|
695
|
+
{dur !== undefined && <span>· {formatGap(dur)}</span>}
|
|
696
|
+
{toolCount !== undefined && <span>· {toolCount} tools</span>}
|
|
697
|
+
{tok !== undefined && <span>· {formatTokens(tok)} tok</span>}
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
{/* Body — each section collapsed by default; click to expand. */}
|
|
701
|
+
<div style={{ padding: "12px 18px 18px", display: "flex", flexDirection: "column", gap: 8 }}>
|
|
702
|
+
{loading && (
|
|
703
|
+
<div style={{ fontSize: 11.5, color: "var(--af-text-tertiary)", fontStyle: "italic" }}>
|
|
704
|
+
Loading full transcript…
|
|
705
|
+
</div>
|
|
706
|
+
)}
|
|
707
|
+
{failed && (
|
|
708
|
+
<div style={{ fontSize: 11.5, color: "var(--af-text-tertiary)", fontStyle: "italic" }}>
|
|
709
|
+
Full transcript unavailable — showing the journal summary.
|
|
710
|
+
</div>
|
|
711
|
+
)}
|
|
712
|
+
|
|
713
|
+
{prompt && (
|
|
714
|
+
<DrawerCollapsible title="Task" hint={detail ? undefined : "preview"}>
|
|
715
|
+
<div style={{ fontSize: 12, color: "var(--af-text-secondary)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>
|
|
716
|
+
{prompt}
|
|
717
|
+
</div>
|
|
718
|
+
</DrawerCollapsible>
|
|
719
|
+
)}
|
|
720
|
+
|
|
721
|
+
{detail && detail.steps.length > 0 && (
|
|
722
|
+
<DrawerCollapsible
|
|
723
|
+
title="Steps"
|
|
724
|
+
count={detail.steps.length}
|
|
725
|
+
hint={detail.toolCalls.slice(0, 6).map((t) => `${shortenToolName(t.name)} ×${t.count}`).join(" · ")}
|
|
726
|
+
>
|
|
727
|
+
<div style={{ border: "1px solid var(--af-border-subtle)", borderRadius: 6, background: "var(--af-surface)" }}>
|
|
728
|
+
{detail.steps.map((s) => (
|
|
729
|
+
<WorkflowStepRow key={s.index} s={s} />
|
|
730
|
+
))}
|
|
731
|
+
</div>
|
|
732
|
+
</DrawerCollapsible>
|
|
733
|
+
)}
|
|
734
|
+
|
|
735
|
+
{finalText && (
|
|
736
|
+
<DrawerCollapsible title="Result" hint={detail ? undefined : "preview"}>
|
|
737
|
+
<div
|
|
738
|
+
style={{
|
|
739
|
+
fontFamily: "var(--font-mono)",
|
|
740
|
+
fontSize: 11,
|
|
741
|
+
color: "var(--af-text-secondary)",
|
|
742
|
+
whiteSpace: "pre-wrap",
|
|
743
|
+
wordBreak: "break-word",
|
|
744
|
+
lineHeight: 1.5,
|
|
745
|
+
background: "var(--af-surface)",
|
|
746
|
+
border: "1px solid var(--af-border-subtle)",
|
|
747
|
+
borderRadius: 4,
|
|
748
|
+
padding: "8px 10px",
|
|
749
|
+
}}
|
|
750
|
+
>
|
|
751
|
+
{finalText}
|
|
752
|
+
</div>
|
|
753
|
+
</DrawerCollapsible>
|
|
754
|
+
)}
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** Collapsible section inside the agent sheet — default collapsed. */
|
|
761
|
+
function DrawerCollapsible({
|
|
762
|
+
title,
|
|
763
|
+
count,
|
|
764
|
+
hint,
|
|
765
|
+
defaultOpen = false,
|
|
766
|
+
children,
|
|
767
|
+
}: {
|
|
768
|
+
title: string;
|
|
769
|
+
count?: number;
|
|
770
|
+
hint?: string;
|
|
771
|
+
defaultOpen?: boolean;
|
|
772
|
+
children: React.ReactNode;
|
|
773
|
+
}) {
|
|
774
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
775
|
+
return (
|
|
776
|
+
<div style={{ border: "1px solid var(--af-border-subtle)", borderRadius: 8, overflow: "hidden" }}>
|
|
777
|
+
<button
|
|
778
|
+
onClick={() => setOpen((v) => !v)}
|
|
779
|
+
style={{
|
|
780
|
+
width: "100%",
|
|
781
|
+
display: "flex",
|
|
782
|
+
alignItems: "center",
|
|
783
|
+
gap: 8,
|
|
784
|
+
padding: "9px 12px",
|
|
785
|
+
background: open ? "var(--af-surface-subtle, rgba(120,115,108,0.05))" : "transparent",
|
|
786
|
+
border: "none",
|
|
787
|
+
cursor: "pointer",
|
|
788
|
+
textAlign: "left",
|
|
789
|
+
color: "inherit",
|
|
790
|
+
}}
|
|
791
|
+
>
|
|
792
|
+
<span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>
|
|
793
|
+
{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
794
|
+
</span>
|
|
795
|
+
<span style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--af-text-secondary)" }}>
|
|
796
|
+
{title}
|
|
797
|
+
</span>
|
|
798
|
+
{count !== undefined && (
|
|
799
|
+
<span
|
|
800
|
+
style={{
|
|
801
|
+
fontSize: 10,
|
|
802
|
+
fontWeight: 700,
|
|
803
|
+
padding: "1px 7px",
|
|
804
|
+
borderRadius: 100,
|
|
805
|
+
background: "rgba(234,88,12,0.14)",
|
|
806
|
+
color: "#EA580C",
|
|
807
|
+
fontFamily: "var(--font-mono)",
|
|
808
|
+
}}
|
|
809
|
+
>
|
|
810
|
+
{count}
|
|
811
|
+
</span>
|
|
812
|
+
)}
|
|
813
|
+
{hint && (
|
|
814
|
+
<span
|
|
815
|
+
style={{
|
|
816
|
+
marginLeft: "auto",
|
|
817
|
+
fontSize: 10.5,
|
|
818
|
+
color: "var(--af-text-tertiary)",
|
|
819
|
+
fontFamily: "var(--font-mono)",
|
|
820
|
+
overflow: "hidden",
|
|
821
|
+
textOverflow: "ellipsis",
|
|
822
|
+
whiteSpace: "nowrap",
|
|
823
|
+
maxWidth: 280,
|
|
824
|
+
}}
|
|
825
|
+
>
|
|
826
|
+
{hint}
|
|
827
|
+
</span>
|
|
828
|
+
)}
|
|
829
|
+
</button>
|
|
830
|
+
{open && <div style={{ padding: "0 12px 12px" }}>{children}</div>}
|
|
831
|
+
</div>
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** One step row — click to reveal the full tool input (e.g. the complete
|
|
836
|
+
* multi-line bash command) when it's longer than the one-line preview. */
|
|
837
|
+
function WorkflowStepRow({ s }: { s: WfAgentDetail["steps"][number] }) {
|
|
838
|
+
const [open, setOpen] = useState(false);
|
|
839
|
+
const expandable = !!s.full;
|
|
840
|
+
return (
|
|
841
|
+
<div style={{ borderTop: s.index === 1 ? "none" : "1px solid var(--af-border-subtle)" }}>
|
|
842
|
+
<div
|
|
843
|
+
onClick={() => expandable && setOpen((v) => !v)}
|
|
844
|
+
style={{
|
|
845
|
+
display: "flex",
|
|
846
|
+
alignItems: "baseline",
|
|
847
|
+
gap: 8,
|
|
848
|
+
padding: "4px 10px",
|
|
849
|
+
fontFamily: "var(--font-mono)",
|
|
850
|
+
fontSize: 11,
|
|
851
|
+
cursor: expandable ? "pointer" : "default",
|
|
852
|
+
}}
|
|
853
|
+
>
|
|
854
|
+
<span style={{ color: "var(--af-text-tertiary)", minWidth: 28, textAlign: "right", flexShrink: 0 }}>
|
|
855
|
+
{s.index}
|
|
856
|
+
</span>
|
|
857
|
+
<span style={{ fontWeight: 600, color: s.isError ? "#EF4444" : "var(--af-text)", flexShrink: 0, minWidth: 104 }}>
|
|
858
|
+
{s.isError ? "⚠ " : ""}
|
|
859
|
+
{shortenToolName(s.tool)}
|
|
860
|
+
</span>
|
|
861
|
+
<span
|
|
862
|
+
style={{ flex: 1, color: "var(--af-text-tertiary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
|
863
|
+
title={s.preview}
|
|
864
|
+
>
|
|
865
|
+
{s.preview}
|
|
866
|
+
</span>
|
|
867
|
+
{expandable && (
|
|
868
|
+
<span style={{ color: "var(--af-text-tertiary)", display: "inline-flex", flexShrink: 0 }}>
|
|
869
|
+
{open ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
|
|
870
|
+
</span>
|
|
871
|
+
)}
|
|
872
|
+
</div>
|
|
873
|
+
{open && s.full && (
|
|
874
|
+
<pre
|
|
875
|
+
style={{
|
|
876
|
+
margin: 0,
|
|
877
|
+
padding: "6px 10px 10px 40px",
|
|
878
|
+
fontFamily: "var(--font-mono)",
|
|
879
|
+
fontSize: 11,
|
|
880
|
+
whiteSpace: "pre-wrap",
|
|
881
|
+
wordBreak: "break-word",
|
|
882
|
+
color: "var(--af-text-secondary)",
|
|
883
|
+
background: "var(--af-surface-subtle, rgba(120,115,108,0.05))",
|
|
884
|
+
lineHeight: 1.5,
|
|
885
|
+
}}
|
|
886
|
+
>
|
|
887
|
+
{s.full}
|
|
888
|
+
</pre>
|
|
889
|
+
)}
|
|
890
|
+
</div>
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** Collapsible coarse run log (▶ / ✓ task markers) — secondary to the
|
|
895
|
+
* per-phase agent view. */
|
|
896
|
+
function WorkflowRunLog({ logs }: { logs: string[] }) {
|
|
897
|
+
const [open, setOpen] = useState(false);
|
|
898
|
+
return (
|
|
899
|
+
<div>
|
|
900
|
+
<button
|
|
901
|
+
onClick={() => setOpen((v) => !v)}
|
|
902
|
+
style={{
|
|
903
|
+
display: "inline-flex",
|
|
904
|
+
alignItems: "center",
|
|
905
|
+
gap: 5,
|
|
906
|
+
fontSize: 10,
|
|
907
|
+
fontWeight: 600,
|
|
908
|
+
textTransform: "uppercase",
|
|
909
|
+
letterSpacing: "0.04em",
|
|
910
|
+
color: "var(--af-text-tertiary)",
|
|
911
|
+
background: "transparent",
|
|
912
|
+
border: "none",
|
|
913
|
+
cursor: "pointer",
|
|
914
|
+
padding: 0,
|
|
915
|
+
}}
|
|
916
|
+
>
|
|
917
|
+
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
918
|
+
Run log ({logs.length})
|
|
919
|
+
</button>
|
|
920
|
+
{open && (
|
|
921
|
+
<div
|
|
922
|
+
style={{
|
|
923
|
+
marginTop: 6,
|
|
924
|
+
fontFamily: "var(--font-mono)",
|
|
925
|
+
fontSize: 11.5,
|
|
926
|
+
lineHeight: 1.7,
|
|
927
|
+
color: "var(--af-text-secondary)",
|
|
928
|
+
background: "var(--af-surface-subtle, rgba(120,115,108,0.05))",
|
|
929
|
+
border: "1px solid var(--af-border-subtle)",
|
|
930
|
+
borderRadius: 6,
|
|
931
|
+
padding: "8px 12px",
|
|
932
|
+
maxHeight: 240,
|
|
933
|
+
overflowY: "auto",
|
|
934
|
+
whiteSpace: "pre-wrap",
|
|
935
|
+
}}
|
|
936
|
+
>
|
|
937
|
+
{logs.map((l, i) => (
|
|
938
|
+
<div key={i}>{l}</div>
|
|
939
|
+
))}
|
|
940
|
+
</div>
|
|
941
|
+
)}
|
|
942
|
+
</div>
|
|
943
|
+
);
|
|
944
|
+
}
|