fleetlens 0.11.2 → 0.12.1

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 (214) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +3 -0
  3. package/app/apps/web/.next/build-manifest.json +3 -3
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +22 -0
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  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.js +2 -1
  14. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/app/apps/web/.next/server/app/api/ask/route.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 +2 -2
  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 +2 -2
  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 +1 -1
  23. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +2 -2
  25. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +1 -1
  27. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/events/route.js +1 -1
  29. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/jobs/route.js +1 -1
  31. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/pdf/[key]/route/app-paths-manifest.json +3 -0
  33. package/app/apps/web/.next/server/app/api/pdf/[key]/route/build-manifest.json +9 -0
  34. package/app/apps/web/.next/server/app/api/pdf/[key]/route/server-reference-manifest.json +4 -0
  35. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +10 -0
  36. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.map +5 -0
  37. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -0
  38. package/app/apps/web/.next/server/app/api/pdf/[key]/route_client-reference-manifest.js +3 -0
  39. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js +1 -1
  40. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  41. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js +1 -1
  42. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  43. package/app/apps/web/.next/server/app/api/runs/route.js +1 -1
  44. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  45. package/app/apps/web/.next/server/app/api/settings/route.js +1 -1
  46. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  47. package/app/apps/web/.next/server/app/api/team-config/route.js +1 -1
  48. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
  49. package/app/apps/web/.next/server/app/api/workflow-agent/route/app-paths-manifest.json +3 -0
  50. package/app/apps/web/.next/server/app/api/workflow-agent/route/build-manifest.json +9 -0
  51. package/app/apps/web/.next/server/app/api/workflow-agent/route/server-reference-manifest.json +4 -0
  52. package/app/apps/web/.next/server/app/api/workflow-agent/route.js +7 -0
  53. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.map +5 -0
  54. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -0
  55. package/app/apps/web/.next/server/app/api/workflow-agent/route_client-reference-manifest.js +3 -0
  56. package/app/apps/web/.next/server/app/changelog/page.js +2 -1
  57. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  58. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  59. package/app/apps/web/.next/server/app/day/[date]/page.js +2 -1
  60. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  61. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  62. package/app/apps/web/.next/server/app/day/page.js +2 -1
  63. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  64. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  65. package/app/apps/web/.next/server/app/digest/[date]/page.js +2 -1
  66. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  67. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  68. package/app/apps/web/.next/server/app/digest/page.js +2 -1
  69. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  70. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  71. package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -1
  72. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  73. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  74. package/app/apps/web/.next/server/app/insights/page.js +2 -1
  75. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  76. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  77. package/app/apps/web/.next/server/app/page.js +2 -1
  78. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  79. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  80. package/app/apps/web/.next/server/app/parallelism/page.js +2 -1
  81. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  82. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  83. package/app/apps/web/.next/server/app/print/[key]/page/app-paths-manifest.json +3 -0
  84. package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +17 -0
  85. package/app/apps/web/.next/server/app/print/[key]/page/next-font-manifest.json +6 -0
  86. package/app/apps/web/.next/server/app/print/[key]/page/react-loadable-manifest.json +1 -0
  87. package/app/apps/web/.next/server/app/print/[key]/page/server-reference-manifest.json +4 -0
  88. package/app/apps/web/.next/server/app/print/[key]/page.js +19 -0
  89. package/app/apps/web/.next/server/app/print/[key]/page.js.map +5 -0
  90. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -0
  91. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +3 -0
  92. package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -1
  93. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  94. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  95. package/app/apps/web/.next/server/app/projects/page.js +2 -1
  96. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  97. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  98. package/app/apps/web/.next/server/app/runs/page.js +2 -1
  99. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  100. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  101. package/app/apps/web/.next/server/app/sessions/[id]/page.js +2 -1
  102. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  103. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  104. package/app/apps/web/.next/server/app/sessions/page.js +2 -1
  105. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  106. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  107. package/app/apps/web/.next/server/app/settings/page.js +2 -1
  108. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  109. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  110. package/app/apps/web/.next/server/app/team/page.js +2 -1
  111. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  112. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  113. package/app/apps/web/.next/server/app/usage/page.js +2 -1
  114. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  115. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  116. package/app/apps/web/.next/server/app-paths-manifest.json +3 -0
  117. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +2 -2
  118. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0_8zh-i._.js → [root-of-the-server]__04j3tng._.js} +2 -2
  119. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0jprdks._.js → [root-of-the-server]__0cnh3k4._.js} +2 -2
  120. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e.f-t.._.js +3 -0
  121. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0kc9e1n._.js +3 -0
  122. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0y5p73y._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0ej_p3s._.js → [root-of-the-server]__132eosx._.js} +2 -2
  124. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_pdf_[key]_route_actions_0intcw9.js +3 -0
  125. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_workflow-agent_route_actions_0gxw9tv.js +3 -0
  126. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +1 -1
  127. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +1 -1
  130. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04il6m2._.js +11 -0
  131. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
  132. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +3 -0
  133. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o03ymy._.js +2 -2
  134. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
  135. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z7mg0b._.js +1 -1
  136. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +12 -0
  137. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +8 -0
  138. package/app/apps/web/.next/server/chunks/ssr/_0cgg5zi._.js +3 -0
  139. package/app/apps/web/.next/server/chunks/ssr/_0jw2sm7._.js +1 -1
  140. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +3 -0
  141. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +8 -0
  142. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  143. package/app/apps/web/.next/server/chunks/ssr/apps_web_0.mu7c7._.js +1 -1
  144. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_print_[key]_page_actions_0_ygul-.js +3 -0
  145. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +6 -0
  146. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_week-digest_tsx_0c9ha31._.js +3 -0
  147. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0k0tl52._.js +3 -0
  148. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +3 -0
  149. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +3 -0
  150. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07orc4m.js +4 -0
  151. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_12.endt._.js +1 -1
  152. package/app/apps/web/.next/server/edge/chunks/[root-of-the-server]__0.sx~id._.js +11 -0
  153. package/app/apps/web/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0p1t1z8.js +3 -0
  154. package/app/apps/web/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0mw-sqe.js +3 -0
  155. package/app/apps/web/.next/server/functions-config-manifest.json +4 -1
  156. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  157. package/app/apps/web/.next/server/middleware-manifest.json +30 -2
  158. package/app/apps/web/.next/server/pages/500.html +1 -1
  159. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  160. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  161. package/app/apps/web/.next/static/chunks/00xkqnvk4-d1-.js +10 -0
  162. package/app/apps/web/.next/static/chunks/03_7wmpodz36t.js +1 -0
  163. package/app/apps/web/.next/static/chunks/0716x58e1_2c5.js +1 -0
  164. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +4 -0
  165. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +4 -0
  166. package/app/apps/web/.next/static/chunks/0vf-3rauafbek.js +6 -0
  167. package/app/apps/web/.next/static/chunks/0vl27-vjob2_9.js +6 -0
  168. package/app/apps/web/.next/static/chunks/0yo458-7577lr.js +1 -0
  169. package/app/apps/web/.next/static/chunks/14bjditmmrn7l.js +1 -0
  170. package/app/apps/web/.next/static/rrY-ephAArv8tjJOXD622/_clientMiddlewareManifest.js +6 -0
  171. package/app/apps/web/app/api/events/route.ts +50 -2
  172. package/app/apps/web/app/api/pdf/[key]/route.ts +199 -0
  173. package/app/apps/web/app/api/workflow-agent/route.ts +27 -0
  174. package/app/apps/web/app/insights/[key]/page.tsx +4 -1
  175. package/app/apps/web/app/layout.tsx +23 -1
  176. package/app/apps/web/app/page.tsx +24 -1
  177. package/app/apps/web/app/print/[key]/page.tsx +91 -0
  178. package/app/apps/web/app/projects/[slug]/page.tsx +6 -7
  179. package/app/apps/web/app/projects/page.tsx +2 -3
  180. package/app/apps/web/app/sessions/[id]/session-view.tsx +1297 -19
  181. package/app/apps/web/app/sessions/sessions-grid.tsx +10 -1
  182. package/app/apps/web/components/dashboard-view.tsx +2 -1
  183. package/app/apps/web/components/export-pdf-button.tsx +76 -0
  184. package/app/apps/web/components/month-digest-view.tsx +10 -0
  185. package/app/apps/web/components/week-digest-view.tsx +10 -0
  186. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  187. package/app/apps/web/lib/entries-index.ts +5 -4
  188. package/app/apps/web/lib/find-chrome.ts +71 -0
  189. package/app/apps/web/middleware.ts +22 -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 +1716 -281
  193. package/dist/index.js +1617 -195
  194. package/package.json +1 -1
  195. package/app/apps/web/.next/server/chunks/packages_parser_dist_04rh1zd._.js +0 -3
  196. package/app/apps/web/.next/server/chunks/packages_parser_dist_0ndsxe6._.js +0 -3
  197. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_xpuh7._.js +0 -3
  198. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d~2~le._.js +0 -8
  199. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +0 -12
  200. package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +0 -8
  201. package/app/apps/web/.next/server/chunks/ssr/_0fjbf--._.js +0 -3
  202. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +0 -6
  203. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0af94gl._.js +0 -3
  204. package/app/apps/web/.next/static/chunks/0.3mqbj24kwtc.js +0 -6
  205. package/app/apps/web/.next/static/chunks/0byz8fqw.exef.js +0 -1
  206. package/app/apps/web/.next/static/chunks/0l3ck.awv~4t7.js +0 -1
  207. package/app/apps/web/.next/static/chunks/0lxuac2v0_gdl.js +0 -6
  208. package/app/apps/web/.next/static/chunks/0m62vx92~0_0d.js +0 -1
  209. package/app/apps/web/.next/static/chunks/0rtba0saz74e1.js +0 -10
  210. package/app/apps/web/.next/static/chunks/0s4eyofh28l~u.js +0 -1
  211. package/app/apps/web/.next/static/chunks/0uev~g~qvbjv9.js +0 -4
  212. package/app/apps/web/.next/static/m6ibRSdAu8jD10VBA2OFH/_clientMiddlewareManifest.js +0 -1
  213. /package/app/apps/web/.next/static/{m6ibRSdAu8jD10VBA2OFH → rrY-ephAArv8tjJOXD622}/_buildManifest.js +0 -0
  214. /package/app/apps/web/.next/static/{m6ibRSdAu8jD10VBA2OFH → rrY-ephAArv8tjJOXD622}/_ssgManifest.js +0 -0
@@ -12,6 +12,7 @@ import {
12
12
  Cpu,
13
13
  Folder,
14
14
  GitPullRequest,
15
+ Workflow as WorkflowIcon,
15
16
  Wrench,
16
17
  X,
17
18
  } from "lucide-react";
@@ -22,6 +23,7 @@ import type {
22
23
  SessionDetail,
23
24
  SessionEvent,
24
25
  SubagentRun,
26
+ WorkflowRun,
25
27
  PrMarker,
26
28
  } from "@claude-lens/parser";
27
29
  import type { Entry } from "@claude-lens/entries";
@@ -93,9 +95,11 @@ const FILTER_MODES: { value: FilterMode; label: string }[] = [
93
95
 
94
96
  /** Drawer width in px — reserved on the transcript's right edge when open. */
95
97
  const DRAWER_WIDTH = 460;
98
+ /** Wider sheet for the workflow-agent step log — dense step rows need room. */
99
+ const WF_AGENT_DRAWER_WIDTH = 600;
96
100
 
97
- type TabId = "transcript" | "team" | "debug";
98
- const VALID_TABS: TabId[] = ["transcript", "team", "debug"];
101
+ type TabId = "transcript" | "workflows" | "team" | "debug";
102
+ const VALID_TABS: TabId[] = ["transcript", "workflows", "team", "debug"];
99
103
 
100
104
  export function SessionView({
101
105
  session,
@@ -152,6 +156,14 @@ export function SessionView({
152
156
  const [filter, setFilter] = useState<FilterMode>("turns");
153
157
  const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
154
158
  const [selectedSubagentId, setSelectedSubagentId] = useState<string | null>(null);
159
+ /** Workflow run to focus (auto-expand + scroll) on the Workflows tab —
160
+ * set when a workflow lane on the minimap is clicked. */
161
+ const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
162
+ /** Workflow agent whose full step log is open in the right side-sheet. */
163
+ const [selectedWorkflowAgent, setSelectedWorkflowAgent] = useState<{
164
+ runId: string;
165
+ agent: WorkflowRun["agents"][number];
166
+ } | null>(null);
155
167
  const [askOpen, setAskOpen] = useState(false);
156
168
  const [expandedTurns, setExpandedTurns] = useState<Set<number>>(new Set());
157
169
  /** When a timeline click sets the index we also want to scroll. Track that
@@ -159,6 +171,20 @@ export function SessionView({
159
171
  const [scrollIntent, setScrollIntent] = useState(0);
160
172
  const rowRefs = useRef<Record<number, HTMLDivElement | null>>({});
161
173
 
174
+ // The event/subagent drawers are transcript-scoped (position: fixed, shown
175
+ // whenever a selection exists). Clear the selection when leaving the
176
+ // transcript so a stale drawer doesn't overlay the Workflows/Team/Debug tabs.
177
+ useEffect(() => {
178
+ if (tab !== "transcript") {
179
+ setSelectedIndex(null);
180
+ setSelectedSubagentId(null);
181
+ }
182
+ if (tab !== "workflows") {
183
+ setSelectedWorkflowId(null);
184
+ setSelectedWorkflowAgent(null);
185
+ }
186
+ }, [tab]);
187
+
162
188
  /** Measured height of the sticky header — used both as the drawer's
163
189
  * top offset (so it sits exactly below the header) and as the
164
190
  * scroll-margin for transcript rows (so click-to-focus lands a row
@@ -265,6 +291,8 @@ export function SessionView({
265
291
 
266
292
  const { events, durationMs, totalUsage, model, eventCount, projectName } = session;
267
293
  const airTimeMs = session.airTimeMs ?? durationMs;
294
+ const workflowCount = session.workflows?.length ?? 0;
295
+ const hasWorkflows = workflowCount > 0;
268
296
 
269
297
  /** Inbound `<teammate-message>` events are cross-session team traffic
270
298
  * wrapped in a synthetic user event. On the LEAD's transcript these are
@@ -337,13 +365,23 @@ export function SessionView({
337
365
  anchors.sort((a, b) => a.ms - b.ms);
338
366
  const sessionStartMs = anchors[0]!.ms - (events.find((e) => e.timestamp)?.tOffsetMs ?? 0);
339
367
 
340
- const subagentSpans = (session.subagents ?? [])
341
- .filter(
342
- (s): s is SubagentRun & { startTOffsetMs: number; endTOffsetMs: number } =>
343
- s.startTOffsetMs !== undefined && s.endTOffsetMs !== undefined,
344
- )
345
- .map((s) => ({ start: s.startTOffsetMs, end: s.endTOffsetMs }))
346
- .sort((a, b) => a.start - b.start);
368
+ // "Busy" spans = delegated agent work that means the parent isn't idle:
369
+ // subagent runs AND dynamic-workflow execution. Both are carved out of the
370
+ // idle bands so a long Workflow run (during which the parent just waits)
371
+ // reads as active agent time, not dead air. A still-running workflow has
372
+ // no end offset yet — extend it to the last anchor.
373
+ const lastAnchorOff = anchors[anchors.length - 1]!.ms - sessionStartMs;
374
+ const busySpans = [
375
+ ...(session.subagents ?? [])
376
+ .filter((s) => s.startTOffsetMs !== undefined && s.endTOffsetMs !== undefined)
377
+ .map((s) => ({ start: s.startTOffsetMs as number, end: s.endTOffsetMs as number })),
378
+ ...(session.workflows ?? [])
379
+ .filter((w) => w.startTOffsetMs !== undefined)
380
+ .map((w) => ({
381
+ start: w.startTOffsetMs as number,
382
+ end: (w.endTOffsetMs ?? lastAnchorOff) as number,
383
+ })),
384
+ ].sort((a, b) => a.start - b.start);
347
385
 
348
386
  /** Subtract subagent spans from [start, end] in offset coordinates,
349
387
  * returning 0+ remaining slices in time order. */
@@ -352,7 +390,7 @@ export function SessionView({
352
390
  end: number,
353
391
  ): { start: number; end: number }[] => {
354
392
  let pieces: { start: number; end: number }[] = [{ start, end }];
355
- for (const span of subagentSpans) {
393
+ for (const span of busySpans) {
356
394
  if (span.end <= start) continue;
357
395
  if (span.start >= end) break;
358
396
  const next: { start: number; end: number }[] = [];
@@ -370,9 +408,9 @@ export function SessionView({
370
408
  };
371
409
 
372
410
  const overlapsSubagent = (start: number, end: number): boolean => {
373
- for (const span of subagentSpans) {
411
+ for (const span of busySpans) {
374
412
  if (span.end <= start) continue;
375
- // subagentSpans is sorted by start, so once span.start >= end no
413
+ // busySpans is sorted by start, so once span.start >= end no
376
414
  // later span can overlap [start, end] either — bail out cleanly.
377
415
  if (span.start >= end) return false;
378
416
  return true;
@@ -433,7 +471,7 @@ export function SessionView({
433
471
  }
434
472
  }
435
473
  return bands;
436
- }, [events, session.subagents]);
474
+ }, [events, session.subagents, session.workflows]);
437
475
 
438
476
  const coldResumeMarkers = useMemo(() => {
439
477
  const seen = new Set<string>();
@@ -530,6 +568,23 @@ export function SessionView({
530
568
  setScrollIntent((n) => n + 1);
531
569
  }
532
570
 
571
+ /** Scroll the transcript to the row whose assistant message issued a given
572
+ * tool_use id. Used by workflow cards / lanes to jump to the originating
573
+ * Workflow tool call. */
574
+ function jumpToToolUse(toolUseId?: string) {
575
+ if (!toolUseId) return;
576
+ const ev = events.find((e) =>
577
+ e.blocks.some((b) => b?.type === "tool_use" && b.id === toolUseId),
578
+ );
579
+ if (ev) {
580
+ setSelectedSubagentId(null);
581
+ // The Workflow row lives in the transcript — switch there first (e.g.
582
+ // when jumping from the Workflows tab) so the scroll target exists.
583
+ if (tab !== "transcript") setTab("transcript");
584
+ scrollToIndex(ev.index);
585
+ }
586
+ }
587
+
533
588
  const totalInput = totalUsage.input + totalUsage.cacheRead + totalUsage.cacheWrite;
534
589
 
535
590
  return (
@@ -604,6 +659,28 @@ export function SessionView({
604
659
  >
605
660
  Transcript
606
661
  </button>
662
+ {hasWorkflows && (
663
+ <button
664
+ className={`af-tab-btn ${tab === "workflows" ? "active" : ""}`}
665
+ onClick={() => setTab("workflows")}
666
+ >
667
+ Workflows
668
+ <span
669
+ style={{
670
+ marginLeft: 6,
671
+ fontSize: 10,
672
+ fontWeight: 700,
673
+ padding: "1px 6px",
674
+ borderRadius: 100,
675
+ background: "rgba(234,88,12,0.16)",
676
+ color: "#EA580C",
677
+ fontFamily: "var(--font-mono)",
678
+ }}
679
+ >
680
+ {workflowCount}
681
+ </span>
682
+ </button>
683
+ )}
607
684
  {team && (
608
685
  <button
609
686
  className={`af-tab-btn ${tab === "team" ? "active" : ""}`}
@@ -725,6 +802,15 @@ export function SessionView({
725
802
  )}
726
803
  <InlineStatDivider />
727
804
  <InlineStat value={`${eventCount} events`} />
805
+ {(session.workflowCount ?? 0) > 0 && (
806
+ <>
807
+ <InlineStatDivider />
808
+ <InlineStat
809
+ icon={<WorkflowIcon size={12} />}
810
+ value={`${session.workflowCount} workflow${session.workflowCount === 1 ? "" : "s"} · ${(session.spawnedAgentCount ?? 0).toLocaleString()} agents`}
811
+ />
812
+ </>
813
+ )}
728
814
  {prMarkers.length > 0 && (
729
815
  <>
730
816
  <InlineStatDivider />
@@ -855,6 +941,11 @@ export function SessionView({
855
941
  onSelect={scrollToIndex}
856
942
  headerOffset={headerH}
857
943
  subagents={session.subagents}
944
+ workflows={session.workflows}
945
+ onWorkflowClick={(runId) => {
946
+ setSelectedWorkflowId(runId);
947
+ setTab("workflows");
948
+ }}
858
949
  collapsed={collapsed}
859
950
  prMarkers={prMarkers}
860
951
  coldResumeMarkers={coldResumeMarkers}
@@ -892,7 +983,30 @@ export function SessionView({
892
983
  onVisibleTrackIdsChange={setTeamVisibleTrackIds}
893
984
  seekTarget={teamSeekTarget}
894
985
  />
895
- ) : tab === "transcript" ? (
986
+ ) : tab === "workflows" && hasWorkflows ? (
987
+ <div style={{ paddingBottom: 24 }}>
988
+ <WorkflowsPanel
989
+ workflows={session.workflows!}
990
+ spawnedAgentCount={session.spawnedAgentCount ?? 0}
991
+ focusRunId={selectedWorkflowId}
992
+ selectedAgentKey={
993
+ selectedWorkflowAgent
994
+ ? `${selectedWorkflowAgent.runId}:${selectedWorkflowAgent.agent.agentId ?? selectedWorkflowAgent.agent.index}`
995
+ : null
996
+ }
997
+ onOpenAgent={(runId, agent) =>
998
+ setSelectedWorkflowAgent((cur) =>
999
+ cur && cur.runId === runId && cur.agent.index === agent.index
1000
+ ? null
1001
+ : { runId, agent },
1002
+ )
1003
+ }
1004
+ onJumpToParent={jumpToToolUse}
1005
+ />
1006
+ </div>
1007
+ ) : tab === "debug" ? (
1008
+ <DebugList events={events} />
1009
+ ) : (
896
1010
  <>
897
1011
  {entries.length > 0 && (
898
1012
  <div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 14 }}>
@@ -943,8 +1057,6 @@ export function SessionView({
943
1057
  rawIdleBands={rawIdleBands}
944
1058
  />
945
1059
  </>
946
- ) : (
947
- <DebugList events={events} />
948
1060
  )}
949
1061
  </div>
950
1062
 
@@ -1021,6 +1133,33 @@ export function SessionView({
1021
1133
  );
1022
1134
  })()}
1023
1135
 
1136
+ {/* Workflow-agent step log — opens as a right side-sheet so the full
1137
+ (often 100+ step) transcript doesn't make the Workflows tab huge. */}
1138
+ {selectedWorkflowAgent && (
1139
+ <aside
1140
+ style={{
1141
+ position: "fixed",
1142
+ top: headerH,
1143
+ right: 0,
1144
+ bottom: 0,
1145
+ width: WF_AGENT_DRAWER_WIDTH,
1146
+ maxWidth: "100vw",
1147
+ borderLeft: "1px solid var(--af-border-subtle)",
1148
+ background: "var(--af-surface)",
1149
+ overflowY: "auto",
1150
+ zIndex: 28,
1151
+ boxShadow: "-8px 0 24px rgba(15, 23, 42, 0.1)",
1152
+ }}
1153
+ >
1154
+ <WorkflowAgentDrawer
1155
+ sessionId={session.id}
1156
+ runId={selectedWorkflowAgent.runId}
1157
+ agent={selectedWorkflowAgent.agent}
1158
+ onClose={() => setSelectedWorkflowAgent(null)}
1159
+ />
1160
+ </aside>
1161
+ )}
1162
+
1024
1163
  {/* Ask drawer */}
1025
1164
  {askOpen && (
1026
1165
  <aside
@@ -1458,6 +1597,8 @@ function Minimap({
1458
1597
  onSelect,
1459
1598
  headerOffset,
1460
1599
  subagents,
1600
+ workflows,
1601
+ onWorkflowClick,
1461
1602
  collapsed,
1462
1603
  selectedSubagentId,
1463
1604
  onSelectSubagent,
@@ -1472,6 +1613,8 @@ function Minimap({
1472
1613
  onSelect: (i: number) => void;
1473
1614
  headerOffset: number;
1474
1615
  subagents?: SubagentRun[];
1616
+ workflows?: WorkflowRun[];
1617
+ onWorkflowClick?: (runId: string) => void;
1475
1618
  collapsed?: boolean;
1476
1619
  selectedSubagentId?: string | null;
1477
1620
  onSelectSubagent?: (id: string | null) => void;
@@ -1494,6 +1637,9 @@ function Minimap({
1494
1637
  const SUB_LANE_H = 11;
1495
1638
  /** Gap between the main timeline and the sub-agent lanes (when present). */
1496
1639
  const SUB_LANE_GAP = 6;
1640
+ /** Per-workflow lane height — a touch taller than subagent lanes so the
1641
+ * workflow tier reads as distinct. */
1642
+ const WF_LANE_H = 13;
1497
1643
  const BAR_TOP = 3;
1498
1644
  const BAR_BOT = MAIN_H - 3;
1499
1645
  const BAR_H = BAR_BOT - BAR_TOP;
@@ -1513,6 +1659,7 @@ function Minimap({
1513
1659
  turn?: TurnMegaRow;
1514
1660
  idleMs?: number;
1515
1661
  subagent?: SubagentRun;
1662
+ workflow?: WorkflowRun;
1516
1663
  pr?: PrMarker;
1517
1664
  cold?: {
1518
1665
  tOffsetMs: number;
@@ -1827,7 +1974,50 @@ function Minimap({
1827
1974
 
1828
1975
  const SUB_BLOCK_TOP = MAIN_H + SUB_LANE_GAP;
1829
1976
  const SUB_LANES_H = laneCount > 0 ? laneCount * SUB_LANE_H : 0;
1830
- const TOTAL_H = MAIN_H + (laneCount > 0 ? SUB_LANE_GAP + SUB_LANES_H : 0);
1977
+
1978
+ // ---- Workflow lane assignment --------------------------------------
1979
+ // Same greedy sweep as subagents. Workflow runs are mostly sequential
1980
+ // milestones, so this usually collapses to one lane — but overlap is
1981
+ // handled if two runs ever ran concurrently.
1982
+ const { wfLaneOf, wfLaneCount } = useMemo(() => {
1983
+ // Only place workflows we can actually draw (startTOffsetMs known). A
1984
+ // running run has no end yet — it extends to the session end (safeDur),
1985
+ // matching the open-ended bar the render draws. Counting only placeable
1986
+ // runs keeps wfLaneCount in lockstep with what renders, so no empty
1987
+ // reserved band appears.
1988
+ const placeable = (workflows ?? []).filter((w) => w.startTOffsetMs !== undefined);
1989
+ if (placeable.length === 0) {
1990
+ return { wfLaneOf: new Map<string, number>(), wfLaneCount: 0 };
1991
+ }
1992
+ const sorted = [...placeable].sort(
1993
+ (a, b) => (a.startTOffsetMs ?? 0) - (b.startTOffsetMs ?? 0),
1994
+ );
1995
+ const laneEnds: number[] = [];
1996
+ const wfLaneOf = new Map<string, number>();
1997
+ for (const w of sorted) {
1998
+ const start = w.startTOffsetMs ?? 0;
1999
+ const end = w.endTOffsetMs ?? safeDur;
2000
+ let assigned = -1;
2001
+ for (let i = 0; i < laneEnds.length; i++) {
2002
+ if (laneEnds[i]! <= start) {
2003
+ assigned = i;
2004
+ break;
2005
+ }
2006
+ }
2007
+ if (assigned === -1) {
2008
+ assigned = laneEnds.length;
2009
+ laneEnds.push(0);
2010
+ }
2011
+ laneEnds[assigned] = Math.max(end, start + 1);
2012
+ wfLaneOf.set(w.runId, assigned);
2013
+ }
2014
+ return { wfLaneOf, wfLaneCount: laneEnds.length };
2015
+ }, [workflows, safeDur]);
2016
+
2017
+ const SUB_BLOCK_BOTTOM = MAIN_H + (laneCount > 0 ? SUB_LANE_GAP + SUB_LANES_H : 0);
2018
+ const WF_BLOCK_TOP = SUB_BLOCK_BOTTOM + (wfLaneCount > 0 ? SUB_LANE_GAP : 0);
2019
+ const WF_LANES_H = wfLaneCount > 0 ? wfLaneCount * WF_LANE_H : 0;
2020
+ const TOTAL_H = SUB_BLOCK_BOTTOM + (wfLaneCount > 0 ? SUB_LANE_GAP + WF_LANES_H : 0);
1831
2021
 
1832
2022
  return (
1833
2023
  <div
@@ -2023,6 +2213,73 @@ function Minimap({
2023
2213
  </g>
2024
2214
  )}
2025
2215
 
2216
+ {/* Workflow lanes — one bar per dynamic-workflow run, on the same
2217
+ time axis as the main timeline, colored by status (orange tier,
2218
+ distinct from the blue/purple subagents). The agent count is the
2219
+ headline — a single Workflow tool call hides a whole fleet. */}
2220
+ {wfLaneCount > 0 && workflows && (
2221
+ <g>
2222
+ <line
2223
+ x1={0}
2224
+ x2={WIDTH}
2225
+ y1={WF_BLOCK_TOP - SUB_LANE_GAP / 2}
2226
+ y2={WF_BLOCK_TOP - SUB_LANE_GAP / 2}
2227
+ stroke="var(--af-border-subtle)"
2228
+ strokeWidth="0.6"
2229
+ strokeDasharray="2 4"
2230
+ />
2231
+ {workflows.map((w) => {
2232
+ if (w.startTOffsetMs === undefined) return null;
2233
+ const lane = wfLaneOf.get(w.runId) ?? 0;
2234
+ const startX = msToX(w.startTOffsetMs);
2235
+ // A still-running run has no endTOffsetMs — draw it open-ended to
2236
+ // the session end so it's visible (and dashed, to read as ongoing)
2237
+ // rather than vanishing while still occupying a reserved lane.
2238
+ const inProgress = w.endTOffsetMs === undefined;
2239
+ const endX = msToX(w.endTOffsetMs ?? safeDur);
2240
+ const wWidth = Math.max(endX - startX, 8);
2241
+ const y = WF_BLOCK_TOP + lane * WF_LANE_H;
2242
+ const h = WF_LANE_H - 2;
2243
+ const fill = workflowColor(w.status);
2244
+ // Center the agent count label inside the bar when there's room.
2245
+ const label = `${w.agentCount}`;
2246
+ const showLabel = wWidth > 22;
2247
+ return (
2248
+ <g key={w.runId} style={{ cursor: "pointer" }}>
2249
+ <rect
2250
+ x={startX}
2251
+ y={y}
2252
+ width={wWidth}
2253
+ height={h}
2254
+ fill={fill}
2255
+ fillOpacity={inProgress ? 0.55 : 1}
2256
+ stroke={inProgress ? fill : "transparent"}
2257
+ strokeWidth={inProgress ? 0.8 : 0}
2258
+ strokeDasharray={inProgress ? "2 2" : undefined}
2259
+ rx="2"
2260
+ onMouseEnter={(e) => setHover({ clientX: e.clientX, workflow: w })}
2261
+ onClick={() => onWorkflowClick?.(w.runId)}
2262
+ />
2263
+ {showLabel && (
2264
+ <text
2265
+ x={startX + wWidth / 2}
2266
+ y={y + h / 2 + 3}
2267
+ textAnchor="middle"
2268
+ fontSize="8.5"
2269
+ fontWeight="700"
2270
+ fill="#fff"
2271
+ pointerEvents="none"
2272
+ style={{ fontFamily: "var(--font-mono)" }}
2273
+ >
2274
+ {label}
2275
+ </text>
2276
+ )}
2277
+ </g>
2278
+ );
2279
+ })}
2280
+ </g>
2281
+ )}
2282
+
2026
2283
  {/* PR markers — diamond + vertical line at each `gh pr create` */}
2027
2284
  {prMarkers?.map((pr, i) => {
2028
2285
  if (pr.tOffsetMs === undefined) return null;
@@ -2123,6 +2380,7 @@ function Minimap({
2123
2380
  turn={hover.turn}
2124
2381
  idleMs={hover.idleMs}
2125
2382
  subagent={hover.subagent}
2383
+ workflow={hover.workflow}
2126
2384
  pr={hover.pr}
2127
2385
  cold={hover.cold}
2128
2386
  model={model}
@@ -2205,6 +2463,17 @@ function subagentColor(agentType: string, background?: boolean): string {
2205
2463
  return background ? bright : base;
2206
2464
  }
2207
2465
 
2466
+ /** Workflow signature color, modulated by run status. Deliberately a warm
2467
+ * orange — distinct from the blue/purple subagent palette so workflow lanes
2468
+ * read as a different tier on the minimap. Failed runs go red, in-flight
2469
+ * runs go green. */
2470
+ function workflowColor(status: string): string {
2471
+ const s = status.toLowerCase();
2472
+ if (s === "failed" || s === "aborted" || s === "error") return "#EF4444";
2473
+ if (s === "running" || s === "in_progress" || s === "active") return "#10B981";
2474
+ return "#EA580C";
2475
+ }
2476
+
2208
2477
  function MinimapHoverCard({
2209
2478
  containerRef,
2210
2479
  clientX,
@@ -2212,6 +2481,7 @@ function MinimapHoverCard({
2212
2481
  turn,
2213
2482
  idleMs,
2214
2483
  subagent,
2484
+ workflow,
2215
2485
  pr,
2216
2486
  cold,
2217
2487
  model,
@@ -2222,6 +2492,7 @@ function MinimapHoverCard({
2222
2492
  turn?: TurnMegaRow;
2223
2493
  idleMs?: number;
2224
2494
  subagent?: SubagentRun;
2495
+ workflow?: WorkflowRun;
2225
2496
  pr?: PrMarker;
2226
2497
  cold?: { tOffsetMs: number; info: NonNullable<SessionEvent["coldResume"]> };
2227
2498
  model?: string;
@@ -2233,6 +2504,92 @@ function MinimapHoverCard({
2233
2504
  // header, so there's never reliable space above it for a tooltip.
2234
2505
  const posStyle = { top: "calc(100% + 8px)", left };
2235
2506
 
2507
+ if (workflow) {
2508
+ const w = workflow;
2509
+ const startOff = formatOffset(w.startTOffsetMs);
2510
+ const endOff = formatOffset(w.endTOffsetMs);
2511
+ const dur = w.durationMs !== undefined ? formatGap(w.durationMs) : "";
2512
+ return (
2513
+ <div
2514
+ style={{
2515
+ position: "absolute",
2516
+ ...posStyle,
2517
+ zIndex: 100,
2518
+ background: "#0F172A",
2519
+ color: "#F1F5F9",
2520
+ borderRadius: 10,
2521
+ padding: "12px 14px",
2522
+ fontSize: 11,
2523
+ pointerEvents: "none",
2524
+ boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
2525
+ maxWidth: 440,
2526
+ minWidth: 280,
2527
+ lineHeight: 1.45,
2528
+ }}
2529
+ >
2530
+ <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
2531
+ <span
2532
+ style={{
2533
+ fontSize: 10,
2534
+ fontWeight: 600,
2535
+ padding: "2px 8px",
2536
+ borderRadius: 4,
2537
+ background: workflowColor(w.status),
2538
+ color: "#fff",
2539
+ textTransform: "uppercase",
2540
+ letterSpacing: "0.04em",
2541
+ }}
2542
+ >
2543
+ workflow · {w.status}
2544
+ </span>
2545
+ <span
2546
+ style={{
2547
+ fontFamily: "var(--font-mono)",
2548
+ fontSize: 10,
2549
+ opacity: 0.7,
2550
+ marginLeft: "auto",
2551
+ }}
2552
+ >
2553
+ {startOff} → {endOff} · {dur}
2554
+ </span>
2555
+ </div>
2556
+ <div style={{ fontWeight: 600, marginBottom: 4, fontFamily: "var(--font-mono)" }}>
2557
+ {w.name}
2558
+ </div>
2559
+ {w.description && (
2560
+ <div style={{ opacity: 0.8, marginBottom: 6 }}>{w.description}</div>
2561
+ )}
2562
+ <div
2563
+ style={{
2564
+ fontSize: 10,
2565
+ opacity: 0.75,
2566
+ display: "flex",
2567
+ gap: 8,
2568
+ flexWrap: "wrap",
2569
+ paddingTop: 6,
2570
+ borderTop: "1px solid rgba(241,245,249,0.08)",
2571
+ fontFamily: "var(--font-mono)",
2572
+ }}
2573
+ >
2574
+ <span style={{ color: "#FB923C", fontWeight: 600 }}>{w.agentCount} agents</span>
2575
+ <span>·</span>
2576
+ <span>{w.toolCallCount.toLocaleString()} tools</span>
2577
+ <span>·</span>
2578
+ <span>{formatTokens(w.totalTokens)} tok</span>
2579
+ {w.phases.length > 0 && (
2580
+ <>
2581
+ <span>·</span>
2582
+ <span>{w.phases.length} phases</span>
2583
+ </>
2584
+ )}
2585
+ </div>
2586
+ <div style={{ marginTop: 6, fontSize: 10, opacity: 0.55, fontStyle: "italic" }}>
2587
+ Click to open this run in the Workflows tab.
2588
+ </div>
2589
+ </div>
2590
+ );
2591
+ }
2592
+
2236
2593
  if (subagent) {
2237
2594
  const startOff = formatOffset(subagent.startTOffsetMs);
2238
2595
  const endOff = formatOffset(subagent.endTOffsetMs);
@@ -4466,6 +4823,924 @@ function SubagentDrawer({
4466
4823
  );
4467
4824
  }
4468
4825
 
4826
+ /* ------------------------------------------------------------------ */
4827
+ /* Workflows panel */
4828
+ /* */
4829
+ /* A single `Workflow` tool call collapses a whole dynamic-workflow */
4830
+ /* fan-out into one transcript row. This panel surfaces what that row */
4831
+ /* actually did: each run's spawned-agent count, tool calls, tokens, */
4832
+ /* phases, and progress log — the fleet work the transcript hides. */
4833
+ /* ------------------------------------------------------------------ */
4834
+
4835
+ function WorkflowsPanel({
4836
+ workflows,
4837
+ spawnedAgentCount,
4838
+ focusRunId,
4839
+ selectedAgentKey,
4840
+ onOpenAgent,
4841
+ onJumpToParent,
4842
+ }: {
4843
+ workflows: WorkflowRun[];
4844
+ spawnedAgentCount: number;
4845
+ focusRunId?: string | null;
4846
+ selectedAgentKey?: string | null;
4847
+ onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
4848
+ onJumpToParent: (toolUseId?: string) => void;
4849
+ }) {
4850
+ const totalTokens = workflows.reduce((n, w) => n + w.totalTokens, 0);
4851
+ const totalTools = workflows.reduce((n, w) => n + w.toolCallCount, 0);
4852
+ return (
4853
+ <section
4854
+ style={{
4855
+ marginBottom: 16,
4856
+ border: "1px solid var(--af-border-subtle)",
4857
+ borderRadius: 10,
4858
+ background: "var(--af-surface)",
4859
+ overflow: "hidden",
4860
+ }}
4861
+ >
4862
+ <div
4863
+ style={{
4864
+ display: "flex",
4865
+ alignItems: "center",
4866
+ gap: 10,
4867
+ flexWrap: "wrap",
4868
+ padding: "10px 14px",
4869
+ borderBottom: "1px solid var(--af-border-subtle)",
4870
+ background: "var(--af-surface-subtle, rgba(234,88,12,0.04))",
4871
+ }}
4872
+ >
4873
+ <span
4874
+ style={{
4875
+ display: "inline-flex",
4876
+ alignItems: "center",
4877
+ gap: 7,
4878
+ fontSize: 13,
4879
+ fontWeight: 600,
4880
+ color: "var(--af-text)",
4881
+ }}
4882
+ >
4883
+ <WorkflowIcon size={14} style={{ color: "#EA580C" }} />
4884
+ Workflows
4885
+ </span>
4886
+ <span
4887
+ style={{
4888
+ fontSize: 11.5,
4889
+ color: "var(--af-text-tertiary)",
4890
+ fontFamily: "var(--font-mono)",
4891
+ }}
4892
+ >
4893
+ {workflows.length} run{workflows.length === 1 ? "" : "s"} ·{" "}
4894
+ {spawnedAgentCount.toLocaleString()} agents · {totalTools.toLocaleString()} tool calls ·{" "}
4895
+ {formatTokens(totalTokens)} tok orchestrated
4896
+ </span>
4897
+ </div>
4898
+ <div style={{ display: "flex", flexDirection: "column" }}>
4899
+ {workflows.map((w) => (
4900
+ <WorkflowCard
4901
+ key={w.runId}
4902
+ w={w}
4903
+ focused={focusRunId === w.runId}
4904
+ selectedAgentKey={selectedAgentKey}
4905
+ onOpenAgent={onOpenAgent}
4906
+ onJumpToParent={onJumpToParent}
4907
+ />
4908
+ ))}
4909
+ </div>
4910
+ </section>
4911
+ );
4912
+ }
4913
+
4914
+ function WorkflowCard({
4915
+ w,
4916
+ focused = false,
4917
+ selectedAgentKey,
4918
+ onOpenAgent,
4919
+ onJumpToParent,
4920
+ }: {
4921
+ w: WorkflowRun;
4922
+ focused?: boolean;
4923
+ selectedAgentKey?: string | null;
4924
+ onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
4925
+ onJumpToParent: (toolUseId?: string) => void;
4926
+ }) {
4927
+ const [open, setOpen] = useState(focused);
4928
+ const cardRef = useRef<HTMLDivElement>(null);
4929
+ // Focused via a minimap lane click — expand and scroll this run into view.
4930
+ useEffect(() => {
4931
+ if (focused) {
4932
+ setOpen(true);
4933
+ cardRef.current?.scrollIntoView({ block: "center", behavior: "smooth" });
4934
+ }
4935
+ }, [focused]);
4936
+ const color = workflowColor(w.status);
4937
+ const dur = w.durationMs !== undefined ? formatGap(w.durationMs) : "—";
4938
+ const startOff = formatOffset(w.startTOffsetMs);
4939
+
4940
+ return (
4941
+ <div
4942
+ ref={cardRef}
4943
+ style={{
4944
+ borderTop: "1px solid var(--af-border-subtle)",
4945
+ background: focused ? "rgba(234,88,12,0.07)" : "transparent",
4946
+ transition: "background 0.5s ease",
4947
+ scrollMarginTop: 120,
4948
+ }}
4949
+ >
4950
+ {/* Collapsed header — click to expand */}
4951
+ <button
4952
+ onClick={() => setOpen((v) => !v)}
4953
+ style={{
4954
+ width: "100%",
4955
+ display: "flex",
4956
+ alignItems: "center",
4957
+ gap: 10,
4958
+ padding: "10px 14px",
4959
+ background: "transparent",
4960
+ border: "none",
4961
+ cursor: "pointer",
4962
+ textAlign: "left",
4963
+ color: "inherit",
4964
+ flexWrap: "wrap",
4965
+ }}
4966
+ >
4967
+ <span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>
4968
+ {open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
4969
+ </span>
4970
+ <span
4971
+ style={{
4972
+ fontFamily: "var(--font-mono)",
4973
+ fontSize: 13,
4974
+ fontWeight: 600,
4975
+ color: "var(--af-text)",
4976
+ minWidth: 0,
4977
+ overflow: "hidden",
4978
+ textOverflow: "ellipsis",
4979
+ whiteSpace: "nowrap",
4980
+ }}
4981
+ title={w.name}
4982
+ >
4983
+ {w.name}
4984
+ </span>
4985
+ <span
4986
+ style={{
4987
+ fontSize: 9.5,
4988
+ fontWeight: 600,
4989
+ padding: "2px 7px",
4990
+ borderRadius: 100,
4991
+ background: color,
4992
+ color: "#fff",
4993
+ textTransform: "uppercase",
4994
+ letterSpacing: "0.04em",
4995
+ }}
4996
+ >
4997
+ {w.status}
4998
+ </span>
4999
+ <span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 12 }}>
5000
+ <span
5001
+ style={{
5002
+ fontSize: 12,
5003
+ fontWeight: 700,
5004
+ padding: "2px 10px",
5005
+ borderRadius: 100,
5006
+ background: "rgba(234,88,12,0.14)",
5007
+ color: "#EA580C",
5008
+ fontFamily: "var(--font-mono)",
5009
+ whiteSpace: "nowrap",
5010
+ }}
5011
+ >
5012
+ {w.agentCount} agents
5013
+ </span>
5014
+ <WfMiniStat icon={<Wrench size={11} />} value={`${w.toolCallCount.toLocaleString()}`} />
5015
+ <WfMiniStat value={`${formatTokens(w.totalTokens)} tok`} />
5016
+ <WfMiniStat icon={<Clock size={11} />} value={dur} />
5017
+ </span>
5018
+ </button>
5019
+
5020
+ {/* Expanded detail */}
5021
+ {open && (
5022
+ <div style={{ padding: "0 14px 14px 38px", display: "flex", flexDirection: "column", gap: 12 }}>
5023
+ {w.description && (
5024
+ <div style={{ fontSize: 12.5, color: "var(--af-text-secondary)", lineHeight: 1.5 }}>
5025
+ {w.description}
5026
+ </div>
5027
+ )}
5028
+
5029
+ <div
5030
+ style={{
5031
+ display: "flex",
5032
+ gap: 14,
5033
+ flexWrap: "wrap",
5034
+ fontSize: 11,
5035
+ fontFamily: "var(--font-mono)",
5036
+ color: "var(--af-text-tertiary)",
5037
+ }}
5038
+ >
5039
+ <span>starts at {startOff}</span>
5040
+ {w.model && <span>· {w.model}</span>}
5041
+ <span>· runId {w.runId}</span>
5042
+ {w.parentToolUseId && (
5043
+ <button
5044
+ type="button"
5045
+ onClick={() => onJumpToParent(w.parentToolUseId)}
5046
+ style={{
5047
+ marginLeft: "auto",
5048
+ fontSize: 11,
5049
+ color: "var(--af-accent)",
5050
+ background: "transparent",
5051
+ border: "none",
5052
+ cursor: "pointer",
5053
+ padding: 0,
5054
+ fontFamily: "var(--font-mono)",
5055
+ }}
5056
+ >
5057
+ Jump to Workflow call →
5058
+ </button>
5059
+ )}
5060
+ </div>
5061
+
5062
+ {w.agents.length > 0 ? (
5063
+ <WorkflowPhaseTabs
5064
+ w={w}
5065
+ selectedAgentKey={selectedAgentKey}
5066
+ onOpenAgent={onOpenAgent}
5067
+ />
5068
+ ) : (
5069
+ w.phases.length > 0 && (
5070
+ <div>
5071
+ <SectionLabel>Phases</SectionLabel>
5072
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
5073
+ {w.phases.map((p, i) => (
5074
+ <span
5075
+ key={`${p.title}-${i}`}
5076
+ title={p.detail}
5077
+ style={{
5078
+ display: "inline-flex",
5079
+ alignItems: "center",
5080
+ gap: 5,
5081
+ fontSize: 11,
5082
+ padding: "3px 9px",
5083
+ borderRadius: 4,
5084
+ background: "var(--af-border-subtle)",
5085
+ color: "var(--af-text)",
5086
+ }}
5087
+ >
5088
+ <b style={{ fontWeight: 600, fontFamily: "var(--font-mono)" }}>{p.title}</b>
5089
+ {p.detail && (
5090
+ <span style={{ color: "var(--af-text-tertiary)", maxWidth: 360, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
5091
+ {p.detail}
5092
+ </span>
5093
+ )}
5094
+ </span>
5095
+ ))}
5096
+ </div>
5097
+ </div>
5098
+ )
5099
+ )}
5100
+
5101
+ {w.logs.length > 0 && <WorkflowRunLog logs={w.logs} />}
5102
+ </div>
5103
+ )}
5104
+ </div>
5105
+ );
5106
+ }
5107
+
5108
+ /** Compact model label: "claude-opus-4-8[1m]" → "opus-4-8", "claude-haiku-4-5-20251001" → "haiku-4-5". */
5109
+ function shortenModel(model: string): string {
5110
+ return model
5111
+ .replace(/^claude-/, "")
5112
+ .replace(/\[1m\]$/, "")
5113
+ .replace(/-\d{6,}$/, "")
5114
+ .trim();
5115
+ }
5116
+
5117
+ /** Status dot color for a single workflow-spawned agent. */
5118
+ function agentStateColor(state?: string): string {
5119
+ const s = (state ?? "").toLowerCase();
5120
+ if (s === "done" || s === "completed" || s === "success") return "#10B981";
5121
+ if (s === "error" || s === "failed") return "#EF4444";
5122
+ if (s === "running" || s === "active" || s === "in_progress") return "#F59E0B";
5123
+ return "#8A8580";
5124
+ }
5125
+
5126
+ /** Per-phase tabs for a workflow run. Each tab lists the agents (actions)
5127
+ * that ran in that phase so you can review what actually happened. */
5128
+ function WorkflowPhaseTabs({
5129
+ w,
5130
+ selectedAgentKey,
5131
+ onOpenAgent,
5132
+ }: {
5133
+ w: WorkflowRun;
5134
+ selectedAgentKey?: string | null;
5135
+ onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
5136
+ }) {
5137
+ // Build the phase list from meta.phases (ordered), bucketing agents by
5138
+ // phaseIndex. Agents whose phaseIndex isn't in meta.phases fall into an
5139
+ // appended bucket keyed by their phaseTitle so nothing is dropped.
5140
+ const groups = useMemo(() => {
5141
+ const byIndex = new Map<number, WorkflowRun["agents"]>();
5142
+ const extras = new Map<string, WorkflowRun["agents"]>();
5143
+ for (const a of w.agents) {
5144
+ if (a.phaseIndex && a.phaseIndex >= 1 && a.phaseIndex <= w.phases.length) {
5145
+ const arr = byIndex.get(a.phaseIndex) ?? [];
5146
+ arr.push(a);
5147
+ byIndex.set(a.phaseIndex, arr);
5148
+ } else {
5149
+ const key = a.phaseTitle ?? "Other";
5150
+ const arr = extras.get(key) ?? [];
5151
+ arr.push(a);
5152
+ extras.set(key, arr);
5153
+ }
5154
+ }
5155
+ const out = w.phases.map((p, i) => ({
5156
+ key: `p${i + 1}`,
5157
+ title: p.title,
5158
+ detail: p.detail,
5159
+ agents: byIndex.get(i + 1) ?? [],
5160
+ }));
5161
+ for (const [title, agents] of extras) {
5162
+ out.push({ key: `x-${title}`, title, detail: undefined, agents });
5163
+ }
5164
+ return out;
5165
+ }, [w]);
5166
+
5167
+ // Default to the first phase that actually has agents.
5168
+ const firstWithAgents = groups.find((g) => g.agents.length > 0)?.key ?? groups[0]?.key ?? "";
5169
+ const [active, setActive] = useState(firstWithAgents);
5170
+ const activeGroup = groups.find((g) => g.key === active) ?? groups[0];
5171
+
5172
+ return (
5173
+ <div>
5174
+ <SectionLabel>Phases &amp; actions</SectionLabel>
5175
+ {/* Phase tab strip */}
5176
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 10 }}>
5177
+ {groups.map((g) => {
5178
+ const isActive = g.key === active;
5179
+ return (
5180
+ <button
5181
+ key={g.key}
5182
+ onClick={() => setActive(g.key)}
5183
+ style={{
5184
+ display: "inline-flex",
5185
+ alignItems: "center",
5186
+ gap: 6,
5187
+ fontSize: 11.5,
5188
+ fontWeight: isActive ? 600 : 500,
5189
+ padding: "4px 10px",
5190
+ borderRadius: 6,
5191
+ border: "1px solid",
5192
+ borderColor: isActive ? "#EA580C" : "var(--af-border-subtle)",
5193
+ background: isActive ? "rgba(234,88,12,0.12)" : "transparent",
5194
+ color: isActive ? "#EA580C" : "var(--af-text-secondary)",
5195
+ cursor: "pointer",
5196
+ fontFamily: "var(--font-mono)",
5197
+ }}
5198
+ >
5199
+ {g.title}
5200
+ <span
5201
+ style={{
5202
+ fontSize: 10,
5203
+ fontWeight: 700,
5204
+ padding: "0px 5px",
5205
+ borderRadius: 100,
5206
+ background: isActive ? "#EA580C" : "var(--af-border-subtle)",
5207
+ color: isActive ? "#fff" : "var(--af-text-tertiary)",
5208
+ }}
5209
+ >
5210
+ {g.agents.length}
5211
+ </span>
5212
+ </button>
5213
+ );
5214
+ })}
5215
+ </div>
5216
+
5217
+ {/* Active phase body */}
5218
+ {activeGroup && (
5219
+ <div>
5220
+ {activeGroup.detail && (
5221
+ <div
5222
+ style={{
5223
+ fontSize: 11.5,
5224
+ color: "var(--af-text-tertiary)",
5225
+ marginBottom: 8,
5226
+ fontStyle: "italic",
5227
+ }}
5228
+ >
5229
+ {activeGroup.detail}
5230
+ </div>
5231
+ )}
5232
+ {activeGroup.agents.length === 0 ? (
5233
+ <div style={{ fontSize: 12, color: "var(--af-text-tertiary)", padding: "8px 0" }}>
5234
+ No agents recorded for this phase.
5235
+ </div>
5236
+ ) : (
5237
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
5238
+ {activeGroup.agents.map((a) => (
5239
+ <WorkflowAgentRow
5240
+ key={`${a.index}-${a.agentId ?? a.label}`}
5241
+ a={a}
5242
+ selected={selectedAgentKey === `${w.runId}:${a.agentId ?? a.index}`}
5243
+ onOpen={() => onOpenAgent(w.runId, a)}
5244
+ />
5245
+ ))}
5246
+ </div>
5247
+ )}
5248
+ </div>
5249
+ )}
5250
+ </div>
5251
+ );
5252
+ }
5253
+
5254
+ /** One agent (action) inside a phase — collapsed shows label + metering;
5255
+ * expanding reveals the task prompt and the returned result. */
5256
+ /** Full transcript detail for one workflow agent — fetched on demand from
5257
+ * /api/workflow-agent. Mirrors the parser's WorkflowAgentDetail. */
5258
+ type WfAgentDetail = {
5259
+ prompt?: string;
5260
+ finalText?: string;
5261
+ steps: { index: number; tool: string; preview: string; full?: string; isError?: boolean }[];
5262
+ toolCalls: { name: string; count: number }[];
5263
+ toolCallCount: number;
5264
+ assistantMessageCount: number;
5265
+ eventCount: number;
5266
+ totalUsage: { input: number; output: number; cacheRead: number; cacheWrite: number };
5267
+ model?: string;
5268
+ durationMs?: number;
5269
+ };
5270
+
5271
+ function WorkflowAgentRow({
5272
+ a,
5273
+ selected,
5274
+ onOpen,
5275
+ }: {
5276
+ a: WorkflowRun["agents"][number];
5277
+ selected: boolean;
5278
+ onOpen: () => void;
5279
+ }) {
5280
+ const dur = a.durationMs !== undefined ? formatGap(a.durationMs) : undefined;
5281
+ return (
5282
+ <button
5283
+ onClick={onOpen}
5284
+ style={{
5285
+ width: "100%",
5286
+ display: "flex",
5287
+ alignItems: "center",
5288
+ gap: 8,
5289
+ padding: "8px 10px",
5290
+ textAlign: "left",
5291
+ cursor: "pointer",
5292
+ border: "1px solid",
5293
+ borderColor: selected ? "#EA580C" : "var(--af-border-subtle)",
5294
+ borderRadius: 6,
5295
+ background: selected
5296
+ ? "rgba(234,88,12,0.08)"
5297
+ : "var(--af-surface-subtle, rgba(120,115,108,0.04))",
5298
+ color: "inherit",
5299
+ flexWrap: "wrap",
5300
+ }}
5301
+ >
5302
+ <span
5303
+ title={a.state}
5304
+ style={{ width: 7, height: 7, borderRadius: "50%", background: agentStateColor(a.state), flexShrink: 0 }}
5305
+ />
5306
+ <span
5307
+ style={{
5308
+ fontFamily: "var(--font-mono)",
5309
+ fontSize: 12,
5310
+ fontWeight: 600,
5311
+ color: "var(--af-text)",
5312
+ minWidth: 0,
5313
+ overflow: "hidden",
5314
+ textOverflow: "ellipsis",
5315
+ whiteSpace: "nowrap",
5316
+ maxWidth: 280,
5317
+ }}
5318
+ title={a.label}
5319
+ >
5320
+ {a.label}
5321
+ </span>
5322
+ {a.model && (
5323
+ <span style={{ fontSize: 10, color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)" }}>
5324
+ {shortenModel(a.model)}
5325
+ </span>
5326
+ )}
5327
+ {a.lastToolSummary && (
5328
+ <span
5329
+ style={{
5330
+ fontSize: 11,
5331
+ color: "var(--af-text-tertiary)",
5332
+ overflow: "hidden",
5333
+ textOverflow: "ellipsis",
5334
+ whiteSpace: "nowrap",
5335
+ maxWidth: 240,
5336
+ }}
5337
+ title={a.lastToolSummary}
5338
+ >
5339
+ → {a.lastToolSummary}
5340
+ </span>
5341
+ )}
5342
+ <span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
5343
+ {dur && <WfMiniStat icon={<Clock size={10} />} value={dur} />}
5344
+ {a.tokens !== undefined && <WfMiniStat value={`${formatTokens(a.tokens)} tok`} />}
5345
+ {a.toolCalls !== undefined && <WfMiniStat icon={<Wrench size={10} />} value={`${a.toolCalls}`} />}
5346
+ <span style={{ color: selected ? "#EA580C" : "var(--af-text-tertiary)", display: "inline-flex" }}>
5347
+ <ChevronRight size={14} />
5348
+ </span>
5349
+ </span>
5350
+ </button>
5351
+ );
5352
+ }
5353
+
5354
+ /** Right side-sheet showing one workflow agent's full transcript — Task,
5355
+ * ordered Steps, and Result — fetched on demand. Kept out of the card so the
5356
+ * Workflows tab stays short even for 100+ step agents. */
5357
+ function WorkflowAgentDrawer({
5358
+ sessionId,
5359
+ runId,
5360
+ agent,
5361
+ onClose,
5362
+ }: {
5363
+ sessionId: string;
5364
+ runId: string;
5365
+ agent: WorkflowRun["agents"][number];
5366
+ onClose: () => void;
5367
+ }) {
5368
+ const [detail, setDetail] = useState<WfAgentDetail | null>(null);
5369
+ const [loading, setLoading] = useState(false);
5370
+ const [failed, setFailed] = useState(false);
5371
+
5372
+ useEffect(() => {
5373
+ setDetail(null);
5374
+ setFailed(false);
5375
+ if (!agent.agentId) return;
5376
+ setLoading(true);
5377
+ const ctrl = new AbortController();
5378
+ const url = `/api/workflow-agent?session=${encodeURIComponent(sessionId)}&run=${encodeURIComponent(runId)}&agent=${encodeURIComponent(agent.agentId)}`;
5379
+ fetch(url, { signal: ctrl.signal })
5380
+ .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
5381
+ .then((d: WfAgentDetail) => setDetail(d))
5382
+ .catch((e) => {
5383
+ if (e?.name !== "AbortError") setFailed(true);
5384
+ })
5385
+ .finally(() => setLoading(false));
5386
+ return () => ctrl.abort();
5387
+ }, [sessionId, runId, agent.agentId]);
5388
+
5389
+ const prompt = detail?.prompt ?? agent.promptPreview;
5390
+ const finalText = detail?.finalText ?? agent.resultPreview;
5391
+ const dur = detail?.durationMs ?? agent.durationMs;
5392
+ const model = detail?.model ?? agent.model;
5393
+ const toolCount = detail?.toolCallCount ?? agent.toolCalls;
5394
+ const tok = detail ? detail.totalUsage.input + detail.totalUsage.output : agent.tokens;
5395
+
5396
+ return (
5397
+ <div>
5398
+ {/* Sticky title bar */}
5399
+ <div
5400
+ style={{
5401
+ padding: "14px 18px",
5402
+ borderBottom: "1px solid var(--af-border-subtle)",
5403
+ display: "flex",
5404
+ alignItems: "center",
5405
+ gap: 10,
5406
+ background: "var(--af-surface)",
5407
+ position: "sticky",
5408
+ top: 0,
5409
+ zIndex: 1,
5410
+ }}
5411
+ >
5412
+ <span
5413
+ title={agent.state}
5414
+ style={{ width: 8, height: 8, borderRadius: "50%", background: agentStateColor(agent.state), flexShrink: 0 }}
5415
+ />
5416
+ <span
5417
+ style={{
5418
+ fontSize: 13.5,
5419
+ fontWeight: 600,
5420
+ fontFamily: "var(--font-mono)",
5421
+ color: "var(--af-text)",
5422
+ flex: 1,
5423
+ minWidth: 0,
5424
+ overflow: "hidden",
5425
+ textOverflow: "ellipsis",
5426
+ whiteSpace: "nowrap",
5427
+ }}
5428
+ title={agent.label}
5429
+ >
5430
+ {agent.label}
5431
+ </span>
5432
+ <button
5433
+ onClick={onClose}
5434
+ style={{ background: "transparent", border: "none", cursor: "pointer", color: "var(--af-text-tertiary)", padding: 4 }}
5435
+ aria-label="Close"
5436
+ >
5437
+ <X size={16} />
5438
+ </button>
5439
+ </div>
5440
+
5441
+ {/* Meta strip */}
5442
+ <div
5443
+ style={{
5444
+ padding: "9px 18px",
5445
+ fontFamily: "var(--font-mono)",
5446
+ fontSize: 11,
5447
+ color: "var(--af-text-tertiary)",
5448
+ borderBottom: "1px solid var(--af-border-subtle)",
5449
+ display: "flex",
5450
+ gap: 12,
5451
+ flexWrap: "wrap",
5452
+ }}
5453
+ >
5454
+ {agent.phaseTitle && <span>phase {agent.phaseTitle}</span>}
5455
+ {agent.state && <span>· {agent.state}</span>}
5456
+ {model && <span>· {shortenModel(model)}</span>}
5457
+ {dur !== undefined && <span>· {formatGap(dur)}</span>}
5458
+ {toolCount !== undefined && <span>· {toolCount} tools</span>}
5459
+ {tok !== undefined && <span>· {formatTokens(tok)} tok</span>}
5460
+ </div>
5461
+
5462
+ {/* Body — each section collapsed by default; click to expand. */}
5463
+ <div style={{ padding: "12px 18px 18px", display: "flex", flexDirection: "column", gap: 8 }}>
5464
+ {loading && (
5465
+ <div style={{ fontSize: 11.5, color: "var(--af-text-tertiary)", fontStyle: "italic" }}>
5466
+ Loading full transcript…
5467
+ </div>
5468
+ )}
5469
+ {failed && (
5470
+ <div style={{ fontSize: 11.5, color: "var(--af-text-tertiary)", fontStyle: "italic" }}>
5471
+ Full transcript unavailable — showing the journal summary.
5472
+ </div>
5473
+ )}
5474
+
5475
+ {prompt && (
5476
+ <DrawerCollapsible title="Task" hint={detail ? undefined : "preview"}>
5477
+ <div style={{ fontSize: 12, color: "var(--af-text-secondary)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>
5478
+ {prompt}
5479
+ </div>
5480
+ </DrawerCollapsible>
5481
+ )}
5482
+
5483
+ {detail && detail.steps.length > 0 && (
5484
+ <DrawerCollapsible
5485
+ title="Steps"
5486
+ count={detail.steps.length}
5487
+ hint={detail.toolCalls.slice(0, 6).map((t) => `${shortenToolName(t.name)} ×${t.count}`).join(" · ")}
5488
+ >
5489
+ <div style={{ border: "1px solid var(--af-border-subtle)", borderRadius: 6, background: "var(--af-surface)" }}>
5490
+ {detail.steps.map((s) => (
5491
+ <WorkflowStepRow key={s.index} s={s} />
5492
+ ))}
5493
+ </div>
5494
+ </DrawerCollapsible>
5495
+ )}
5496
+
5497
+ {finalText && (
5498
+ <DrawerCollapsible title="Result" hint={detail ? undefined : "preview"}>
5499
+ <div
5500
+ style={{
5501
+ fontFamily: "var(--font-mono)",
5502
+ fontSize: 11,
5503
+ color: "var(--af-text-secondary)",
5504
+ whiteSpace: "pre-wrap",
5505
+ wordBreak: "break-word",
5506
+ lineHeight: 1.5,
5507
+ background: "var(--af-surface)",
5508
+ border: "1px solid var(--af-border-subtle)",
5509
+ borderRadius: 4,
5510
+ padding: "8px 10px",
5511
+ }}
5512
+ >
5513
+ {finalText}
5514
+ </div>
5515
+ </DrawerCollapsible>
5516
+ )}
5517
+ </div>
5518
+ </div>
5519
+ );
5520
+ }
5521
+
5522
+ /** Collapsible section inside the agent sheet — default collapsed. */
5523
+ function DrawerCollapsible({
5524
+ title,
5525
+ count,
5526
+ hint,
5527
+ defaultOpen = false,
5528
+ children,
5529
+ }: {
5530
+ title: string;
5531
+ count?: number;
5532
+ hint?: string;
5533
+ defaultOpen?: boolean;
5534
+ children: React.ReactNode;
5535
+ }) {
5536
+ const [open, setOpen] = useState(defaultOpen);
5537
+ return (
5538
+ <div style={{ border: "1px solid var(--af-border-subtle)", borderRadius: 8, overflow: "hidden" }}>
5539
+ <button
5540
+ onClick={() => setOpen((v) => !v)}
5541
+ style={{
5542
+ width: "100%",
5543
+ display: "flex",
5544
+ alignItems: "center",
5545
+ gap: 8,
5546
+ padding: "9px 12px",
5547
+ background: open ? "var(--af-surface-subtle, rgba(120,115,108,0.05))" : "transparent",
5548
+ border: "none",
5549
+ cursor: "pointer",
5550
+ textAlign: "left",
5551
+ color: "inherit",
5552
+ }}
5553
+ >
5554
+ <span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>
5555
+ {open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
5556
+ </span>
5557
+ <span style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--af-text-secondary)" }}>
5558
+ {title}
5559
+ </span>
5560
+ {count !== undefined && (
5561
+ <span
5562
+ style={{
5563
+ fontSize: 10,
5564
+ fontWeight: 700,
5565
+ padding: "1px 7px",
5566
+ borderRadius: 100,
5567
+ background: "rgba(234,88,12,0.14)",
5568
+ color: "#EA580C",
5569
+ fontFamily: "var(--font-mono)",
5570
+ }}
5571
+ >
5572
+ {count}
5573
+ </span>
5574
+ )}
5575
+ {hint && (
5576
+ <span
5577
+ style={{
5578
+ marginLeft: "auto",
5579
+ fontSize: 10.5,
5580
+ color: "var(--af-text-tertiary)",
5581
+ fontFamily: "var(--font-mono)",
5582
+ overflow: "hidden",
5583
+ textOverflow: "ellipsis",
5584
+ whiteSpace: "nowrap",
5585
+ maxWidth: 280,
5586
+ }}
5587
+ >
5588
+ {hint}
5589
+ </span>
5590
+ )}
5591
+ </button>
5592
+ {open && <div style={{ padding: "0 12px 12px" }}>{children}</div>}
5593
+ </div>
5594
+ );
5595
+ }
5596
+
5597
+ /** One step row — click to reveal the full tool input (e.g. the complete
5598
+ * multi-line bash command) when it's longer than the one-line preview. */
5599
+ function WorkflowStepRow({ s }: { s: WfAgentDetail["steps"][number] }) {
5600
+ const [open, setOpen] = useState(false);
5601
+ const expandable = !!s.full;
5602
+ return (
5603
+ <div style={{ borderTop: s.index === 1 ? "none" : "1px solid var(--af-border-subtle)" }}>
5604
+ <div
5605
+ onClick={() => expandable && setOpen((v) => !v)}
5606
+ style={{
5607
+ display: "flex",
5608
+ alignItems: "baseline",
5609
+ gap: 8,
5610
+ padding: "4px 10px",
5611
+ fontFamily: "var(--font-mono)",
5612
+ fontSize: 11,
5613
+ cursor: expandable ? "pointer" : "default",
5614
+ }}
5615
+ >
5616
+ <span style={{ color: "var(--af-text-tertiary)", minWidth: 28, textAlign: "right", flexShrink: 0 }}>
5617
+ {s.index}
5618
+ </span>
5619
+ <span style={{ fontWeight: 600, color: s.isError ? "#EF4444" : "var(--af-text)", flexShrink: 0, minWidth: 104 }}>
5620
+ {s.isError ? "⚠ " : ""}
5621
+ {shortenToolName(s.tool)}
5622
+ </span>
5623
+ <span
5624
+ style={{ flex: 1, color: "var(--af-text-tertiary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
5625
+ title={s.preview}
5626
+ >
5627
+ {s.preview}
5628
+ </span>
5629
+ {expandable && (
5630
+ <span style={{ color: "var(--af-text-tertiary)", display: "inline-flex", flexShrink: 0 }}>
5631
+ {open ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
5632
+ </span>
5633
+ )}
5634
+ </div>
5635
+ {open && s.full && (
5636
+ <pre
5637
+ style={{
5638
+ margin: 0,
5639
+ padding: "6px 10px 10px 40px",
5640
+ fontFamily: "var(--font-mono)",
5641
+ fontSize: 11,
5642
+ whiteSpace: "pre-wrap",
5643
+ wordBreak: "break-word",
5644
+ color: "var(--af-text-secondary)",
5645
+ background: "var(--af-surface-subtle, rgba(120,115,108,0.05))",
5646
+ lineHeight: 1.5,
5647
+ }}
5648
+ >
5649
+ {s.full}
5650
+ </pre>
5651
+ )}
5652
+ </div>
5653
+ );
5654
+ }
5655
+
5656
+ /** Collapsible coarse run log (▶ / ✓ task markers) — secondary to the
5657
+ * per-phase agent view. */
5658
+ function WorkflowRunLog({ logs }: { logs: string[] }) {
5659
+ const [open, setOpen] = useState(false);
5660
+ return (
5661
+ <div>
5662
+ <button
5663
+ onClick={() => setOpen((v) => !v)}
5664
+ style={{
5665
+ display: "inline-flex",
5666
+ alignItems: "center",
5667
+ gap: 5,
5668
+ fontSize: 10,
5669
+ fontWeight: 600,
5670
+ textTransform: "uppercase",
5671
+ letterSpacing: "0.04em",
5672
+ color: "var(--af-text-tertiary)",
5673
+ background: "transparent",
5674
+ border: "none",
5675
+ cursor: "pointer",
5676
+ padding: 0,
5677
+ }}
5678
+ >
5679
+ {open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
5680
+ Run log ({logs.length})
5681
+ </button>
5682
+ {open && (
5683
+ <div
5684
+ style={{
5685
+ marginTop: 6,
5686
+ fontFamily: "var(--font-mono)",
5687
+ fontSize: 11.5,
5688
+ lineHeight: 1.7,
5689
+ color: "var(--af-text-secondary)",
5690
+ background: "var(--af-surface-subtle, rgba(120,115,108,0.05))",
5691
+ border: "1px solid var(--af-border-subtle)",
5692
+ borderRadius: 6,
5693
+ padding: "8px 12px",
5694
+ maxHeight: 240,
5695
+ overflowY: "auto",
5696
+ whiteSpace: "pre-wrap",
5697
+ }}
5698
+ >
5699
+ {logs.map((l, i) => (
5700
+ <div key={i}>{l}</div>
5701
+ ))}
5702
+ </div>
5703
+ )}
5704
+ </div>
5705
+ );
5706
+ }
5707
+
5708
+ function WfMiniStat({ icon, value }: { icon?: React.ReactNode; value: string }) {
5709
+ return (
5710
+ <span
5711
+ style={{
5712
+ display: "inline-flex",
5713
+ alignItems: "center",
5714
+ gap: 4,
5715
+ fontSize: 11.5,
5716
+ color: "var(--af-text-secondary)",
5717
+ fontFamily: "var(--font-mono)",
5718
+ whiteSpace: "nowrap",
5719
+ }}
5720
+ >
5721
+ {icon && <span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>{icon}</span>}
5722
+ {value}
5723
+ </span>
5724
+ );
5725
+ }
5726
+
5727
+ function SectionLabel({ children }: { children: React.ReactNode }) {
5728
+ return (
5729
+ <div
5730
+ style={{
5731
+ fontSize: 10,
5732
+ fontWeight: 600,
5733
+ textTransform: "uppercase",
5734
+ letterSpacing: "0.04em",
5735
+ color: "var(--af-text-tertiary)",
5736
+ marginBottom: 8,
5737
+ }}
5738
+ >
5739
+ {children}
5740
+ </div>
5741
+ );
5742
+ }
5743
+
4469
5744
  function StatCell({ label, value }: { label: string; value: string }) {
4470
5745
  return (
4471
5746
  <div>
@@ -4903,8 +6178,11 @@ function formatDurationHeader(ms: number): string {
4903
6178
  const s = Math.round(ms / 1000);
4904
6179
  if (s < 60) return `${s}s`;
4905
6180
  const m = Math.floor(s / 60);
4906
- const rem = s % 60;
4907
- return `${m}m ${rem}s`;
6181
+ if (m < 60) return `${m}m ${s % 60}s`;
6182
+ // Roll up to hours — agent time can reach many hours once workflow
6183
+ // execution is folded in, and "1116m" reads poorly.
6184
+ const h = Math.floor(m / 60);
6185
+ return `${h}h ${m % 60}m`;
4908
6186
  }
4909
6187
 
4910
6188
  function EntryDayStrip({ entry }: { entry: Entry }) {