fleetlens 0.13.0 → 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.
Files changed (211) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +8 -8
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +5 -5
  5. package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +7 -7
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  12. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +5 -5
  14. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +1 -1
  19. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +1 -1
  21. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  23. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  25. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  27. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  29. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  33. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
  35. package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +5 -5
  36. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  37. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  38. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  39. package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +5 -5
  40. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  41. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  42. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  43. package/app/apps/web/.next/server/app/day/page/build-manifest.json +5 -5
  44. package/app/apps/web/.next/server/app/day/page.js +1 -1
  45. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  46. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  47. package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +5 -5
  48. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  49. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  50. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  51. package/app/apps/web/.next/server/app/digest/page/build-manifest.json +5 -5
  52. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  53. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  54. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  55. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +5 -5
  56. package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
  57. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  58. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  59. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +5 -5
  60. package/app/apps/web/.next/server/app/insights/page.js +3 -2
  61. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  62. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  63. package/app/apps/web/.next/server/app/page/build-manifest.json +5 -5
  64. package/app/apps/web/.next/server/app/page.js +1 -1
  65. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  66. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  67. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +5 -5
  68. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  69. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  70. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  71. package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +5 -5
  72. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  73. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  74. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  75. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +5 -5
  76. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
  77. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  78. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  79. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +5 -5
  80. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  81. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  82. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  83. package/app/apps/web/.next/server/app/runs/page/build-manifest.json +5 -5
  84. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  85. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  86. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  87. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +5 -5
  88. package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
  89. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  90. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  91. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +5 -5
  92. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  93. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  94. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  95. package/app/apps/web/.next/server/app/settings/page/build-manifest.json +5 -5
  96. package/app/apps/web/.next/server/app/settings/page.js +1 -1
  97. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  98. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  99. package/app/apps/web/.next/server/app/team/page/build-manifest.json +5 -5
  100. package/app/apps/web/.next/server/app/team/page.js +1 -1
  101. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  102. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  103. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +5 -5
  104. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  105. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  106. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  107. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gril0u._.js +9 -0
  108. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0n79hpm._.js +9 -0
  109. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~utv~v._.js +9 -0
  110. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  111. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  112. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
  113. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0-1_njm._.js → [root-of-the-server]__0k1x-c9._.js} +2 -2
  114. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +8 -0
  115. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +1 -1
  116. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
  117. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
  118. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
  119. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  120. package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
  121. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  122. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
  124. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
  125. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  127. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  129. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  130. package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
  131. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  132. package/app/apps/web/.next/server/pages/500.html +1 -1
  133. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  134. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  135. package/app/apps/web/.next/static/chunks/02xxpexzk3rf-.js +1 -0
  136. package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
  137. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
  138. package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
  139. package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
  140. package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
  141. package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
  142. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
  143. package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
  144. package/app/apps/web/.next/static/chunks/{0gblp_v.kiu-..js → 0qv-gw888_5nu.js} +1 -1
  145. package/app/apps/web/.next/static/chunks/{14bjditmmrn7l.js → 0tirv_vo6q8uc.js} +1 -1
  146. package/app/apps/web/.next/static/chunks/0uyctl-m8iydb.js +1 -0
  147. package/app/apps/web/.next/static/chunks/0~qdw1b2uz._5.js +10 -0
  148. package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
  149. package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
  150. package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
  151. package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
  152. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
  153. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
  154. package/app/apps/web/app/insights/page.tsx +20 -5
  155. package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
  156. package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
  157. package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
  158. package/app/apps/web/app/parallelism/gantt-chart.tsx +24 -903
  159. package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
  160. package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
  161. package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
  162. package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
  163. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
  164. package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
  165. package/app/apps/web/app/sessions/[id]/session-view/minimap.tsx +1587 -0
  166. package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
  167. package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
  168. package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
  169. package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
  170. package/app/apps/web/app/sessions/[id]/session-view/workflows-panel.tsx +944 -0
  171. package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
  172. package/app/apps/web/app/sessions/[id]/session-view.tsx +1513 -5018
  173. package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
  174. package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
  175. package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
  176. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
  177. package/app/apps/web/app/sessions/sessions-grid.tsx +10 -10
  178. package/app/apps/web/components/digest-actions.tsx +103 -0
  179. package/app/apps/web/components/job-queue-widget.tsx +1 -1
  180. package/app/apps/web/components/month-digest-view.tsx +19 -64
  181. package/app/apps/web/components/searchable-select.tsx +215 -0
  182. package/app/apps/web/components/tail-mode.tsx +46 -9
  183. package/app/apps/web/components/week-digest-view.tsx +16 -71
  184. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  185. package/app/apps/web/lib/entries-index.ts +7 -8
  186. package/app/apps/web/lib/entries.ts +0 -9
  187. package/app/apps/web/lib/format.test.ts +221 -0
  188. package/app/apps/web/lib/format.ts +17 -0
  189. package/app/apps/web/lib/utilization-tone.test.ts +56 -0
  190. package/app/apps/web/package.json +1 -1
  191. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  192. package/dist/daemon-worker.js +35 -43
  193. package/dist/index.js +37 -49
  194. package/package.json +1 -1
  195. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
  196. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
  197. package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
  198. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
  199. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
  200. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
  201. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
  202. package/app/apps/web/.next/static/chunks/05k84gif3ucxm.js +0 -6
  203. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
  204. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
  205. package/app/apps/web/.next/static/chunks/0pzt9vemtzct8.js +0 -10
  206. package/app/apps/web/.next/static/chunks/0qmh5da6gfm56.js +0 -1
  207. package/app/apps/web/.next/static/chunks/0rnay-a.-jr8-.js +0 -1
  208. package/app/apps/web/.next/static/chunks/0snv3-2ux2xvv.js +0 -6
  209. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_buildManifest.js +0 -0
  210. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_clientMiddlewareManifest.js +0 -0
  211. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → 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 &amp; 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
+ }