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.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/app-path-routes-manifest.json +9 -0
- package/app/apps/web/.next/build-manifest.json +6 -6
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/routes-manifest.json +62 -0
- package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/ask/route.js +3 -2
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js +1 -0
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/insights/months-index/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/insights/months-index/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/insights/months-index/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/insights/months-index/route.js +7 -0
- package/app/apps/web/.next/server/app/api/insights/months-index/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/insights/months-index/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/insights/months-index/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js +6 -0
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/insights/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/insights/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/insights/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/insights/route.js +8 -0
- package/app/apps/web/.next/server/app/api/insights/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/insights/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/insights/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js +6 -0
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/insights/saved/[key]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/insights/saved/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/insights/saved/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/insights/saved/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/insights/saved/route.js +6 -0
- package/app/apps/web/.next/server/app/api/insights/saved/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/insights/saved/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/insights/saved/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js +7 -0
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/insights/weeks-index/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/insights/[key]/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/insights/[key]/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/insights/[key]/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/insights/[key]/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/insights/[key]/page.js +16 -0
- package/app/apps/web/.next/server/app/insights/[key]/page.js.map +5 -0
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/insights/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/insights/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/insights/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/insights/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/insights/page.js +16 -0
- package/app/apps/web/.next/server/app/insights/page.js.map +5 -0
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page.js +16 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page.js.map +5 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/page.js +3 -3
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -3
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/projects/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +2 -2
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/sessions/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page/build-manifest.json +3 -3
- package/app/apps/web/.next/server/app/usage/page.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app-paths-manifest.json +9 -0
- 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
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_saved_[key]_route_actions_0a5j9yw.js +3 -0
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_weeks-index_route_actions_0hh-1c7.js +3 -0
- package/app/apps/web/.next/server/chunks/{[externals]__0rv9ct5._.js → [externals]__0_xztrf._.js} +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.hmj0z._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__01h_g-p._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh-z10._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ri7q~r._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0rr1~~x._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s0gv.0._.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_pdf_[key]_route_actions_0lu-lnx.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_route_actions_0h8l5lz.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_saved_route_actions_13rlblw.js +3 -0
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_00t2xj6.js +68 -0
- 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
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08.d5ia._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b323~k._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0euu8sa._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gx-4qa._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z45d0g._.js +49 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0r~__0v._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_[key]_page_actions_0_z2~e_.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_page_actions_0zjdm~n.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_print_[key]_page_actions_0a853fw.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insight-report_tsx_13q2a-c._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insights-view_tsx_11730ta._.js +6 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0nqz8dw._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0vz7_px.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10cdrh2.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12n1lw_.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +1 -1
- package/app/apps/web/.next/server/functions-config-manifest.json +7 -1
- package/app/apps/web/.next/server/middleware-build-manifest.js +6 -6
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +6 -0
- package/app/apps/web/.next/static/chunks/03_7xtpi9sv4g.js +4 -0
- package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +6 -0
- package/app/apps/web/.next/static/chunks/063oimt30jeo9.js +10 -0
- package/app/apps/web/.next/static/chunks/08c1pcrocqq5s.js +6 -0
- package/app/apps/web/.next/static/chunks/0e-wrhqh4h3r5.js +2 -0
- package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +1 -0
- package/app/apps/web/.next/static/chunks/0i7er7_bfab36.js +10 -0
- package/app/apps/web/.next/static/chunks/0ipytyi01h0ta.js +1 -0
- package/app/apps/web/.next/static/chunks/137uw2-qbro07.js +31 -0
- package/app/apps/web/.next/static/chunks/{turbopack-17n9y8y0h4q9f.js → turbopack-169w-1._23ra_.js} +1 -1
- package/app/apps/web/app/api/insights/months-index/route.ts +80 -0
- package/app/apps/web/app/api/insights/pdf/[key]/route.ts +45 -0
- package/app/apps/web/app/api/insights/route.ts +508 -0
- package/app/apps/web/app/api/insights/saved/[key]/route.ts +11 -0
- package/app/apps/web/app/api/insights/saved/route.ts +9 -0
- package/app/apps/web/app/api/insights/weeks-index/route.ts +95 -0
- package/app/apps/web/app/insights/[key]/page.tsx +59 -0
- package/app/apps/web/app/insights/page.tsx +7 -0
- package/app/apps/web/app/insights/print/[key]/page.tsx +73 -0
- package/app/apps/web/app/layout.tsx +2 -0
- package/app/apps/web/app/projects/[slug]/page.tsx +2 -0
- package/app/apps/web/app/sessions/[id]/page.tsx +27 -1
- package/app/apps/web/app/sessions/[id]/session-view.tsx +372 -793
- package/app/apps/web/app/sessions/[id]/team-tab/adapter.ts +633 -0
- package/app/apps/web/app/sessions/[id]/team-tab/format.ts +20 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +682 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-tab-client.tsx +78 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-table.tsx +796 -0
- package/app/apps/web/app/sessions/[id]/team-tab/turn-drawer.tsx +805 -0
- package/app/apps/web/app/sessions/[id]/tool-cards.tsx +461 -0
- package/app/apps/web/app/sessions/[id]/turn-steps.tsx +306 -0
- package/app/apps/web/app/sessions/sessions-grid.tsx +36 -5
- package/app/apps/web/components/insight-report.tsx +1031 -0
- package/app/apps/web/components/insights-view.tsx +647 -0
- package/app/apps/web/components/live-refresher.tsx +11 -2
- package/app/apps/web/components/live-sessions-widget.tsx +20 -6
- package/app/apps/web/components/sidebar.tsx +8 -0
- package/app/apps/web/components/team-badge.tsx +90 -0
- package/app/apps/web/eslint.config.mjs +2 -0
- package/app/apps/web/lib/ai/chrome-pdf.ts +64 -0
- package/app/apps/web/lib/ai/insights-prompt.ts +71 -0
- package/app/apps/web/lib/ai/saved-reports.ts +83 -0
- package/app/apps/web/lib/date-range.ts +11 -2
- package/app/apps/web/lib/use-live-events.ts +7 -2
- package/app/apps/web/package.json +3 -2
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +2765 -11
- package/dist/index.js +3047 -253
- package/package.json +5 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08cr2u9._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b..h_8._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0cdd~8n._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ms7h0~._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~a~v00._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/_0zv~jv-._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/_12dtryh._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/node_modules_0i2xw~e._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0.knjn.._.js +0 -3
- package/app/apps/web/.next/static/chunks/0257pdz1-imal.js +0 -2
- package/app/apps/web/.next/static/chunks/03fw7f2obcvq~.js +0 -6
- package/app/apps/web/.next/static/chunks/07oyxajlp3dvb.js +0 -6
- package/app/apps/web/.next/static/chunks/0a24elrqoqqep.js +0 -10
- package/app/apps/web/.next/static/chunks/0i4zodv86~~~i.js +0 -1
- package/app/apps/web/.next/static/chunks/0qmh881w772ef.js +0 -31
- package/app/apps/web/.next/static/chunks/0rbm60-qyshdm.js +0 -6
- package/app/apps/web/.next/static/chunks/0wdga7zbwvg70.js +0 -1
- /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_clientMiddlewareManifest.js +0 -0
- /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
|
+
|