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