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,637 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import type {
6
+ GanttDay,
7
+ GanttSession,
8
+ ParallelismBurst,
9
+ } from "@claude-lens/parser";
10
+ import { formatDuration, prettyProjectName } from "@/lib/format";
11
+ import { OutcomePill } from "@/components/outcome-pill";
12
+ import { fmtTime, sessionColor, stripXml } from "./gantt-chart-utils";
13
+ import type { SessionEntrySummary } from "./gantt-chart";
14
+
15
+ export function BurstDetailModal({
16
+ burst,
17
+ gantt,
18
+ sessionEntries,
19
+ onClose,
20
+ onShowInTimeline,
21
+ }: {
22
+ burst: ParallelismBurst;
23
+ gantt: GanttDay;
24
+ sessionEntries: Record<string, SessionEntrySummary>;
25
+ onClose: () => void;
26
+ onShowInTimeline: () => void;
27
+ }) {
28
+ // Close on ESC.
29
+ useEffect(() => {
30
+ const onKey = (e: KeyboardEvent) => {
31
+ if (e.key === "Escape") onClose();
32
+ };
33
+ window.addEventListener("keydown", onKey);
34
+ return () => window.removeEventListener("keydown", onKey);
35
+ }, [onClose]);
36
+
37
+ // Track which session card should be visually focused. Clicking a
38
+ // numbered track badge in the mini-Gantt sets this, scrolls the card
39
+ // into view, and paints it with a highlighted border.
40
+ const [focusedSessionId, setFocusedSessionId] = useState<string | null>(null);
41
+ const cardRefs = useRef<Map<string, HTMLAnchorElement>>(new Map());
42
+
43
+ // Resolve involved sessions from the Gantt index, preserving the order
44
+ // from the burst's sessionIds list.
45
+ const sessionById = useMemo(() => {
46
+ const m = new Map<string, GanttSession>();
47
+ for (const s of gantt.sessions) m.set(s.id, s);
48
+ return m;
49
+ }, [gantt]);
50
+
51
+ const involved: GanttSession[] = burst.sessionIds
52
+ .map((id) => sessionById.get(id))
53
+ .filter((s): s is GanttSession => !!s);
54
+
55
+ // Sort by start time ascending so the timeline reads top-to-bottom.
56
+ involved.sort((a, b) => a.startMs - b.startMs);
57
+
58
+ const handleFocusTrack = (sessionId: string) => {
59
+ setFocusedSessionId(sessionId);
60
+ const card = cardRefs.current.get(sessionId);
61
+ if (card) {
62
+ card.scrollIntoView({ behavior: "smooth", block: "center" });
63
+ }
64
+ };
65
+
66
+ // Build the mini-Gantt time range: expand to show the full day span of
67
+ // involved sessions so you can tell "this session started way earlier,
68
+ // the burst was just a slice". Padded by 15 min each side.
69
+ const GANTT_PAD_MS = 15 * 60 * 1000;
70
+ const rawEarliest = Math.min(
71
+ burst.startMs,
72
+ ...involved.map((s) => s.startMs),
73
+ );
74
+ const rawLatest = Math.max(
75
+ burst.endMs,
76
+ ...involved.map((s) => s.endMs),
77
+ );
78
+ const rangeStartMs = rawEarliest - GANTT_PAD_MS;
79
+ const rangeEndMs = rawLatest + GANTT_PAD_MS;
80
+ const rangeDuration = rangeEndMs - rangeStartMs;
81
+
82
+ // Layout for the mini-Gantt.
83
+ const MINI_CHART_WIDTH = 510;
84
+ const MINI_ROW_HEIGHT = 22;
85
+ const MINI_ROW_GAP = 3;
86
+ const MINI_HEADER_HEIGHT = 22;
87
+ // Left column for numbered track badges (1, 2, 3 …). Clicks here focus
88
+ // the corresponding session card in the list below.
89
+ const NUMBER_COL_WIDTH = 24;
90
+ const miniSvgWidth = NUMBER_COL_WIDTH + MINI_CHART_WIDTH;
91
+ const miniBodyHeight =
92
+ involved.length * (MINI_ROW_HEIGHT + MINI_ROW_GAP) - MINI_ROW_GAP;
93
+ const miniTotalHeight = MINI_HEADER_HEIGHT + miniBodyHeight + 6;
94
+
95
+ const msToMiniX = (ms: number): number => {
96
+ return ((ms - rangeStartMs) / rangeDuration) * MINI_CHART_WIDTH;
97
+ };
98
+
99
+ // Hour tick marks within the mini-gantt range.
100
+ const miniHourMarks: number[] = [];
101
+ {
102
+ const cur = new Date(rangeStartMs);
103
+ cur.setMinutes(0, 0, 0);
104
+ if (cur.getTime() < rangeStartMs) cur.setHours(cur.getHours() + 1);
105
+ while (cur.getTime() <= rangeEndMs) {
106
+ miniHourMarks.push(cur.getTime());
107
+ cur.setHours(cur.getHours() + 1);
108
+ }
109
+ }
110
+
111
+ // Burst shaded band (the actual concurrency window).
112
+ const burstX1 = msToMiniX(burst.startMs);
113
+ const burstX2 = msToMiniX(burst.endMs);
114
+
115
+ // For each session, compute "active time inside the burst window"
116
+ // by clipping its segments to [burst.startMs, burst.endMs] and summing.
117
+ const activeInBurst = (s: GanttSession): number => {
118
+ let sum = 0;
119
+ for (const seg of s.segments) {
120
+ const a = Math.max(seg.startMs, burst.startMs);
121
+ const b = Math.min(seg.endMs, burst.endMs);
122
+ if (b > a) sum += b - a;
123
+ }
124
+ return sum;
125
+ };
126
+
127
+ const totalDurationMs = burst.endMs - burst.startMs;
128
+
129
+ return (
130
+ <div
131
+ role="dialog"
132
+ aria-modal="true"
133
+ aria-labelledby="burst-detail-title"
134
+ onClick={onClose}
135
+ style={{
136
+ position: "fixed",
137
+ inset: 0,
138
+ zIndex: 200,
139
+ background: "rgba(0, 0, 0, 0.55)",
140
+ backdropFilter: "blur(4px)",
141
+ WebkitBackdropFilter: "blur(4px)",
142
+ display: "flex",
143
+ alignItems: "center",
144
+ justifyContent: "center",
145
+ padding: 20,
146
+ }}
147
+ >
148
+ <div
149
+ onClick={(e) => e.stopPropagation()}
150
+ style={{
151
+ background: "var(--af-surface-elevated)",
152
+ border: "1px solid var(--af-border-subtle)",
153
+ borderRadius: 12,
154
+ width: "min(720px, 100%)",
155
+ maxHeight: "90vh",
156
+ display: "flex",
157
+ flexDirection: "column",
158
+ boxShadow: "0 20px 60px rgba(0, 0, 0, 0.4)",
159
+ }}
160
+ >
161
+ {/* Header */}
162
+ <div
163
+ style={{
164
+ display: "flex",
165
+ alignItems: "center",
166
+ gap: 14,
167
+ padding: "16px 22px",
168
+ borderBottom: "1px solid var(--af-border-subtle)",
169
+ }}
170
+ >
171
+ <div style={{ flex: 1, minWidth: 0 }}>
172
+ <div
173
+ style={{
174
+ display: "flex",
175
+ alignItems: "center",
176
+ gap: 10,
177
+ marginBottom: 4,
178
+ }}
179
+ >
180
+ <span
181
+ style={{
182
+ display: "inline-flex",
183
+ alignItems: "center",
184
+ padding: "3px 10px",
185
+ borderRadius: 10,
186
+ fontFamily: "var(--font-mono)",
187
+ fontSize: 11,
188
+ fontWeight: 700,
189
+ color: "white",
190
+ background: burst.crossProject
191
+ ? "rgba(167, 139, 250, 0.95)"
192
+ : "rgba(45, 212, 191, 0.95)",
193
+ }}
194
+ >
195
+ ×{burst.peak} peak
196
+ </span>
197
+ {burst.crossProject && (
198
+ <span
199
+ style={{
200
+ fontSize: 9,
201
+ color: "rgba(167, 139, 250, 1)",
202
+ textTransform: "uppercase",
203
+ letterSpacing: "0.05em",
204
+ fontWeight: 700,
205
+ }}
206
+ >
207
+ cross-project
208
+ </span>
209
+ )}
210
+ </div>
211
+ <h2
212
+ id="burst-detail-title"
213
+ style={{
214
+ margin: 0,
215
+ fontSize: 16,
216
+ fontWeight: 700,
217
+ letterSpacing: "-0.01em",
218
+ display: "flex",
219
+ alignItems: "center",
220
+ gap: 10,
221
+ flexWrap: "wrap",
222
+ }}
223
+ >
224
+ <span style={{ fontFamily: "var(--font-mono)" }}>
225
+ {fmtTime(burst.startMs)}–{fmtTime(burst.endMs)}
226
+ </span>
227
+ <span
228
+ style={{
229
+ fontSize: 12,
230
+ fontWeight: 400,
231
+ color: "var(--af-text-tertiary)",
232
+ fontFamily: "var(--font-mono)",
233
+ }}
234
+ >
235
+ {formatDuration(totalDurationMs)}
236
+ </span>
237
+ <span
238
+ style={{
239
+ fontSize: 12,
240
+ fontWeight: 400,
241
+ color: "var(--af-text-tertiary)",
242
+ }}
243
+ >
244
+ · {involved.length} session{involved.length === 1 ? "" : "s"}
245
+ </span>
246
+ </h2>
247
+ </div>
248
+ <button
249
+ type="button"
250
+ onClick={onShowInTimeline}
251
+ style={{
252
+ padding: "6px 12px",
253
+ background: "transparent",
254
+ border: "1px solid var(--af-border-subtle)",
255
+ borderRadius: 6,
256
+ fontSize: 11,
257
+ color: "var(--af-text-secondary)",
258
+ cursor: "pointer",
259
+ fontWeight: 500,
260
+ whiteSpace: "nowrap",
261
+ }}
262
+ title="Pin this burst on the main timeline and close"
263
+ >
264
+ Show in timeline →
265
+ </button>
266
+ <button
267
+ type="button"
268
+ onClick={onClose}
269
+ aria-label="Close"
270
+ style={{
271
+ background: "transparent",
272
+ border: "none",
273
+ color: "var(--af-text-tertiary)",
274
+ fontSize: 24,
275
+ lineHeight: 1,
276
+ cursor: "pointer",
277
+ padding: 0,
278
+ width: 28,
279
+ height: 28,
280
+ }}
281
+ >
282
+ ×
283
+ </button>
284
+ </div>
285
+
286
+ {/* Body — scrolls */}
287
+ <div style={{ overflow: "auto", padding: "18px 22px" }}>
288
+ {/* Mini-Gantt */}
289
+ <div
290
+ style={{
291
+ fontSize: 10,
292
+ color: "var(--af-text-tertiary)",
293
+ textTransform: "uppercase",
294
+ letterSpacing: "0.05em",
295
+ marginBottom: 8,
296
+ fontWeight: 600,
297
+ }}
298
+ >
299
+ Timeline (shaded = burst window)
300
+ </div>
301
+ <div
302
+ style={{
303
+ background: "var(--af-surface)",
304
+ border: "1px solid var(--af-border-subtle)",
305
+ borderRadius: 8,
306
+ padding: "8px 10px",
307
+ overflow: "auto",
308
+ }}
309
+ >
310
+ <svg
311
+ width={miniSvgWidth}
312
+ height={miniTotalHeight}
313
+ style={{ display: "block" }}
314
+ >
315
+ {/* Numbered track badges (left column, not translated) */}
316
+ {involved.map((s, i) => {
317
+ const y = MINI_HEADER_HEIGHT + i * (MINI_ROW_HEIGHT + MINI_ROW_GAP);
318
+ const cy = y + MINI_ROW_HEIGHT / 2;
319
+ const color = sessionColor(s);
320
+ const isFocused = focusedSessionId === s.id;
321
+ return (
322
+ <g
323
+ key={`num-${s.id}`}
324
+ onClick={() => handleFocusTrack(s.id)}
325
+ style={{ cursor: "pointer" }}
326
+ >
327
+ <title>{`Track ${i + 1} — click to focus in list below`}</title>
328
+ <circle
329
+ cx={NUMBER_COL_WIDTH / 2}
330
+ cy={cy}
331
+ r={9}
332
+ fill={isFocused ? color : "var(--af-surface-elevated)"}
333
+ stroke={color}
334
+ strokeWidth={isFocused ? 2 : 1.5}
335
+ />
336
+ <text
337
+ x={NUMBER_COL_WIDTH / 2}
338
+ y={cy + 3.5}
339
+ textAnchor="middle"
340
+ fontSize={10}
341
+ fontWeight={700}
342
+ fill={isFocused ? "#fff" : "var(--af-text)"}
343
+ style={{ pointerEvents: "none" }}
344
+ >
345
+ {i + 1}
346
+ </text>
347
+ </g>
348
+ );
349
+ })}
350
+
351
+ {/* Chart content — translated right of the number column */}
352
+ <g transform={`translate(${NUMBER_COL_WIDTH}, 0)`}>
353
+ {/* Burst shaded band */}
354
+ <rect
355
+ x={burstX1}
356
+ y={0}
357
+ width={Math.max(burstX2 - burstX1, 2)}
358
+ height={miniTotalHeight}
359
+ fill={
360
+ burst.crossProject
361
+ ? "rgba(167, 139, 250, 0.14)"
362
+ : "rgba(45, 212, 191, 0.14)"
363
+ }
364
+ />
365
+ {/* Burst band outline */}
366
+ <line
367
+ x1={burstX1}
368
+ x2={burstX1}
369
+ y1={0}
370
+ y2={miniTotalHeight}
371
+ stroke={
372
+ burst.crossProject
373
+ ? "rgba(167, 139, 250, 0.7)"
374
+ : "rgba(45, 212, 191, 0.7)"
375
+ }
376
+ strokeWidth={1}
377
+ strokeDasharray="3 2"
378
+ />
379
+ <line
380
+ x1={burstX2}
381
+ x2={burstX2}
382
+ y1={0}
383
+ y2={miniTotalHeight}
384
+ stroke={
385
+ burst.crossProject
386
+ ? "rgba(167, 139, 250, 0.7)"
387
+ : "rgba(45, 212, 191, 0.7)"
388
+ }
389
+ strokeWidth={1}
390
+ strokeDasharray="3 2"
391
+ />
392
+
393
+ {/* Hour marks */}
394
+ {miniHourMarks.map((ms) => {
395
+ const x = msToMiniX(ms);
396
+ return (
397
+ <g key={`mini-hm-${ms}`}>
398
+ <line
399
+ x1={x}
400
+ x2={x}
401
+ y1={MINI_HEADER_HEIGHT}
402
+ y2={miniTotalHeight}
403
+ stroke="var(--af-border-subtle)"
404
+ strokeWidth={0.5}
405
+ strokeDasharray="2 3"
406
+ />
407
+ <text
408
+ x={x}
409
+ y={MINI_HEADER_HEIGHT - 6}
410
+ textAnchor="middle"
411
+ fontSize={9}
412
+ fill="var(--af-text-tertiary)"
413
+ >
414
+ {fmtTime(ms)}
415
+ </text>
416
+ </g>
417
+ );
418
+ })}
419
+
420
+ {/* Session rows — clickable via an invisible row hit rect */}
421
+ {involved.map((s, i) => {
422
+ const y = MINI_HEADER_HEIGHT + i * (MINI_ROW_HEIGHT + MINI_ROW_GAP);
423
+ const color = sessionColor(s);
424
+ const isFocused = focusedSessionId === s.id;
425
+ return (
426
+ <g key={`mini-row-${s.id}`}>
427
+ {/* Row hit target — click the whole row to focus */}
428
+ <rect
429
+ x={0}
430
+ y={y}
431
+ width={MINI_CHART_WIDTH}
432
+ height={MINI_ROW_HEIGHT}
433
+ fill={
434
+ isFocused
435
+ ? "rgba(255,255,255,0.05)"
436
+ : "transparent"
437
+ }
438
+ onClick={() => handleFocusTrack(s.id)}
439
+ style={{ cursor: "pointer" }}
440
+ />
441
+ {s.segments.map((seg, si) => {
442
+ const x1 = msToMiniX(seg.startMs);
443
+ const x2 = msToMiniX(seg.endMs);
444
+ const w = Math.max(x2 - x1, 2);
445
+ return (
446
+ <rect
447
+ key={`mini-seg-${si}`}
448
+ x={x1}
449
+ y={y + 3}
450
+ width={w}
451
+ height={MINI_ROW_HEIGHT - 6}
452
+ fill={color}
453
+ rx={2}
454
+ stroke={isFocused ? "var(--af-text)" : "none"}
455
+ strokeWidth={isFocused ? 1 : 0}
456
+ style={{ pointerEvents: "none" }}
457
+ />
458
+ );
459
+ })}
460
+ </g>
461
+ );
462
+ })}
463
+ </g>
464
+ </svg>
465
+ </div>
466
+
467
+ {/* Session cards */}
468
+ <div
469
+ style={{
470
+ fontSize: 10,
471
+ color: "var(--af-text-tertiary)",
472
+ textTransform: "uppercase",
473
+ letterSpacing: "0.05em",
474
+ margin: "18px 0 8px",
475
+ fontWeight: 600,
476
+ }}
477
+ >
478
+ Sessions in this burst
479
+ </div>
480
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
481
+ {involved.map((s, i) => {
482
+ const color = sessionColor(s);
483
+ const activeMs = activeInBurst(s);
484
+ const activePct = totalDurationMs > 0
485
+ ? Math.round((activeMs / totalDurationMs) * 100)
486
+ : 0;
487
+ const isFocused = focusedSessionId === s.id;
488
+ return (
489
+ <Link
490
+ key={`burst-session-${s.id}`}
491
+ href={`/sessions/${s.id}`}
492
+ ref={(el) => {
493
+ if (el) cardRefs.current.set(s.id, el);
494
+ else cardRefs.current.delete(s.id);
495
+ }}
496
+ style={{
497
+ display: "block",
498
+ padding: "11px 14px",
499
+ background: isFocused
500
+ ? "var(--af-surface-hover)"
501
+ : "var(--af-surface)",
502
+ border: isFocused
503
+ ? "1px solid var(--af-text-tertiary)"
504
+ : "1px solid var(--af-border-subtle)",
505
+ borderLeft: `3px solid ${color}`,
506
+ borderRadius: 8,
507
+ textDecoration: "none",
508
+ color: "var(--af-text)",
509
+ fontSize: 12,
510
+ lineHeight: 1.45,
511
+ transition: "background 0.15s, border-color 0.15s",
512
+ boxShadow: isFocused
513
+ ? `0 0 0 2px ${color}`
514
+ : "none",
515
+ }}
516
+ >
517
+ <div
518
+ style={{
519
+ display: "flex",
520
+ alignItems: "flex-start",
521
+ justifyContent: "space-between",
522
+ gap: 10,
523
+ }}
524
+ >
525
+ {/* Numbered track badge matching the mini-Gantt row */}
526
+ <span
527
+ style={{
528
+ display: "inline-flex",
529
+ alignItems: "center",
530
+ justifyContent: "center",
531
+ width: 20,
532
+ height: 20,
533
+ borderRadius: "50%",
534
+ background: isFocused ? color : "transparent",
535
+ border: `1.5px solid ${color}`,
536
+ color: isFocused ? "#fff" : "var(--af-text)",
537
+ fontSize: 10,
538
+ fontWeight: 700,
539
+ flexShrink: 0,
540
+ marginTop: 2,
541
+ }}
542
+ aria-label={`Track ${i + 1}`}
543
+ >
544
+ {i + 1}
545
+ </span>
546
+ <div style={{ minWidth: 0, flex: 1 }}>
547
+ <div
548
+ style={{
549
+ fontSize: 11,
550
+ color: "var(--af-text-tertiary)",
551
+ fontFamily: "var(--font-mono)",
552
+ marginBottom: 3,
553
+ overflow: "hidden",
554
+ textOverflow: "ellipsis",
555
+ whiteSpace: "nowrap",
556
+ }}
557
+ title={prettyProjectName(s.projectName)}
558
+ >
559
+ {(() => {
560
+ const name = prettyProjectName(s.projectName);
561
+ const parts = name.split("/").filter(Boolean);
562
+ return parts.length > 2
563
+ ? "…/" + parts.slice(-2).join("/")
564
+ : name;
565
+ })()}
566
+ </div>
567
+ <div
568
+ style={{
569
+ fontWeight: 500,
570
+ overflow: "hidden",
571
+ textOverflow: "ellipsis",
572
+ whiteSpace: "nowrap",
573
+ }}
574
+ title={stripXml(s.firstUserPreview ?? "")}
575
+ >
576
+ {stripXml(s.firstUserPreview ?? "") || (
577
+ <em style={{ color: "var(--af-text-tertiary)" }}>
578
+ (no user message)
579
+ </em>
580
+ )}
581
+ </div>
582
+ {s.lastAgentPreview && (
583
+ <div
584
+ style={{
585
+ fontSize: 11,
586
+ color: "var(--af-text-secondary)",
587
+ marginTop: 3,
588
+ overflow: "hidden",
589
+ textOverflow: "ellipsis",
590
+ whiteSpace: "nowrap",
591
+ fontStyle: "italic",
592
+ }}
593
+ title={stripXml(s.lastAgentPreview)}
594
+ >
595
+ ↳ {stripXml(s.lastAgentPreview)}
596
+ </div>
597
+ )}
598
+ </div>
599
+ <div
600
+ style={{
601
+ fontFamily: "var(--font-mono)",
602
+ fontSize: 10,
603
+ color: "var(--af-text-tertiary)",
604
+ textAlign: "right",
605
+ whiteSpace: "nowrap",
606
+ flexShrink: 0,
607
+ display: "flex",
608
+ flexDirection: "column",
609
+ alignItems: "flex-end",
610
+ gap: 4,
611
+ }}
612
+ >
613
+ {(() => {
614
+ const sum = sessionEntries[s.id];
615
+ if (sum?.outcome) {
616
+ return <OutcomePill outcome={sum.outcome} size="sm" label="text" agent={s.agent} />;
617
+ }
618
+ return null;
619
+ })()}
620
+ <div style={{ color: "var(--af-text-secondary)" }}>
621
+ {formatDuration(activeMs)}{" "}
622
+ <span style={{ opacity: 0.6 }}>({activePct}%)</span>
623
+ </div>
624
+ <div style={{ fontSize: 9, marginTop: 2 }}>
625
+ in burst
626
+ </div>
627
+ </div>
628
+ </div>
629
+ </Link>
630
+ );
631
+ })}
632
+ </div>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ );
637
+ }