fleetlens 0.2.9 → 0.4.0

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 (237) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +9 -0
  3. package/app/apps/web/.next/build-manifest.json +6 -6
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +62 -0
  6. package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +3 -3
  7. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +3 -3
  15. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  16. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/app/apps/web/.next/server/app/api/ask/route.js +3 -2
  19. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/events/route.js +1 -0
  21. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/insights/months-index/route/app-paths-manifest.json +3 -0
  23. package/app/apps/web/.next/server/app/api/insights/months-index/route/build-manifest.json +9 -0
  24. package/app/apps/web/.next/server/app/api/insights/months-index/route/server-reference-manifest.json +4 -0
  25. package/app/apps/web/.next/server/app/api/insights/months-index/route.js +7 -0
  26. package/app/apps/web/.next/server/app/api/insights/months-index/route.js.map +5 -0
  27. package/app/apps/web/.next/server/app/api/insights/months-index/route.js.nft.json +1 -0
  28. package/app/apps/web/.next/server/app/api/insights/months-index/route_client-reference-manifest.js +3 -0
  29. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/app-paths-manifest.json +3 -0
  30. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/build-manifest.json +9 -0
  31. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/server-reference-manifest.json +4 -0
  32. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js +6 -0
  33. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.map +5 -0
  34. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.nft.json +1 -0
  35. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route_client-reference-manifest.js +3 -0
  36. package/app/apps/web/.next/server/app/api/insights/route/app-paths-manifest.json +3 -0
  37. package/app/apps/web/.next/server/app/api/insights/route/build-manifest.json +9 -0
  38. package/app/apps/web/.next/server/app/api/insights/route/server-reference-manifest.json +4 -0
  39. package/app/apps/web/.next/server/app/api/insights/route.js +8 -0
  40. package/app/apps/web/.next/server/app/api/insights/route.js.map +5 -0
  41. package/app/apps/web/.next/server/app/api/insights/route.js.nft.json +1 -0
  42. package/app/apps/web/.next/server/app/api/insights/route_client-reference-manifest.js +3 -0
  43. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/app-paths-manifest.json +3 -0
  44. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/build-manifest.json +9 -0
  45. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/server-reference-manifest.json +4 -0
  46. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js +6 -0
  47. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.map +5 -0
  48. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.nft.json +1 -0
  49. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route_client-reference-manifest.js +3 -0
  50. package/app/apps/web/.next/server/app/api/insights/saved/route/app-paths-manifest.json +3 -0
  51. package/app/apps/web/.next/server/app/api/insights/saved/route/build-manifest.json +9 -0
  52. package/app/apps/web/.next/server/app/api/insights/saved/route/server-reference-manifest.json +4 -0
  53. package/app/apps/web/.next/server/app/api/insights/saved/route.js +6 -0
  54. package/app/apps/web/.next/server/app/api/insights/saved/route.js.map +5 -0
  55. package/app/apps/web/.next/server/app/api/insights/saved/route.js.nft.json +1 -0
  56. package/app/apps/web/.next/server/app/api/insights/saved/route_client-reference-manifest.js +3 -0
  57. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/app-paths-manifest.json +3 -0
  58. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/build-manifest.json +9 -0
  59. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/server-reference-manifest.json +4 -0
  60. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js +7 -0
  61. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.map +5 -0
  62. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.nft.json +1 -0
  63. package/app/apps/web/.next/server/app/api/insights/weeks-index/route_client-reference-manifest.js +3 -0
  64. package/app/apps/web/.next/server/app/insights/[key]/page/app-paths-manifest.json +3 -0
  65. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +17 -0
  66. package/app/apps/web/.next/server/app/insights/[key]/page/next-font-manifest.json +6 -0
  67. package/app/apps/web/.next/server/app/insights/[key]/page/react-loadable-manifest.json +1 -0
  68. package/app/apps/web/.next/server/app/insights/[key]/page/server-reference-manifest.json +4 -0
  69. package/app/apps/web/.next/server/app/insights/[key]/page.js +16 -0
  70. package/app/apps/web/.next/server/app/insights/[key]/page.js.map +5 -0
  71. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -0
  72. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +3 -0
  73. package/app/apps/web/.next/server/app/insights/page/app-paths-manifest.json +3 -0
  74. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +17 -0
  75. package/app/apps/web/.next/server/app/insights/page/next-font-manifest.json +6 -0
  76. package/app/apps/web/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
  77. package/app/apps/web/.next/server/app/insights/page/server-reference-manifest.json +4 -0
  78. package/app/apps/web/.next/server/app/insights/page.js +16 -0
  79. package/app/apps/web/.next/server/app/insights/page.js.map +5 -0
  80. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -0
  81. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +3 -0
  82. package/app/apps/web/.next/server/app/insights/print/[key]/page/app-paths-manifest.json +3 -0
  83. package/app/apps/web/.next/server/app/insights/print/[key]/page/build-manifest.json +17 -0
  84. package/app/apps/web/.next/server/app/insights/print/[key]/page/next-font-manifest.json +6 -0
  85. package/app/apps/web/.next/server/app/insights/print/[key]/page/react-loadable-manifest.json +1 -0
  86. package/app/apps/web/.next/server/app/insights/print/[key]/page/server-reference-manifest.json +4 -0
  87. package/app/apps/web/.next/server/app/insights/print/[key]/page.js +16 -0
  88. package/app/apps/web/.next/server/app/insights/print/[key]/page.js.map +5 -0
  89. package/app/apps/web/.next/server/app/insights/print/[key]/page.js.nft.json +1 -0
  90. package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +3 -0
  91. package/app/apps/web/.next/server/app/page/build-manifest.json +3 -3
  92. package/app/apps/web/.next/server/app/page.js +3 -3
  93. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  94. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +3 -3
  96. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  97. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  98. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  99. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +3 -3
  100. package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -3
  101. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  102. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  103. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +3 -3
  104. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  105. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  106. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  107. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +3 -3
  108. package/app/apps/web/.next/server/app/sessions/[id]/page.js +2 -2
  109. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  110. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  111. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +3 -3
  112. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  113. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  114. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  115. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +3 -3
  116. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  117. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  118. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  119. package/app/apps/web/.next/server/app-paths-manifest.json +9 -0
  120. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_months-index_route_actions_0~h~zci.js +3 -0
  121. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_saved_[key]_route_actions_0a5j9yw.js +3 -0
  122. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_weeks-index_route_actions_0hh-1c7.js +3 -0
  123. package/app/apps/web/.next/server/chunks/{[externals]__0rv9ct5._.js → [externals]__0_xztrf._.js} +2 -2
  124. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.hmj0z._.js +3 -0
  125. package/app/apps/web/.next/server/chunks/[root-of-the-server]__01h_g-p._.js +3 -0
  126. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh-z10._.js +3 -0
  127. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +1 -1
  128. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ri7q~r._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0rr1~~x._.js +3 -0
  130. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s0gv.0._.js +3 -0
  131. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_pdf_[key]_route_actions_0lu-lnx.js +3 -0
  132. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_route_actions_0h8l5lz.js +3 -0
  133. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_saved_route_actions_13rlblw.js +3 -0
  134. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_00t2xj6.js +68 -0
  135. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0sgyieh._.js → node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js} +5 -5
  136. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +3 -0
  137. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +3 -0
  138. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08.d5ia._.js +12 -0
  139. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b323~k._.js +8 -0
  140. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0euu8sa._.js +3 -0
  141. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +3 -0
  142. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gx-4qa._.js +3 -0
  143. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
  144. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z45d0g._.js +49 -0
  145. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +3 -0
  146. package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +1 -1
  147. package/app/apps/web/.next/server/chunks/ssr/_0r~__0v._.js +12 -0
  148. package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
  149. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_[key]_page_actions_0_z2~e_.js +3 -0
  150. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_page_actions_0zjdm~n.js +3 -0
  151. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_print_[key]_page_actions_0a853fw.js +3 -0
  152. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +1 -1
  153. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insight-report_tsx_13q2a-c._.js +12 -0
  154. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insights-view_tsx_11730ta._.js +6 -0
  155. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +3 -0
  156. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0nqz8dw._.js +3 -0
  157. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js +3 -0
  158. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0vz7_px.js +4 -0
  159. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10cdrh2.js +4 -0
  160. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12n1lw_.js +4 -0
  161. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
  162. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +1 -1
  163. package/app/apps/web/.next/server/functions-config-manifest.json +7 -1
  164. package/app/apps/web/.next/server/middleware-build-manifest.js +6 -6
  165. package/app/apps/web/.next/server/pages/500.html +1 -1
  166. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  167. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  168. package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +6 -0
  169. package/app/apps/web/.next/static/chunks/03_7xtpi9sv4g.js +4 -0
  170. package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +6 -0
  171. package/app/apps/web/.next/static/chunks/063oimt30jeo9.js +10 -0
  172. package/app/apps/web/.next/static/chunks/08c1pcrocqq5s.js +6 -0
  173. package/app/apps/web/.next/static/chunks/0e-wrhqh4h3r5.js +2 -0
  174. package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +1 -0
  175. package/app/apps/web/.next/static/chunks/0i7er7_bfab36.js +10 -0
  176. package/app/apps/web/.next/static/chunks/0ipytyi01h0ta.js +1 -0
  177. package/app/apps/web/.next/static/chunks/137uw2-qbro07.js +31 -0
  178. package/app/apps/web/.next/static/chunks/{turbopack-17n9y8y0h4q9f.js → turbopack-169w-1._23ra_.js} +1 -1
  179. package/app/apps/web/app/api/insights/months-index/route.ts +80 -0
  180. package/app/apps/web/app/api/insights/pdf/[key]/route.ts +45 -0
  181. package/app/apps/web/app/api/insights/route.ts +508 -0
  182. package/app/apps/web/app/api/insights/saved/[key]/route.ts +11 -0
  183. package/app/apps/web/app/api/insights/saved/route.ts +9 -0
  184. package/app/apps/web/app/api/insights/weeks-index/route.ts +95 -0
  185. package/app/apps/web/app/insights/[key]/page.tsx +59 -0
  186. package/app/apps/web/app/insights/page.tsx +7 -0
  187. package/app/apps/web/app/insights/print/[key]/page.tsx +73 -0
  188. package/app/apps/web/app/layout.tsx +2 -0
  189. package/app/apps/web/app/projects/[slug]/page.tsx +2 -0
  190. package/app/apps/web/app/sessions/[id]/page.tsx +27 -1
  191. package/app/apps/web/app/sessions/[id]/session-view.tsx +372 -793
  192. package/app/apps/web/app/sessions/[id]/team-tab/adapter.ts +633 -0
  193. package/app/apps/web/app/sessions/[id]/team-tab/format.ts +20 -0
  194. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +682 -0
  195. package/app/apps/web/app/sessions/[id]/team-tab/team-tab-client.tsx +78 -0
  196. package/app/apps/web/app/sessions/[id]/team-tab/team-table.tsx +796 -0
  197. package/app/apps/web/app/sessions/[id]/team-tab/turn-drawer.tsx +805 -0
  198. package/app/apps/web/app/sessions/[id]/tool-cards.tsx +461 -0
  199. package/app/apps/web/app/sessions/[id]/turn-steps.tsx +306 -0
  200. package/app/apps/web/app/sessions/sessions-grid.tsx +36 -5
  201. package/app/apps/web/components/insight-report.tsx +1031 -0
  202. package/app/apps/web/components/insights-view.tsx +647 -0
  203. package/app/apps/web/components/live-refresher.tsx +11 -2
  204. package/app/apps/web/components/live-sessions-widget.tsx +20 -6
  205. package/app/apps/web/components/sidebar.tsx +8 -0
  206. package/app/apps/web/components/team-badge.tsx +90 -0
  207. package/app/apps/web/eslint.config.mjs +2 -0
  208. package/app/apps/web/lib/ai/chrome-pdf.ts +64 -0
  209. package/app/apps/web/lib/ai/insights-prompt.ts +71 -0
  210. package/app/apps/web/lib/ai/saved-reports.ts +83 -0
  211. package/app/apps/web/lib/date-range.ts +11 -2
  212. package/app/apps/web/lib/use-live-events.ts +7 -2
  213. package/app/apps/web/package.json +3 -2
  214. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  215. package/dist/daemon-worker.js +2765 -11
  216. package/dist/index.js +3047 -253
  217. package/package.json +5 -3
  218. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08cr2u9._.js +0 -3
  219. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b..h_8._.js +0 -3
  220. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0cdd~8n._.js +0 -3
  221. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ms7h0~._.js +0 -8
  222. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~a~v00._.js +0 -3
  223. package/app/apps/web/.next/server/chunks/ssr/_0zv~jv-._.js +0 -12
  224. package/app/apps/web/.next/server/chunks/ssr/_12dtryh._.js +0 -12
  225. package/app/apps/web/.next/server/chunks/ssr/node_modules_0i2xw~e._.js +0 -3
  226. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0.knjn.._.js +0 -3
  227. package/app/apps/web/.next/static/chunks/0257pdz1-imal.js +0 -2
  228. package/app/apps/web/.next/static/chunks/03fw7f2obcvq~.js +0 -6
  229. package/app/apps/web/.next/static/chunks/07oyxajlp3dvb.js +0 -6
  230. package/app/apps/web/.next/static/chunks/0a24elrqoqqep.js +0 -10
  231. package/app/apps/web/.next/static/chunks/0i4zodv86~~~i.js +0 -1
  232. package/app/apps/web/.next/static/chunks/0qmh881w772ef.js +0 -31
  233. package/app/apps/web/.next/static/chunks/0rbm60-qyshdm.js +0 -6
  234. package/app/apps/web/.next/static/chunks/0wdga7zbwvg70.js +0 -1
  235. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_buildManifest.js +0 -0
  236. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_clientMiddlewareManifest.js +0 -0
  237. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_ssgManifest.js +0 -0
@@ -0,0 +1,796 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useRef, useEffect, useCallback, useState } from "react";
5
+ import { ExternalLink } from "lucide-react";
6
+ import { formatGap } from "@/lib/format";
7
+ import type { TimelineData, TeamTurn, TeamTrack, IdleBand } from "./adapter";
8
+ import { yOfMs } from "./adapter";
9
+ import { formatTeammatePreview } from "./format";
10
+
11
+ const HEADER_HEIGHT = 32;
12
+
13
+ const TIME_COL_WIDTH = 80;
14
+ const COL_GAP = 1;
15
+ const COL_MIN_WIDTH = 240;
16
+
17
+ /** When the vertical scroll moves to a point where no visible member
18
+ * column has any turn active, auto-scroll horizontally to bring the
19
+ * leftmost active one into view — but only after this quiet period
20
+ * has elapsed since the user last manually scrolled horizontally, so
21
+ * manual pan isn't fought by the auto-scroll. */
22
+ const USER_HSCROLL_GRACE_MS = 800;
23
+ /** Minimum time between two consecutive auto-scrolls so a continuous
24
+ * vertical scroll through a blank stretch doesn't thrash horizontally. */
25
+ const AUTOSCROLL_COOLDOWN_MS = 1200;
26
+
27
+ export type SeekTarget = { tsMs: number; trackId?: string };
28
+
29
+ type Props = {
30
+ data: TimelineData;
31
+ onPlayheadChange: (tsMs: number | null) => void;
32
+ /** Publishes which member track ids currently have their midpoint inside
33
+ * the visible horizontal viewport, in track order. Used by the parent
34
+ * to keep the sticky minimap's visible lanes in sync with whatever the
35
+ * user is looking at in the table. */
36
+ onVisibleTrackIdsChange: (ids: string[]) => void;
37
+ scrollTarget: SeekTarget | null;
38
+ onTurnClick: (turn: TeamTurn) => void;
39
+ };
40
+
41
+ /** Programmatically scroll horizontally. The container has
42
+ * `scroll-behavior: smooth` set as a persistent inline style, so a
43
+ * plain scrollLeft assignment becomes a smooth animation driven by
44
+ * the browser. */
45
+ function smoothScrollLeft(el: HTMLElement, targetLeft: number): void {
46
+ el.scrollLeft = targetLeft;
47
+ }
48
+
49
+ export function TeamTable({
50
+ data,
51
+ onPlayheadChange,
52
+ onVisibleTrackIdsChange,
53
+ scrollTarget,
54
+ onTurnClick,
55
+ }: Props) {
56
+ const lastVisibleIdsRef = useRef<string>("");
57
+ const scrollRef = useRef<HTMLDivElement>(null);
58
+ const [expandedTurns, setExpandedTurns] = useState<Set<string>>(new Set());
59
+ const toggleExpand = useCallback((turnId: string) => {
60
+ setExpandedTurns((prev) => {
61
+ const next = new Set(prev);
62
+ if (next.has(turnId)) next.delete(turnId);
63
+ else next.add(turnId);
64
+ return next;
65
+ });
66
+ }, []);
67
+
68
+ // Track user vs. programmatic horizontal scroll, and cool-down the
69
+ // auto-horizontal-scroll so a continuous vertical drag doesn't yank the
70
+ // table left/right on every frame.
71
+ const lastScrollTopRef = useRef(0);
72
+ const lastScrollLeftRef = useRef(0);
73
+ const lastUserHScrollRef = useRef(0);
74
+ const lastAutoScrollRef = useRef(0);
75
+ const programmaticLeftRef = useRef<number | null>(null);
76
+
77
+ useEffect(() => {
78
+ if (!scrollTarget || !scrollRef.current) return;
79
+ const el = scrollRef.current;
80
+ // Land the row a comfortable margin below the sticky column header
81
+ // (HEADER_HEIGHT) so it's visible, not half-occluded.
82
+ const rawY = yOfMs(data.yAnchors, scrollTarget.tsMs);
83
+ const targetTop = Math.max(0, rawY - HEADER_HEIGHT - 12);
84
+
85
+ let targetLeft = el.scrollLeft;
86
+ if (scrollTarget.trackId) {
87
+ const colIndex = data.tracks.findIndex((t) => t.id === scrollTarget.trackId);
88
+ // Skip horizontal scroll for the lead column (index 0) — it's
89
+ // sticky and always visible, so panning to it would just displace
90
+ // the member columns the user was looking at.
91
+ if (colIndex > 0) {
92
+ const targetColX = TIME_COL_WIDTH + colIndex * (COL_MIN_WIDTH + COL_GAP);
93
+ const viewport = el.clientWidth;
94
+ targetLeft = Math.max(
95
+ 0,
96
+ targetColX - viewport / 2 + COL_MIN_WIDTH / 2,
97
+ );
98
+ }
99
+ }
100
+ // Single scrollTo for both axes so vertical and horizontal animate
101
+ // together. Mark the horizontal target as programmatic so the
102
+ // scroll listener's "user panned" grace timer doesn't fire.
103
+ programmaticLeftRef.current = targetLeft;
104
+ el.scrollTo({ top: targetTop, left: targetLeft, behavior: "smooth" });
105
+ }, [scrollTarget, data.yAnchors, data.tracks]);
106
+
107
+ const onScroll = useCallback(() => {
108
+ if (!scrollRef.current) return;
109
+ const el = scrollRef.current;
110
+ const top = el.scrollTop;
111
+ const left = el.scrollLeft;
112
+ const dy = top - lastScrollTopRef.current;
113
+ const dx = left - lastScrollLeftRef.current;
114
+ lastScrollTopRef.current = top;
115
+ lastScrollLeftRef.current = left;
116
+
117
+ // Distinguish programmatic horizontal scroll (which we just triggered
118
+ // ourselves) from a real user pan. Only the latter should reset the
119
+ // "leave the user alone" timer.
120
+ if (dx !== 0) {
121
+ const wasProgrammatic =
122
+ programmaticLeftRef.current !== null &&
123
+ Math.abs(left - programmaticLeftRef.current) < 2;
124
+ if (wasProgrammatic) programmaticLeftRef.current = null;
125
+ else lastUserHScrollRef.current = Date.now();
126
+ }
127
+
128
+ const anchors = data.yAnchors;
129
+ if (anchors.length === 0) {
130
+ onPlayheadChange(null);
131
+ return;
132
+ }
133
+
134
+ // Binary-search for the wall-clock ms at the top of the viewport.
135
+ let lo = 0;
136
+ let hi = anchors.length - 1;
137
+ while (hi - lo > 1) {
138
+ const mid = (lo + hi) >> 1;
139
+ if (anchors[mid]!.yPx <= top) lo = mid;
140
+ else hi = mid;
141
+ }
142
+ const a = anchors[lo]!;
143
+ const b = anchors[hi]!;
144
+ let ts: number;
145
+ if (b.yPx === a.yPx) ts = a.tsMs;
146
+ else {
147
+ const frac = (top - a.yPx) / (b.yPx - a.yPx);
148
+ ts = a.tsMs + frac * (b.tsMs - a.tsMs);
149
+ }
150
+ onPlayheadChange(ts);
151
+
152
+ // Compute which member columns have their midpoint inside the viewport
153
+ // right now. Published on every scroll so the sticky minimap lanes can
154
+ // stay in sync with whatever the user is looking at. Also reused below
155
+ // for the blank-detection auto-scroll.
156
+ const MEMBER_AREA_LEFT = TIME_COL_WIDTH + COL_MIN_WIDTH + COL_GAP * 2;
157
+ const viewportW = el.clientWidth;
158
+ const visibleMemberRangeStart = left + MEMBER_AREA_LEFT;
159
+ const visibleMemberRangeEnd = left + viewportW;
160
+
161
+ type MemberRange = { trackIdx: number; start: number; end: number };
162
+ const memberRanges: MemberRange[] = [];
163
+ for (let i = 1; i < data.tracks.length; i++) {
164
+ const gridX = TIME_COL_WIDTH + i * (COL_MIN_WIDTH + COL_GAP);
165
+ memberRanges.push({
166
+ trackIdx: i,
167
+ start: gridX,
168
+ end: gridX + COL_MIN_WIDTH,
169
+ });
170
+ }
171
+ // "Visible" means the column's midpoint is inside the viewport's member
172
+ // area. A column that peeks in by a few pixels at the right edge doesn't
173
+ // count — the user can't read a turn from a 30px-wide slice.
174
+ const isVisible = (r: MemberRange) => {
175
+ const mid = (r.start + r.end) / 2;
176
+ return mid >= visibleMemberRangeStart && mid <= visibleMemberRangeEnd;
177
+ };
178
+ const visibleMembers = memberRanges.filter(isVisible);
179
+
180
+ // Publish the current visible ids if they changed (string-compare via
181
+ // joined key avoids churning React state on every scroll event).
182
+ const visibleIds = visibleMembers.map(
183
+ (r) => data.tracks[r.trackIdx]!.id,
184
+ );
185
+ const key = visibleIds.join(",");
186
+ if (key !== lastVisibleIdsRef.current) {
187
+ lastVisibleIdsRef.current = key;
188
+ onVisibleTrackIdsChange(visibleIds);
189
+ }
190
+
191
+ // Auto-scroll: only on vertical movement, and only when the user hasn't
192
+ // just panned horizontally themselves, and only when the cooldown has
193
+ // elapsed. These guards prevent feedback loops and UX fights.
194
+ if (dy === 0) return;
195
+ const now = Date.now();
196
+ if (now - lastUserHScrollRef.current < USER_HSCROLL_GRACE_MS) return;
197
+ if (now - lastAutoScrollRef.current < AUTOSCROLL_COOLDOWN_MS) return;
198
+ if (data.tracks.length <= 1) return;
199
+ if (visibleMembers.length === 0) return;
200
+
201
+ const hasTurnAtTs = (trackIdx: number) => {
202
+ const turns = data.tracks[trackIdx]!.turns;
203
+ if (turns.length === 0) return false;
204
+ // Binary search — turns are sorted by startMs.
205
+ let lo = 0;
206
+ let hi = turns.length - 1;
207
+ while (lo <= hi) {
208
+ const mid = (lo + hi) >> 1;
209
+ const t = turns[mid]!;
210
+ if (t.endMs < ts) lo = mid + 1;
211
+ else if (t.startMs > ts) hi = mid - 1;
212
+ else return true;
213
+ }
214
+ return false;
215
+ };
216
+ const anyVisibleActive = visibleMembers.some((r) => hasTurnAtTs(r.trackIdx));
217
+ if (anyVisibleActive) return;
218
+
219
+ // Nothing visible has activity. Find the leftmost off-screen member that
220
+ // IS active at this ts and animate the scroll so it lands right after
221
+ // the sticky LEAD column.
222
+ let targetIdx = -1;
223
+ for (const r of memberRanges) {
224
+ if (!isVisible(r) && hasTurnAtTs(r.trackIdx)) {
225
+ targetIdx = r.trackIdx;
226
+ break;
227
+ }
228
+ }
229
+ if (targetIdx < 0) return;
230
+
231
+ const targetX = TIME_COL_WIDTH + targetIdx * (COL_MIN_WIDTH + COL_GAP);
232
+ const targetLeft = Math.max(0, targetX - MEMBER_AREA_LEFT - 8);
233
+ lastAutoScrollRef.current = now;
234
+ programmaticLeftRef.current = targetLeft;
235
+ smoothScrollLeft(el, targetLeft);
236
+ }, [data.tracks, data.yAnchors, onPlayheadChange, onVisibleTrackIdsChange]);
237
+
238
+ const gridTemplate = `${TIME_COL_WIDTH}px repeat(${data.tracks.length}, minmax(${COL_MIN_WIDTH}px, 1fr))`;
239
+
240
+ return (
241
+ <div
242
+ ref={scrollRef}
243
+ onScroll={onScroll}
244
+ data-team-scroll=""
245
+ style={{
246
+ position: "relative",
247
+ flex: 1,
248
+ minHeight: 400,
249
+ overflowY: "auto",
250
+ overflowX: "auto",
251
+ // scroll-behavior: smooth on the container so that programmatic
252
+ // scrollLeft/scrollTop assignments (and the blank-detection auto-
253
+ // scroll) animate natively via the browser. User wheel scrolls are
254
+ // NOT affected — Chrome only applies scroll-behavior to API-driven
255
+ // scrolls (scrollTo, scrollBy, .scrollLeft= , etc.), not to wheel
256
+ // or touch input. So manual panning stays direct.
257
+ scrollBehavior: "smooth",
258
+ border: "1px solid var(--af-border-subtle)",
259
+ borderRadius: 6,
260
+ background: "var(--af-surface-elevated)",
261
+ }}
262
+ >
263
+ <div
264
+ style={{
265
+ position: "relative",
266
+ width:
267
+ TIME_COL_WIDTH + data.tracks.length * (COL_MIN_WIDTH + COL_GAP),
268
+ minWidth: "100%",
269
+ height: data.totalHeightPx + 72,
270
+ }}
271
+ >
272
+ <div
273
+ style={{
274
+ position: "sticky",
275
+ top: 0,
276
+ display: "grid",
277
+ gridTemplateColumns: gridTemplate,
278
+ gap: COL_GAP,
279
+ background: "var(--af-surface-hover)",
280
+ borderBottom: "1px solid var(--af-border-subtle)",
281
+ zIndex: 5,
282
+ height: 32,
283
+ }}
284
+ >
285
+ <div
286
+ style={{
287
+ padding: 8,
288
+ fontSize: 9,
289
+ color: "var(--af-text-tertiary)",
290
+ fontFamily: "ui-monospace, monospace",
291
+ textTransform: "uppercase",
292
+ position: "sticky",
293
+ left: 0,
294
+ zIndex: 7,
295
+ background: "var(--af-surface-hover)",
296
+ borderRight: "1px solid var(--af-border-subtle)",
297
+ }}
298
+ >
299
+ Time
300
+ </div>
301
+ {data.tracks.map((t, idx) => {
302
+ const stickyLead = idx === 0;
303
+ return (
304
+ <Link
305
+ key={t.id}
306
+ href={`/sessions/${t.id}`}
307
+ style={{
308
+ padding: 8,
309
+ fontSize: 11,
310
+ fontWeight: t.isLead ? 700 : 600,
311
+ color: t.color,
312
+ fontFamily: "ui-monospace, monospace",
313
+ overflow: "hidden",
314
+ textOverflow: "ellipsis",
315
+ whiteSpace: "nowrap",
316
+ textDecoration: "none",
317
+ display: "flex",
318
+ alignItems: "center",
319
+ gap: 4,
320
+ ...(stickyLead && {
321
+ position: "sticky",
322
+ left: TIME_COL_WIDTH + COL_GAP,
323
+ zIndex: 6,
324
+ background: "var(--af-surface-hover)",
325
+ borderRight: "1px solid var(--af-border-subtle)",
326
+ }),
327
+ }}
328
+ title={`${t.label} — open session`}
329
+ >
330
+ <span
331
+ style={{
332
+ overflow: "hidden",
333
+ textOverflow: "ellipsis",
334
+ }}
335
+ >
336
+ {t.isLead ? "LEAD" : t.label}
337
+ </span>
338
+ <ExternalLink size={10} style={{ opacity: 0.6, flexShrink: 0 }} />
339
+ </Link>
340
+ );
341
+ })}
342
+ </div>
343
+
344
+ <div
345
+ style={{
346
+ position: "relative",
347
+ display: "grid",
348
+ gridTemplateColumns: gridTemplate,
349
+ gap: COL_GAP,
350
+ }}
351
+ >
352
+ <div
353
+ style={{
354
+ position: "sticky",
355
+ left: 0,
356
+ zIndex: 4,
357
+ background: "var(--af-surface-elevated)",
358
+ height: data.totalHeightPx + 20,
359
+ borderRight: "1px solid var(--af-border-subtle)",
360
+ }}
361
+ >
362
+ {data.timeTicks.map((tick, i) => (
363
+ <div
364
+ key={i}
365
+ title={formatFullTimestamp(tick.tsMs)}
366
+ style={{
367
+ position: "absolute",
368
+ top: tick.yPx,
369
+ left: 0,
370
+ right: 0,
371
+ fontSize: 9,
372
+ color: "var(--af-text-tertiary)",
373
+ fontFamily: "ui-monospace, monospace",
374
+ paddingLeft: 6,
375
+ paddingTop: 2,
376
+ borderTop: "1px solid var(--af-border-subtle)",
377
+ }}
378
+ >
379
+ {tick.label}
380
+ </div>
381
+ ))}
382
+ </div>
383
+
384
+ {data.tracks.map((track, trackIdx) => (
385
+ <div
386
+ key={track.id}
387
+ style={{
388
+ position: trackIdx === 0 ? "sticky" : "relative",
389
+ ...(trackIdx === 0 && {
390
+ left: TIME_COL_WIDTH + COL_GAP,
391
+ zIndex: 3,
392
+ background: "var(--af-surface-elevated)",
393
+ }),
394
+ height: data.totalHeightPx + 20,
395
+ borderRight: "1px solid var(--af-border-subtle)",
396
+ }}
397
+ >
398
+ {track.turns.map((turn) => {
399
+ const top = yOfMs(data.yAnchors, turn.startMs);
400
+ const bottom = yOfMs(data.yAnchors, turn.endMs);
401
+ const height = Math.max(40, bottom - top - 4);
402
+ return (
403
+ <TurnCell
404
+ key={turn.id}
405
+ turn={turn}
406
+ track={track}
407
+ top={top}
408
+ height={height}
409
+ onClick={() => onTurnClick(turn)}
410
+ isExpanded={expandedTurns.has(turn.id)}
411
+ onToggleExpand={() => toggleExpand(turn.id)}
412
+ />
413
+ );
414
+ })}
415
+ {track.turns.slice(1).map((turn, i) => {
416
+ const prev = track.turns[i]!;
417
+ const gapMs = turn.startMs - prev.endMs;
418
+ if (gapMs <= 0) return null;
419
+ // Skip if fully inside a team-wide idle band (hatch already covers it)
420
+ const coveredByTeamIdle = data.idleBands.some(
421
+ (b) => b.startMs <= prev.endMs && b.endMs >= turn.startMs,
422
+ );
423
+ if (coveredByTeamIdle) return null;
424
+ const top = yOfMs(data.yAnchors, prev.endMs);
425
+ const bottom = yOfMs(data.yAnchors, turn.startMs);
426
+ const gapHeight = bottom - top;
427
+ if (gapHeight <= 40) return null;
428
+ return (
429
+ <div
430
+ key={`idle-${prev.id}-${turn.id}`}
431
+ title={`${formatFullTimestamp(prev.endMs)} → ${formatFullTimestamp(turn.startMs)}`}
432
+ style={{
433
+ position: "absolute",
434
+ top: top + 2,
435
+ left: 8,
436
+ right: 8,
437
+ height: gapHeight - 4,
438
+ pointerEvents: "none",
439
+ zIndex: 0,
440
+ }}
441
+ >
442
+ <span
443
+ style={{
444
+ position: "sticky",
445
+ top: HEADER_HEIGHT + 6,
446
+ display: "inline-block",
447
+ fontSize: 10,
448
+ fontStyle: "italic",
449
+ fontFamily: "ui-monospace, monospace",
450
+ color: "var(--af-text-tertiary)",
451
+ }}
452
+ >
453
+ idle {formatGap(gapMs)}
454
+ </span>
455
+ </div>
456
+ );
457
+ })}
458
+ </div>
459
+ ))}
460
+
461
+ {data.idleBands.map((band, i) => (
462
+ <IdleBandStrip key={i} band={band} />
463
+ ))}
464
+ </div>
465
+ </div>
466
+ </div>
467
+ );
468
+ }
469
+
470
+ function TurnCell({
471
+ turn,
472
+ track,
473
+ top,
474
+ height,
475
+ onClick,
476
+ isExpanded,
477
+ onToggleExpand,
478
+ }: {
479
+ turn: TeamTurn;
480
+ track: TeamTrack;
481
+ top: number;
482
+ height: number;
483
+ onClick: () => void;
484
+ isExpanded: boolean;
485
+ onToggleExpand: () => void;
486
+ }) {
487
+ const [hover, setHover] = useState(false);
488
+ const summary = turn.megaRow.summary;
489
+ const durationStr = formatGap(turn.durationMs);
490
+
491
+ const tmMsg =
492
+ turn.userPrompt && "event" in turn.userPrompt
493
+ ? turn.userPrompt.event.teammateMessage
494
+ : undefined;
495
+ const userText =
496
+ turn.userPrompt && turn.userPrompt.kind === "user"
497
+ ? (turn.userPrompt.displayPreview ?? turn.userPrompt.event.preview ?? "")
498
+ : "";
499
+ const userLabel = tmMsg
500
+ ? `FROM ${tmMsg.teammateId}`
501
+ : "HUMAN";
502
+ const userPreview = tmMsg
503
+ ? formatTeammatePreview(tmMsg)
504
+ : userText;
505
+ const firstAgent = summary.firstAgentPreview ?? "";
506
+ const finalAgent = summary.finalAgentPreview ?? "";
507
+ const showFinal = !!finalAgent && finalAgent !== firstAgent;
508
+ const tools = summary.toolNames.slice(0, 5);
509
+
510
+ return (
511
+ <div
512
+ onClick={onClick}
513
+ onMouseEnter={() => setHover(true)}
514
+ onMouseLeave={() => setHover(false)}
515
+ style={{
516
+ position: "absolute",
517
+ top: top + 2,
518
+ left: 4,
519
+ right: 4,
520
+ height: isExpanded ? "auto" : height,
521
+ minHeight: isExpanded ? Math.max(height, 160) : undefined,
522
+ background: hover ? "var(--af-surface-hover)" : "var(--af-surface)",
523
+ borderLeft: `3px solid ${track.color}`,
524
+ border: "1px solid var(--af-border-subtle)",
525
+ borderLeftWidth: 3,
526
+ borderRadius: 4,
527
+ padding: "6px 8px",
528
+ fontSize: 10,
529
+ lineHeight: 1.3,
530
+ // IMPORTANT: `overflow: hidden` creates a new scroll container in
531
+ // Chrome, which would make the inner sticky content stick to the
532
+ // cell instead of the team table's scroll viewport. Use clip-path
533
+ // for visual clipping — it's paint-only and doesn't break sticky
534
+ // propagation up to the real scroll ancestor.
535
+ overflow: "visible",
536
+ clipPath: isExpanded ? "none" : "inset(0)",
537
+ boxSizing: "border-box",
538
+ cursor: "pointer",
539
+ outline: isExpanded
540
+ ? `1px solid ${track.color}`
541
+ : hover
542
+ ? `1px solid ${track.color}`
543
+ : "none",
544
+ boxShadow: isExpanded ? "0 4px 16px rgba(0,0,0,0.18)" : undefined,
545
+ zIndex: isExpanded ? 5 : undefined,
546
+ transition: "background 0.1s ease",
547
+ }}
548
+ >
549
+ {/* Sticky inner wrapper — pins the content just below the sticky
550
+ column header row so text stays readable while the tall cell
551
+ scrolls past. The nearest scroll ancestor is the table scrollRef. */}
552
+ <div
553
+ style={{
554
+ position: "sticky",
555
+ top: HEADER_HEIGHT + 8,
556
+ display: "flex",
557
+ flexDirection: "column",
558
+ gap: 4,
559
+ }}
560
+ >
561
+ <div
562
+ style={{
563
+ display: "flex",
564
+ justifyContent: "space-between",
565
+ alignItems: "center",
566
+ gap: 6,
567
+ fontSize: 9,
568
+ color: "var(--af-text-tertiary)",
569
+ fontFamily: "ui-monospace, monospace",
570
+ flexShrink: 0,
571
+ }}
572
+ >
573
+ <span>
574
+ {summary.agentMessages} msg · {summary.toolCalls} tools
575
+ {summary.errors > 0 ? ` · ${summary.errors} err` : ""}
576
+ </span>
577
+ <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
578
+ <span>{durationStr}</span>
579
+ <button
580
+ data-turn-toggle
581
+ aria-label={isExpanded ? "Collapse" : "Expand"}
582
+ onClick={(e) => {
583
+ e.stopPropagation();
584
+ onToggleExpand();
585
+ }}
586
+ style={{
587
+ background: "transparent",
588
+ border: "1px solid var(--af-border-subtle)",
589
+ color: "var(--af-text-secondary)",
590
+ width: 16,
591
+ height: 16,
592
+ borderRadius: 2,
593
+ fontSize: 9,
594
+ lineHeight: 1,
595
+ cursor: "pointer",
596
+ padding: 0,
597
+ display: "flex",
598
+ alignItems: "center",
599
+ justifyContent: "center",
600
+ }}
601
+ >
602
+ {isExpanded ? "▲" : "▼"}
603
+ </button>
604
+ </span>
605
+ </div>
606
+
607
+ {userPreview && (
608
+ <div style={{ flexShrink: 0 }}>
609
+ <div
610
+ style={{
611
+ fontSize: 8,
612
+ color: tmMsg ? "var(--af-text-tertiary)" : track.color,
613
+ fontWeight: 600,
614
+ letterSpacing: "0.05em",
615
+ fontStyle: tmMsg ? "italic" : undefined,
616
+ }}
617
+ >
618
+ {userLabel}
619
+ </div>
620
+ <div
621
+ style={{
622
+ color: tmMsg ? "var(--af-text-tertiary)" : "var(--af-text)",
623
+ overflow: "hidden",
624
+ whiteSpace: "nowrap",
625
+ textOverflow: "ellipsis",
626
+ }}
627
+ >
628
+ {userPreview}
629
+ </div>
630
+ </div>
631
+ )}
632
+
633
+ {firstAgent && (
634
+ <div style={{ flexShrink: 0, minHeight: 0 }}>
635
+ <div
636
+ style={{
637
+ fontSize: 8,
638
+ color: track.color,
639
+ fontWeight: 600,
640
+ letterSpacing: "0.05em",
641
+ }}
642
+ >
643
+ AGENT
644
+ </div>
645
+ <div
646
+ style={
647
+ isExpanded
648
+ ? {
649
+ color: "var(--af-text)",
650
+ whiteSpace: "pre-wrap",
651
+ wordBreak: "break-word",
652
+ }
653
+ : {
654
+ color: "var(--af-text)",
655
+ display: "-webkit-box",
656
+ WebkitLineClamp: 2,
657
+ WebkitBoxOrient: "vertical",
658
+ overflow: "hidden",
659
+ textOverflow: "ellipsis",
660
+ }
661
+ }
662
+ >
663
+ {firstAgent}
664
+ </div>
665
+ </div>
666
+ )}
667
+
668
+ {tools.length > 0 && (
669
+ <div style={{ display: "flex", gap: 3, flexWrap: "wrap", flexShrink: 0 }}>
670
+ {tools.map((t) => (
671
+ <span
672
+ key={t.name}
673
+ style={{
674
+ fontSize: 8,
675
+ padding: "1px 5px",
676
+ background: "var(--af-surface-hover)",
677
+ borderRadius: 2,
678
+ color: "var(--af-text-tertiary)",
679
+ }}
680
+ >
681
+ {t.name}
682
+ {t.count > 1 ? ` ×${t.count}` : ""}
683
+ </span>
684
+ ))}
685
+ </div>
686
+ )}
687
+
688
+ {showFinal && (isExpanded || height > 120) && (
689
+ <div style={{ flexShrink: 0, minHeight: 0 }}>
690
+ <div
691
+ style={{
692
+ fontSize: 8,
693
+ color: track.color,
694
+ fontWeight: 600,
695
+ letterSpacing: "0.05em",
696
+ }}
697
+ >
698
+ RESULT
699
+ </div>
700
+ <div
701
+ style={
702
+ isExpanded
703
+ ? {
704
+ color: "var(--af-text)",
705
+ whiteSpace: "pre-wrap",
706
+ wordBreak: "break-word",
707
+ }
708
+ : {
709
+ color: "var(--af-text)",
710
+ display: "-webkit-box",
711
+ WebkitLineClamp: 2,
712
+ WebkitBoxOrient: "vertical",
713
+ overflow: "hidden",
714
+ textOverflow: "ellipsis",
715
+ }
716
+ }
717
+ >
718
+ {finalAgent}
719
+ </div>
720
+ </div>
721
+ )}
722
+
723
+ {!userText && !firstAgent && (
724
+ <div style={{ color: "var(--af-text-tertiary)", fontStyle: "italic" }}>
725
+ (no preview)
726
+ </div>
727
+ )}
728
+ </div>
729
+ </div>
730
+ );
731
+ }
732
+
733
+ function IdleBandStrip({
734
+ band,
735
+ }: {
736
+ band: IdleBand;
737
+ }) {
738
+ return (
739
+ <div
740
+ title={`${formatFullTimestamp(band.startMs)} → ${formatFullTimestamp(band.endMs)}`}
741
+ style={{
742
+ position: "absolute",
743
+ top: band.yPx,
744
+ left: 0,
745
+ right: 0,
746
+ height: band.heightPx,
747
+ background:
748
+ "repeating-linear-gradient(135deg, var(--af-surface) 0, var(--af-surface) 6px, var(--af-surface-hover) 6px, var(--af-surface-hover) 12px)",
749
+ borderTop: "1px solid var(--af-border-subtle)",
750
+ borderBottom: "1px solid var(--af-border-subtle)",
751
+ // Above sticky TIME col (zIndex:4) and sticky LEAD col (zIndex:3) so
752
+ // the hatched pattern is continuous across the full width. Still
753
+ // below the sticky column-header row (zIndex:5) so the header wins.
754
+ zIndex: 4,
755
+ pointerEvents: "none",
756
+ }}
757
+ >
758
+ <div
759
+ style={{
760
+ position: "sticky",
761
+ left: "50%",
762
+ top: 0,
763
+ height: "100%",
764
+ width: "fit-content",
765
+ transform: "translateX(-50%)",
766
+ display: "flex",
767
+ alignItems: "center",
768
+ justifyContent: "center",
769
+ fontSize: 9,
770
+ color: "var(--af-text-tertiary)",
771
+ fontFamily: "ui-monospace, monospace",
772
+ textTransform: "uppercase",
773
+ letterSpacing: "0.05em",
774
+ whiteSpace: "nowrap",
775
+ padding: "0 8px",
776
+ background: "var(--af-surface-elevated)",
777
+ borderRadius: 3,
778
+ }}
779
+ >
780
+ {formatGap(band.durationMs)} idle
781
+ </div>
782
+ </div>
783
+ );
784
+ }
785
+
786
+ function formatFullTimestamp(tsMs: number): string {
787
+ return new Date(tsMs).toLocaleString([], {
788
+ month: "short",
789
+ day: "numeric",
790
+ year: "numeric",
791
+ hour: "numeric",
792
+ minute: "2-digit",
793
+ second: "2-digit",
794
+ });
795
+ }
796
+