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
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { useMemo, useRef, useState } from "react";
5
5
  import {
6
6
  canonicalProjectName,
7
7
  type GanttDay,
@@ -12,6 +12,21 @@ import type { DayOutcome, EntryEnrichmentStatus } from "@claude-lens/entries";
12
12
  import { formatDuration, formatTokens, prettyProjectName } from "@/lib/format";
13
13
  import { OutcomePill } from "@/components/outcome-pill";
14
14
  import { AgentIcon } from "@/components/agent-icon";
15
+ import {
16
+ BURST_RIBBON_HEIGHT,
17
+ HEADER_HEIGHT,
18
+ LABEL_WIDTH,
19
+ MIN_CHART_WIDTH,
20
+ PAD_MS,
21
+ ROW_GAP,
22
+ ROW_HEIGHT,
23
+ fmtTime,
24
+ projectColor,
25
+ sessionColor,
26
+ stripXml,
27
+ } from "./gantt-chart-utils";
28
+ import { ConcurrencyInfoModal } from "./concurrency-info-modal";
29
+ import { BurstDetailModal } from "./burst-detail-modal";
15
30
 
16
31
  export type SessionEntrySummary = {
17
32
  outcome: DayOutcome | null;
@@ -20,54 +35,6 @@ export type SessionEntrySummary = {
20
35
  localDay: string;
21
36
  };
22
37
 
23
- const ROW_HEIGHT = 30;
24
- const ROW_GAP = 3;
25
- const LABEL_WIDTH = 220;
26
- const HEADER_HEIGHT = 26;
27
- const BURST_RIBBON_HEIGHT = 24;
28
- const MIN_CHART_WIDTH = 700;
29
- const PAD_MS = 30 * 60 * 1000; // 30-min padding on each side
30
-
31
- const PROJECT_COLORS = [
32
- "rgba(45, 212, 191, 0.75)",
33
- "rgba(167, 139, 250, 0.75)",
34
- "rgba(248, 113, 113, 0.75)",
35
- "rgba(52, 211, 153, 0.75)",
36
- "rgba(251, 191, 36, 0.75)",
37
- "rgba(236, 72, 153, 0.75)",
38
- "rgba(34, 211, 238, 0.75)",
39
- "rgba(168, 85, 247, 0.75)",
40
- "rgba(244, 114, 82, 0.75)",
41
- "rgba(96, 165, 250, 0.75)",
42
- ];
43
-
44
- /**
45
- * Hash a project key to a stable color. The key should be a canonical
46
- * project identity (not a raw projectDir) so all worktrees of the same
47
- * repo share one color — visually grouping "this repo's parallel work".
48
- */
49
- function projectColor(projectKey: string): string {
50
- let hash = 0;
51
- for (let i = 0; i < projectKey.length; i++) {
52
- hash = ((hash << 5) - hash + projectKey.charCodeAt(i)) | 0;
53
- }
54
- return PROJECT_COLORS[Math.abs(hash) % PROJECT_COLORS.length]!;
55
- }
56
-
57
- /** Color for a Gantt session, keyed by canonical project name. */
58
- function sessionColor(s: { projectName: string }): string {
59
- return projectColor(canonicalProjectName(s.projectName));
60
- }
61
-
62
- function stripXml(s: string): string {
63
- return s.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
64
- }
65
-
66
- function fmtTime(ms: number): string {
67
- const d = new Date(ms);
68
- return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
69
- }
70
-
71
38
  export function GanttChart({
72
39
  gantt,
73
40
  bursts = [],
@@ -77,6 +44,13 @@ export function GanttChart({
77
44
  bursts?: ParallelismBurst[];
78
45
  sessionEntries?: Record<string, SessionEntrySummary>;
79
46
  }) {
47
+ // The gantt is scoped to one local day; carry that day on session links so a
48
+ // multi-day session opens pinned to THIS day instead of its most-recent one.
49
+ // gantt.date is already that zero-padded `YYYY-MM-DD` local-day key (the same
50
+ // one the session view buckets on), so use it directly rather than round-
51
+ // tripping dayStartMs back through a timezone-sensitive Date.
52
+ const dayParam = gantt.date;
53
+
80
54
  const [hover, setHover] = useState<{
81
55
  session: GanttSession;
82
56
  x: number;
@@ -647,7 +621,7 @@ export function GanttChart({
647
621
  <AgentIcon agent={session.agent} size={11} />
648
622
 
649
623
  <Link
650
- href={`/sessions/${session.id}`}
624
+ href={`/sessions/${session.id}?day=${dayParam}`}
651
625
  style={{
652
626
  fontSize: 10.5,
653
627
  color: "var(--af-text)",
@@ -919,856 +893,3 @@ export function GanttChart({
919
893
  </>
920
894
  );
921
895
  }
922
-
923
- function ConcurrencyInfoModal({ onClose }: { onClose: () => void }) {
924
- // Close on ESC.
925
- useEffect(() => {
926
- const onKey = (e: KeyboardEvent) => {
927
- if (e.key === "Escape") onClose();
928
- };
929
- window.addEventListener("keydown", onKey);
930
- return () => window.removeEventListener("keydown", onKey);
931
- }, [onClose]);
932
-
933
- return (
934
- <div
935
- role="dialog"
936
- aria-modal="true"
937
- aria-labelledby="concurrency-modal-title"
938
- onClick={onClose}
939
- style={{
940
- position: "fixed",
941
- inset: 0,
942
- zIndex: 200,
943
- background: "rgba(0, 0, 0, 0.5)",
944
- backdropFilter: "blur(4px)",
945
- WebkitBackdropFilter: "blur(4px)",
946
- display: "flex",
947
- alignItems: "center",
948
- justifyContent: "center",
949
- padding: 20,
950
- }}
951
- >
952
- <div
953
- onClick={(e) => e.stopPropagation()}
954
- style={{
955
- background: "var(--af-surface-elevated)",
956
- border: "1px solid var(--af-border-subtle)",
957
- borderRadius: 12,
958
- maxWidth: 560,
959
- width: "100%",
960
- maxHeight: "85vh",
961
- overflow: "auto",
962
- boxShadow: "0 16px 48px rgba(0, 0, 0, 0.3)",
963
- }}
964
- >
965
- <div
966
- style={{
967
- display: "flex",
968
- alignItems: "center",
969
- justifyContent: "space-between",
970
- padding: "18px 22px",
971
- borderBottom: "1px solid var(--af-border-subtle)",
972
- }}
973
- >
974
- <h2
975
- id="concurrency-modal-title"
976
- style={{
977
- margin: 0,
978
- fontSize: 16,
979
- fontWeight: 700,
980
- letterSpacing: "-0.01em",
981
- }}
982
- >
983
- How concurrency is measured
984
- </h2>
985
- <button
986
- type="button"
987
- onClick={onClose}
988
- aria-label="Close"
989
- style={{
990
- background: "transparent",
991
- border: "none",
992
- color: "var(--af-text-tertiary)",
993
- fontSize: 22,
994
- lineHeight: 1,
995
- cursor: "pointer",
996
- padding: 0,
997
- width: 28,
998
- height: 28,
999
- }}
1000
- >
1001
- ×
1002
- </button>
1003
- </div>
1004
-
1005
- <div
1006
- style={{
1007
- padding: "18px 22px",
1008
- fontSize: 13,
1009
- color: "var(--af-text-secondary)",
1010
- lineHeight: 1.55,
1011
- }}
1012
- >
1013
- <p style={{ margin: "0 0 14px" }}>
1014
- A <strong style={{ color: "var(--af-text)" }}>concurrency burst</strong>{" "}
1015
- is a window of time when two or more Claude Code sessions were
1016
- actively working in parallel. The goal is to surface moments
1017
- when you were running a multi-agent fleet — not every accidental
1018
- tab overlap.
1019
- </p>
1020
-
1021
- <h3
1022
- style={{
1023
- margin: "18px 0 8px",
1024
- fontSize: 12,
1025
- fontWeight: 700,
1026
- color: "var(--af-text)",
1027
- textTransform: "uppercase",
1028
- letterSpacing: "0.04em",
1029
- }}
1030
- >
1031
- 1. What counts as &quot;active&quot;
1032
- </h3>
1033
- <p style={{ margin: "0 0 6px" }}>
1034
- Each session&apos;s events are split into{" "}
1035
- <strong style={{ color: "var(--af-text)" }}>active segments</strong> —
1036
- stretches of time where no gap between consecutive events exceeds{" "}
1037
- <strong style={{ color: "var(--af-text)" }}>3 minutes</strong>. Any longer
1038
- gap (walked away, laptop closed, thinking) splits the session
1039
- into separate segments.
1040
- </p>
1041
- <p style={{ margin: "0 0 6px", fontSize: 12, color: "var(--af-text-tertiary)" }}>
1042
- This is the same definition used for the &quot;active time&quot; metric on
1043
- the dashboard, so numbers stay consistent.
1044
- </p>
1045
-
1046
- <h3
1047
- style={{
1048
- margin: "18px 0 8px",
1049
- fontSize: 12,
1050
- fontWeight: 700,
1051
- color: "var(--af-text)",
1052
- textTransform: "uppercase",
1053
- letterSpacing: "0.04em",
1054
- }}
1055
- >
1056
- 2. Detecting overlap
1057
- </h3>
1058
- <p style={{ margin: "0 0 6px" }}>
1059
- A sweep-line walks every segment start/end across all sessions.
1060
- Whenever the active count is ≥2, that stretch becomes a raw
1061
- overlap. The <strong style={{ color: "var(--af-text)" }}>peak</strong>{" "}
1062
- reported on each burst is the maximum active count reached
1063
- anywhere inside it.
1064
- </p>
1065
-
1066
- <h3
1067
- style={{
1068
- margin: "18px 0 8px",
1069
- fontSize: 12,
1070
- fontWeight: 700,
1071
- color: "var(--af-text)",
1072
- textTransform: "uppercase",
1073
- letterSpacing: "0.04em",
1074
- }}
1075
- >
1076
- 3. Cleaning up noise
1077
- </h3>
1078
- <p style={{ margin: "0 0 6px" }}>
1079
- Raw overlaps are noisy — a typical day produces dozens of
1080
- sub-minute artifacts from switching between sessions. Two rules
1081
- collapse them into human-readable bursts:
1082
- </p>
1083
- <ul style={{ margin: "6px 0 0 0", paddingLeft: 22 }}>
1084
- <li style={{ marginBottom: 4 }}>
1085
- <strong style={{ color: "var(--af-text)" }}>Drop under 1 minute</strong> —
1086
- overlaps shorter than that are almost always tab-switch
1087
- artifacts, not real parallel work.
1088
- </li>
1089
- <li>
1090
- <strong style={{ color: "var(--af-text)" }}>Merge within 10 minutes</strong> —
1091
- two overlaps separated by less than 10 minutes of idle time
1092
- fuse into a single burst. A morning of back-and-forth agent
1093
- work becomes one burst, not forty.
1094
- </li>
1095
- </ul>
1096
-
1097
- <h3
1098
- style={{
1099
- margin: "18px 0 8px",
1100
- fontSize: 12,
1101
- fontWeight: 700,
1102
- color: "var(--af-text)",
1103
- textTransform: "uppercase",
1104
- letterSpacing: "0.04em",
1105
- }}
1106
- >
1107
- 4. Same-project vs cross-project
1108
- </h3>
1109
- <p style={{ margin: "0 0 6px" }}>
1110
- Bursts are colored by whether the involved sessions span more
1111
- than one project directory.{" "}
1112
- <span style={{ color: "rgba(167, 139, 250, 1)", fontWeight: 600 }}>
1113
- Purple = cross-project
1114
- </span>{" "}
1115
- — different repos running at once, usually genuine fleet work.{" "}
1116
- <span style={{ color: "rgba(45, 212, 191, 1)", fontWeight: 600 }}>
1117
- Teal = same-project
1118
- </span>{" "}
1119
- — multiple sessions in one repo, usually context-switching
1120
- inside a single task.
1121
- </p>
1122
-
1123
- <h3
1124
- style={{
1125
- margin: "18px 0 8px",
1126
- fontSize: 12,
1127
- fontWeight: 700,
1128
- color: "var(--af-text)",
1129
- textTransform: "uppercase",
1130
- letterSpacing: "0.04em",
1131
- }}
1132
- >
1133
- Why bursts, not runs
1134
- </h3>
1135
- <p style={{ margin: "0 0 6px" }}>
1136
- An earlier version reported every raw overlap as a separate
1137
- &quot;parallel run&quot;. On a busy day that produced 40–80 entries,
1138
- most of them seconds long, most of them meaningless. Bursts are
1139
- the unit humans actually think in: &quot;this morning I was
1140
- running 3 agents at once for about 20 minutes.&quot;
1141
- </p>
1142
- </div>
1143
- </div>
1144
- </div>
1145
- );
1146
- }
1147
-
1148
- /* ================================================================== */
1149
- /* Burst detail modal */
1150
- /* ================================================================== */
1151
-
1152
- function BurstDetailModal({
1153
- burst,
1154
- gantt,
1155
- sessionEntries,
1156
- onClose,
1157
- onShowInTimeline,
1158
- }: {
1159
- burst: ParallelismBurst;
1160
- gantt: GanttDay;
1161
- sessionEntries: Record<string, SessionEntrySummary>;
1162
- onClose: () => void;
1163
- onShowInTimeline: () => void;
1164
- }) {
1165
- // Close on ESC.
1166
- useEffect(() => {
1167
- const onKey = (e: KeyboardEvent) => {
1168
- if (e.key === "Escape") onClose();
1169
- };
1170
- window.addEventListener("keydown", onKey);
1171
- return () => window.removeEventListener("keydown", onKey);
1172
- }, [onClose]);
1173
-
1174
- // Track which session card should be visually focused. Clicking a
1175
- // numbered track badge in the mini-Gantt sets this, scrolls the card
1176
- // into view, and paints it with a highlighted border.
1177
- const [focusedSessionId, setFocusedSessionId] = useState<string | null>(null);
1178
- const cardRefs = useRef<Map<string, HTMLAnchorElement>>(new Map());
1179
-
1180
- // Resolve involved sessions from the Gantt index, preserving the order
1181
- // from the burst's sessionIds list.
1182
- const sessionById = useMemo(() => {
1183
- const m = new Map<string, GanttSession>();
1184
- for (const s of gantt.sessions) m.set(s.id, s);
1185
- return m;
1186
- }, [gantt]);
1187
-
1188
- const involved: GanttSession[] = burst.sessionIds
1189
- .map((id) => sessionById.get(id))
1190
- .filter((s): s is GanttSession => !!s);
1191
-
1192
- // Sort by start time ascending so the timeline reads top-to-bottom.
1193
- involved.sort((a, b) => a.startMs - b.startMs);
1194
-
1195
- const handleFocusTrack = (sessionId: string) => {
1196
- setFocusedSessionId(sessionId);
1197
- const card = cardRefs.current.get(sessionId);
1198
- if (card) {
1199
- card.scrollIntoView({ behavior: "smooth", block: "center" });
1200
- }
1201
- };
1202
-
1203
- // Build the mini-Gantt time range: expand to show the full day span of
1204
- // involved sessions so you can tell "this session started way earlier,
1205
- // the burst was just a slice". Padded by 15 min each side.
1206
- const GANTT_PAD_MS = 15 * 60 * 1000;
1207
- const rawEarliest = Math.min(
1208
- burst.startMs,
1209
- ...involved.map((s) => s.startMs),
1210
- );
1211
- const rawLatest = Math.max(
1212
- burst.endMs,
1213
- ...involved.map((s) => s.endMs),
1214
- );
1215
- const rangeStartMs = rawEarliest - GANTT_PAD_MS;
1216
- const rangeEndMs = rawLatest + GANTT_PAD_MS;
1217
- const rangeDuration = rangeEndMs - rangeStartMs;
1218
-
1219
- // Layout for the mini-Gantt.
1220
- const MINI_CHART_WIDTH = 510;
1221
- const MINI_ROW_HEIGHT = 22;
1222
- const MINI_ROW_GAP = 3;
1223
- const MINI_HEADER_HEIGHT = 22;
1224
- // Left column for numbered track badges (1, 2, 3 …). Clicks here focus
1225
- // the corresponding session card in the list below.
1226
- const NUMBER_COL_WIDTH = 24;
1227
- const miniSvgWidth = NUMBER_COL_WIDTH + MINI_CHART_WIDTH;
1228
- const miniBodyHeight =
1229
- involved.length * (MINI_ROW_HEIGHT + MINI_ROW_GAP) - MINI_ROW_GAP;
1230
- const miniTotalHeight = MINI_HEADER_HEIGHT + miniBodyHeight + 6;
1231
-
1232
- const msToMiniX = (ms: number): number => {
1233
- return ((ms - rangeStartMs) / rangeDuration) * MINI_CHART_WIDTH;
1234
- };
1235
-
1236
- // Hour tick marks within the mini-gantt range.
1237
- const miniHourMarks: number[] = [];
1238
- {
1239
- const cur = new Date(rangeStartMs);
1240
- cur.setMinutes(0, 0, 0);
1241
- if (cur.getTime() < rangeStartMs) cur.setHours(cur.getHours() + 1);
1242
- while (cur.getTime() <= rangeEndMs) {
1243
- miniHourMarks.push(cur.getTime());
1244
- cur.setHours(cur.getHours() + 1);
1245
- }
1246
- }
1247
-
1248
- // Burst shaded band (the actual concurrency window).
1249
- const burstX1 = msToMiniX(burst.startMs);
1250
- const burstX2 = msToMiniX(burst.endMs);
1251
-
1252
- // For each session, compute "active time inside the burst window"
1253
- // by clipping its segments to [burst.startMs, burst.endMs] and summing.
1254
- const activeInBurst = (s: GanttSession): number => {
1255
- let sum = 0;
1256
- for (const seg of s.segments) {
1257
- const a = Math.max(seg.startMs, burst.startMs);
1258
- const b = Math.min(seg.endMs, burst.endMs);
1259
- if (b > a) sum += b - a;
1260
- }
1261
- return sum;
1262
- };
1263
-
1264
- const totalDurationMs = burst.endMs - burst.startMs;
1265
-
1266
- return (
1267
- <div
1268
- role="dialog"
1269
- aria-modal="true"
1270
- aria-labelledby="burst-detail-title"
1271
- onClick={onClose}
1272
- style={{
1273
- position: "fixed",
1274
- inset: 0,
1275
- zIndex: 200,
1276
- background: "rgba(0, 0, 0, 0.55)",
1277
- backdropFilter: "blur(4px)",
1278
- WebkitBackdropFilter: "blur(4px)",
1279
- display: "flex",
1280
- alignItems: "center",
1281
- justifyContent: "center",
1282
- padding: 20,
1283
- }}
1284
- >
1285
- <div
1286
- onClick={(e) => e.stopPropagation()}
1287
- style={{
1288
- background: "var(--af-surface-elevated)",
1289
- border: "1px solid var(--af-border-subtle)",
1290
- borderRadius: 12,
1291
- width: "min(720px, 100%)",
1292
- maxHeight: "90vh",
1293
- display: "flex",
1294
- flexDirection: "column",
1295
- boxShadow: "0 20px 60px rgba(0, 0, 0, 0.4)",
1296
- }}
1297
- >
1298
- {/* Header */}
1299
- <div
1300
- style={{
1301
- display: "flex",
1302
- alignItems: "center",
1303
- gap: 14,
1304
- padding: "16px 22px",
1305
- borderBottom: "1px solid var(--af-border-subtle)",
1306
- }}
1307
- >
1308
- <div style={{ flex: 1, minWidth: 0 }}>
1309
- <div
1310
- style={{
1311
- display: "flex",
1312
- alignItems: "center",
1313
- gap: 10,
1314
- marginBottom: 4,
1315
- }}
1316
- >
1317
- <span
1318
- style={{
1319
- display: "inline-flex",
1320
- alignItems: "center",
1321
- padding: "3px 10px",
1322
- borderRadius: 10,
1323
- fontFamily: "var(--font-mono)",
1324
- fontSize: 11,
1325
- fontWeight: 700,
1326
- color: "white",
1327
- background: burst.crossProject
1328
- ? "rgba(167, 139, 250, 0.95)"
1329
- : "rgba(45, 212, 191, 0.95)",
1330
- }}
1331
- >
1332
- ×{burst.peak} peak
1333
- </span>
1334
- {burst.crossProject && (
1335
- <span
1336
- style={{
1337
- fontSize: 9,
1338
- color: "rgba(167, 139, 250, 1)",
1339
- textTransform: "uppercase",
1340
- letterSpacing: "0.05em",
1341
- fontWeight: 700,
1342
- }}
1343
- >
1344
- cross-project
1345
- </span>
1346
- )}
1347
- </div>
1348
- <h2
1349
- id="burst-detail-title"
1350
- style={{
1351
- margin: 0,
1352
- fontSize: 16,
1353
- fontWeight: 700,
1354
- letterSpacing: "-0.01em",
1355
- display: "flex",
1356
- alignItems: "center",
1357
- gap: 10,
1358
- flexWrap: "wrap",
1359
- }}
1360
- >
1361
- <span style={{ fontFamily: "var(--font-mono)" }}>
1362
- {fmtTime(burst.startMs)}–{fmtTime(burst.endMs)}
1363
- </span>
1364
- <span
1365
- style={{
1366
- fontSize: 12,
1367
- fontWeight: 400,
1368
- color: "var(--af-text-tertiary)",
1369
- fontFamily: "var(--font-mono)",
1370
- }}
1371
- >
1372
- {formatDuration(totalDurationMs)}
1373
- </span>
1374
- <span
1375
- style={{
1376
- fontSize: 12,
1377
- fontWeight: 400,
1378
- color: "var(--af-text-tertiary)",
1379
- }}
1380
- >
1381
- · {involved.length} session{involved.length === 1 ? "" : "s"}
1382
- </span>
1383
- </h2>
1384
- </div>
1385
- <button
1386
- type="button"
1387
- onClick={onShowInTimeline}
1388
- style={{
1389
- padding: "6px 12px",
1390
- background: "transparent",
1391
- border: "1px solid var(--af-border-subtle)",
1392
- borderRadius: 6,
1393
- fontSize: 11,
1394
- color: "var(--af-text-secondary)",
1395
- cursor: "pointer",
1396
- fontWeight: 500,
1397
- whiteSpace: "nowrap",
1398
- }}
1399
- title="Pin this burst on the main timeline and close"
1400
- >
1401
- Show in timeline →
1402
- </button>
1403
- <button
1404
- type="button"
1405
- onClick={onClose}
1406
- aria-label="Close"
1407
- style={{
1408
- background: "transparent",
1409
- border: "none",
1410
- color: "var(--af-text-tertiary)",
1411
- fontSize: 24,
1412
- lineHeight: 1,
1413
- cursor: "pointer",
1414
- padding: 0,
1415
- width: 28,
1416
- height: 28,
1417
- }}
1418
- >
1419
- ×
1420
- </button>
1421
- </div>
1422
-
1423
- {/* Body — scrolls */}
1424
- <div style={{ overflow: "auto", padding: "18px 22px" }}>
1425
- {/* Mini-Gantt */}
1426
- <div
1427
- style={{
1428
- fontSize: 10,
1429
- color: "var(--af-text-tertiary)",
1430
- textTransform: "uppercase",
1431
- letterSpacing: "0.05em",
1432
- marginBottom: 8,
1433
- fontWeight: 600,
1434
- }}
1435
- >
1436
- Timeline (shaded = burst window)
1437
- </div>
1438
- <div
1439
- style={{
1440
- background: "var(--af-surface)",
1441
- border: "1px solid var(--af-border-subtle)",
1442
- borderRadius: 8,
1443
- padding: "8px 10px",
1444
- overflow: "auto",
1445
- }}
1446
- >
1447
- <svg
1448
- width={miniSvgWidth}
1449
- height={miniTotalHeight}
1450
- style={{ display: "block" }}
1451
- >
1452
- {/* Numbered track badges (left column, not translated) */}
1453
- {involved.map((s, i) => {
1454
- const y = MINI_HEADER_HEIGHT + i * (MINI_ROW_HEIGHT + MINI_ROW_GAP);
1455
- const cy = y + MINI_ROW_HEIGHT / 2;
1456
- const color = sessionColor(s);
1457
- const isFocused = focusedSessionId === s.id;
1458
- return (
1459
- <g
1460
- key={`num-${s.id}`}
1461
- onClick={() => handleFocusTrack(s.id)}
1462
- style={{ cursor: "pointer" }}
1463
- >
1464
- <title>{`Track ${i + 1} — click to focus in list below`}</title>
1465
- <circle
1466
- cx={NUMBER_COL_WIDTH / 2}
1467
- cy={cy}
1468
- r={9}
1469
- fill={isFocused ? color : "var(--af-surface-elevated)"}
1470
- stroke={color}
1471
- strokeWidth={isFocused ? 2 : 1.5}
1472
- />
1473
- <text
1474
- x={NUMBER_COL_WIDTH / 2}
1475
- y={cy + 3.5}
1476
- textAnchor="middle"
1477
- fontSize={10}
1478
- fontWeight={700}
1479
- fill={isFocused ? "#fff" : "var(--af-text)"}
1480
- style={{ pointerEvents: "none" }}
1481
- >
1482
- {i + 1}
1483
- </text>
1484
- </g>
1485
- );
1486
- })}
1487
-
1488
- {/* Chart content — translated right of the number column */}
1489
- <g transform={`translate(${NUMBER_COL_WIDTH}, 0)`}>
1490
- {/* Burst shaded band */}
1491
- <rect
1492
- x={burstX1}
1493
- y={0}
1494
- width={Math.max(burstX2 - burstX1, 2)}
1495
- height={miniTotalHeight}
1496
- fill={
1497
- burst.crossProject
1498
- ? "rgba(167, 139, 250, 0.14)"
1499
- : "rgba(45, 212, 191, 0.14)"
1500
- }
1501
- />
1502
- {/* Burst band outline */}
1503
- <line
1504
- x1={burstX1}
1505
- x2={burstX1}
1506
- y1={0}
1507
- y2={miniTotalHeight}
1508
- stroke={
1509
- burst.crossProject
1510
- ? "rgba(167, 139, 250, 0.7)"
1511
- : "rgba(45, 212, 191, 0.7)"
1512
- }
1513
- strokeWidth={1}
1514
- strokeDasharray="3 2"
1515
- />
1516
- <line
1517
- x1={burstX2}
1518
- x2={burstX2}
1519
- y1={0}
1520
- y2={miniTotalHeight}
1521
- stroke={
1522
- burst.crossProject
1523
- ? "rgba(167, 139, 250, 0.7)"
1524
- : "rgba(45, 212, 191, 0.7)"
1525
- }
1526
- strokeWidth={1}
1527
- strokeDasharray="3 2"
1528
- />
1529
-
1530
- {/* Hour marks */}
1531
- {miniHourMarks.map((ms) => {
1532
- const x = msToMiniX(ms);
1533
- return (
1534
- <g key={`mini-hm-${ms}`}>
1535
- <line
1536
- x1={x}
1537
- x2={x}
1538
- y1={MINI_HEADER_HEIGHT}
1539
- y2={miniTotalHeight}
1540
- stroke="var(--af-border-subtle)"
1541
- strokeWidth={0.5}
1542
- strokeDasharray="2 3"
1543
- />
1544
- <text
1545
- x={x}
1546
- y={MINI_HEADER_HEIGHT - 6}
1547
- textAnchor="middle"
1548
- fontSize={9}
1549
- fill="var(--af-text-tertiary)"
1550
- >
1551
- {fmtTime(ms)}
1552
- </text>
1553
- </g>
1554
- );
1555
- })}
1556
-
1557
- {/* Session rows — clickable via an invisible row hit rect */}
1558
- {involved.map((s, i) => {
1559
- const y = MINI_HEADER_HEIGHT + i * (MINI_ROW_HEIGHT + MINI_ROW_GAP);
1560
- const color = sessionColor(s);
1561
- const isFocused = focusedSessionId === s.id;
1562
- return (
1563
- <g key={`mini-row-${s.id}`}>
1564
- {/* Row hit target — click the whole row to focus */}
1565
- <rect
1566
- x={0}
1567
- y={y}
1568
- width={MINI_CHART_WIDTH}
1569
- height={MINI_ROW_HEIGHT}
1570
- fill={
1571
- isFocused
1572
- ? "rgba(255,255,255,0.05)"
1573
- : "transparent"
1574
- }
1575
- onClick={() => handleFocusTrack(s.id)}
1576
- style={{ cursor: "pointer" }}
1577
- />
1578
- {s.segments.map((seg, si) => {
1579
- const x1 = msToMiniX(seg.startMs);
1580
- const x2 = msToMiniX(seg.endMs);
1581
- const w = Math.max(x2 - x1, 2);
1582
- return (
1583
- <rect
1584
- key={`mini-seg-${si}`}
1585
- x={x1}
1586
- y={y + 3}
1587
- width={w}
1588
- height={MINI_ROW_HEIGHT - 6}
1589
- fill={color}
1590
- rx={2}
1591
- stroke={isFocused ? "var(--af-text)" : "none"}
1592
- strokeWidth={isFocused ? 1 : 0}
1593
- style={{ pointerEvents: "none" }}
1594
- />
1595
- );
1596
- })}
1597
- </g>
1598
- );
1599
- })}
1600
- </g>
1601
- </svg>
1602
- </div>
1603
-
1604
- {/* Session cards */}
1605
- <div
1606
- style={{
1607
- fontSize: 10,
1608
- color: "var(--af-text-tertiary)",
1609
- textTransform: "uppercase",
1610
- letterSpacing: "0.05em",
1611
- margin: "18px 0 8px",
1612
- fontWeight: 600,
1613
- }}
1614
- >
1615
- Sessions in this burst
1616
- </div>
1617
- <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
1618
- {involved.map((s, i) => {
1619
- const color = sessionColor(s);
1620
- const activeMs = activeInBurst(s);
1621
- const activePct = totalDurationMs > 0
1622
- ? Math.round((activeMs / totalDurationMs) * 100)
1623
- : 0;
1624
- const isFocused = focusedSessionId === s.id;
1625
- return (
1626
- <Link
1627
- key={`burst-session-${s.id}`}
1628
- href={`/sessions/${s.id}`}
1629
- ref={(el) => {
1630
- if (el) cardRefs.current.set(s.id, el);
1631
- else cardRefs.current.delete(s.id);
1632
- }}
1633
- style={{
1634
- display: "block",
1635
- padding: "11px 14px",
1636
- background: isFocused
1637
- ? "var(--af-surface-hover)"
1638
- : "var(--af-surface)",
1639
- border: isFocused
1640
- ? "1px solid var(--af-text-tertiary)"
1641
- : "1px solid var(--af-border-subtle)",
1642
- borderLeft: `3px solid ${color}`,
1643
- borderRadius: 8,
1644
- textDecoration: "none",
1645
- color: "var(--af-text)",
1646
- fontSize: 12,
1647
- lineHeight: 1.45,
1648
- transition: "background 0.15s, border-color 0.15s",
1649
- boxShadow: isFocused
1650
- ? `0 0 0 2px ${color}`
1651
- : "none",
1652
- }}
1653
- >
1654
- <div
1655
- style={{
1656
- display: "flex",
1657
- alignItems: "flex-start",
1658
- justifyContent: "space-between",
1659
- gap: 10,
1660
- }}
1661
- >
1662
- {/* Numbered track badge matching the mini-Gantt row */}
1663
- <span
1664
- style={{
1665
- display: "inline-flex",
1666
- alignItems: "center",
1667
- justifyContent: "center",
1668
- width: 20,
1669
- height: 20,
1670
- borderRadius: "50%",
1671
- background: isFocused ? color : "transparent",
1672
- border: `1.5px solid ${color}`,
1673
- color: isFocused ? "#fff" : "var(--af-text)",
1674
- fontSize: 10,
1675
- fontWeight: 700,
1676
- flexShrink: 0,
1677
- marginTop: 2,
1678
- }}
1679
- aria-label={`Track ${i + 1}`}
1680
- >
1681
- {i + 1}
1682
- </span>
1683
- <div style={{ minWidth: 0, flex: 1 }}>
1684
- <div
1685
- style={{
1686
- fontSize: 11,
1687
- color: "var(--af-text-tertiary)",
1688
- fontFamily: "var(--font-mono)",
1689
- marginBottom: 3,
1690
- overflow: "hidden",
1691
- textOverflow: "ellipsis",
1692
- whiteSpace: "nowrap",
1693
- }}
1694
- title={prettyProjectName(s.projectName)}
1695
- >
1696
- {(() => {
1697
- const name = prettyProjectName(s.projectName);
1698
- const parts = name.split("/").filter(Boolean);
1699
- return parts.length > 2
1700
- ? "…/" + parts.slice(-2).join("/")
1701
- : name;
1702
- })()}
1703
- </div>
1704
- <div
1705
- style={{
1706
- fontWeight: 500,
1707
- overflow: "hidden",
1708
- textOverflow: "ellipsis",
1709
- whiteSpace: "nowrap",
1710
- }}
1711
- title={stripXml(s.firstUserPreview ?? "")}
1712
- >
1713
- {stripXml(s.firstUserPreview ?? "") || (
1714
- <em style={{ color: "var(--af-text-tertiary)" }}>
1715
- (no user message)
1716
- </em>
1717
- )}
1718
- </div>
1719
- {s.lastAgentPreview && (
1720
- <div
1721
- style={{
1722
- fontSize: 11,
1723
- color: "var(--af-text-secondary)",
1724
- marginTop: 3,
1725
- overflow: "hidden",
1726
- textOverflow: "ellipsis",
1727
- whiteSpace: "nowrap",
1728
- fontStyle: "italic",
1729
- }}
1730
- title={stripXml(s.lastAgentPreview)}
1731
- >
1732
- ↳ {stripXml(s.lastAgentPreview)}
1733
- </div>
1734
- )}
1735
- </div>
1736
- <div
1737
- style={{
1738
- fontFamily: "var(--font-mono)",
1739
- fontSize: 10,
1740
- color: "var(--af-text-tertiary)",
1741
- textAlign: "right",
1742
- whiteSpace: "nowrap",
1743
- flexShrink: 0,
1744
- display: "flex",
1745
- flexDirection: "column",
1746
- alignItems: "flex-end",
1747
- gap: 4,
1748
- }}
1749
- >
1750
- {(() => {
1751
- const sum = sessionEntries[s.id];
1752
- if (sum?.outcome) {
1753
- return <OutcomePill outcome={sum.outcome} size="sm" label="text" agent={s.agent} />;
1754
- }
1755
- return null;
1756
- })()}
1757
- <div style={{ color: "var(--af-text-secondary)" }}>
1758
- {formatDuration(activeMs)}{" "}
1759
- <span style={{ opacity: 0.6 }}>({activePct}%)</span>
1760
- </div>
1761
- <div style={{ fontSize: 9, marginTop: 2 }}>
1762
- in burst
1763
- </div>
1764
- </div>
1765
- </div>
1766
- </Link>
1767
- );
1768
- })}
1769
- </div>
1770
- </div>
1771
- </div>
1772
- </div>
1773
- );
1774
- }