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
|
@@ -0,0 +1,1587 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/* ------------------------------------------------------------------ */
|
|
4
|
+
/* Mini-map */
|
|
5
|
+
/* */
|
|
6
|
+
/* Design: */
|
|
7
|
+
/* - Renders ONE segment per presentation row (not per raw event) */
|
|
8
|
+
/* so density matches what the user sees in the transcript. */
|
|
9
|
+
/* - Variable heights by importance: User/Agent/Error/Interrupt */
|
|
10
|
+
/* full height; Tool/Model reduced; idle = full w/ stripes. */
|
|
11
|
+
/* - Minimum block width 5px so single events never disappear. */
|
|
12
|
+
/* - Scroll playhead: thin vertical line tracks the current transcript*/
|
|
13
|
+
/* scroll position inside the <main> container. */
|
|
14
|
+
/* - Time axis ticks below the bar, auto-scaled by total duration. */
|
|
15
|
+
/* - Rich hover card (role pill + preview) instead of bare SVG title. */
|
|
16
|
+
/* ------------------------------------------------------------------ */
|
|
17
|
+
|
|
18
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
19
|
+
import type {
|
|
20
|
+
PresentationRow,
|
|
21
|
+
PresentationRowKind,
|
|
22
|
+
PrMarker,
|
|
23
|
+
SessionEvent,
|
|
24
|
+
SubagentRun,
|
|
25
|
+
TurnMegaRow,
|
|
26
|
+
WorkflowRun,
|
|
27
|
+
} from "@claude-lens/parser";
|
|
28
|
+
import { rowPrimaryIndex, type DisplayRow, type MinimapSeg } from "./types";
|
|
29
|
+
import { subagentColor, workflowColor } from "./colors";
|
|
30
|
+
import { ROLE_THEMES, rowPreview, shortenToolName } from "../turn-steps";
|
|
31
|
+
import {
|
|
32
|
+
estimateCost,
|
|
33
|
+
formatCost,
|
|
34
|
+
formatGap,
|
|
35
|
+
formatOffset,
|
|
36
|
+
formatTokens,
|
|
37
|
+
} from "@/lib/format";
|
|
38
|
+
|
|
39
|
+
/** All rows render at full bar height — distinction comes from color/pattern
|
|
40
|
+
* alone, matching Claude Managed Agents' unified-height timeline. */
|
|
41
|
+
const ROW_IMPORTANCE: Record<PresentationRowKind, number> = {
|
|
42
|
+
user: 1.0,
|
|
43
|
+
agent: 1.0,
|
|
44
|
+
interrupt: 1.0,
|
|
45
|
+
error: 1.0,
|
|
46
|
+
"tool-group": 1.0,
|
|
47
|
+
model: 1.0,
|
|
48
|
+
"task-notification": 1.0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function Minimap({
|
|
52
|
+
displayRows,
|
|
53
|
+
durationMs,
|
|
54
|
+
dayWindow,
|
|
55
|
+
selectedIndex,
|
|
56
|
+
onSelect,
|
|
57
|
+
headerOffset,
|
|
58
|
+
subagents,
|
|
59
|
+
workflows,
|
|
60
|
+
onWorkflowClick,
|
|
61
|
+
selectedSubagentId,
|
|
62
|
+
onSelectSubagent,
|
|
63
|
+
prMarkers,
|
|
64
|
+
coldResumeMarkers,
|
|
65
|
+
rawIdleBands,
|
|
66
|
+
model,
|
|
67
|
+
onPlayheadChange,
|
|
68
|
+
}: {
|
|
69
|
+
displayRows: DisplayRow[];
|
|
70
|
+
durationMs: number;
|
|
71
|
+
/** When set, scope the timeline to this offset window (one local day).
|
|
72
|
+
* null renders the full session. */
|
|
73
|
+
dayWindow?: { startMs: number; endMs: number } | null;
|
|
74
|
+
selectedIndex: number | null;
|
|
75
|
+
onSelect: (i: number) => void;
|
|
76
|
+
headerOffset: number;
|
|
77
|
+
subagents?: SubagentRun[];
|
|
78
|
+
workflows?: WorkflowRun[];
|
|
79
|
+
onWorkflowClick?: (runId: string) => void;
|
|
80
|
+
selectedSubagentId?: string | null;
|
|
81
|
+
onSelectSubagent?: (id: string | null) => void;
|
|
82
|
+
prMarkers?: PrMarker[];
|
|
83
|
+
coldResumeMarkers?: {
|
|
84
|
+
tOffsetMs: number;
|
|
85
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
86
|
+
}[];
|
|
87
|
+
/** Idle bands derived directly from raw event timestamps. Used in place
|
|
88
|
+
* of the legacy "gap before user row" heuristic so Codex sessions
|
|
89
|
+
* (which spend most of their time in agent reasoning, not user input)
|
|
90
|
+
* surface their dead-air on the minimap. */
|
|
91
|
+
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
92
|
+
model?: string;
|
|
93
|
+
/** Fires with the offset (ms) of the topmost on-screen transcript row — the
|
|
94
|
+
* same scroll signal that draws the playhead. SessionView uses it to make
|
|
95
|
+
* the selected day follow scroll. */
|
|
96
|
+
onPlayheadChange?: (ms: number | null) => void;
|
|
97
|
+
}) {
|
|
98
|
+
const WIDTH = 1400;
|
|
99
|
+
/** Main timeline height. Sub-agent lanes stack below this. */
|
|
100
|
+
const MAIN_H = 28;
|
|
101
|
+
/** Per-subagent lane height including the inner gap. */
|
|
102
|
+
const SUB_LANE_H = 11;
|
|
103
|
+
/** Gap between the main timeline and the sub-agent lanes (when present). */
|
|
104
|
+
const SUB_LANE_GAP = 6;
|
|
105
|
+
/** Per-workflow lane height — a touch taller than subagent lanes so the
|
|
106
|
+
* workflow tier reads as distinct. */
|
|
107
|
+
const WF_LANE_H = 13;
|
|
108
|
+
const BAR_TOP = 3;
|
|
109
|
+
const BAR_BOT = MAIN_H - 3;
|
|
110
|
+
const BAR_H = BAR_BOT - BAR_TOP;
|
|
111
|
+
/** Gap subtracted from each segment's width so blocks never touch. */
|
|
112
|
+
const GAP = 3;
|
|
113
|
+
const MIN_BLOCK = 6;
|
|
114
|
+
/** Minimum displayed segment width in SVG units. Every segment gets at
|
|
115
|
+
* least this much space regardless of time duration, so tool-group
|
|
116
|
+
* blocks inside a 24-minute session are still visible + clickable. */
|
|
117
|
+
const MIN_DISPLAY_WIDTH = 12;
|
|
118
|
+
/** Error/interrupt rendered as thin vertical bars capped at this width. */
|
|
119
|
+
const THIN_BAR_MAX = 5;
|
|
120
|
+
|
|
121
|
+
const [hover, setHover] = useState<{
|
|
122
|
+
clientX: number;
|
|
123
|
+
row?: PresentationRow;
|
|
124
|
+
turn?: TurnMegaRow;
|
|
125
|
+
idleMs?: number;
|
|
126
|
+
subagent?: SubagentRun;
|
|
127
|
+
workflow?: WorkflowRun;
|
|
128
|
+
pr?: PrMarker;
|
|
129
|
+
cold?: {
|
|
130
|
+
tOffsetMs: number;
|
|
131
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
132
|
+
};
|
|
133
|
+
} | null>(null);
|
|
134
|
+
const [playheadMs, setPlayheadMs] = useState<number | null>(null);
|
|
135
|
+
// Read inside the observer so the latest callback fires without re-subscribing
|
|
136
|
+
// the IntersectionObserver (its effect deps stay [displayRows.length, headerOffset]).
|
|
137
|
+
const onPlayheadChangeRef = useRef(onPlayheadChange);
|
|
138
|
+
onPlayheadChangeRef.current = onPlayheadChange;
|
|
139
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
140
|
+
|
|
141
|
+
/* Playhead — track which transcript row is at the top of the viewport.
|
|
142
|
+
*
|
|
143
|
+
* The old implementation ran `querySelectorAll("[data-sl-row-index]")`
|
|
144
|
+
* on every scroll event. For a session with ~2000 rows that's thousands
|
|
145
|
+
* of DOM queries per second during scroll — enough to pin a CPU core
|
|
146
|
+
* and make Chrome ask to kill the tab. IntersectionObserver is event-
|
|
147
|
+
* driven: the browser only notifies us when rows enter or leave the region
|
|
148
|
+
* below the sticky header, so the cost scales with *changes* instead of
|
|
149
|
+
* with scroll rate × row count.
|
|
150
|
+
*/
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
// The playhead observer roots on the nearest <main> ancestor. After the
|
|
153
|
+
// Minimap extraction the DOM topology is unchanged so this still resolves —
|
|
154
|
+
// but wrapping the minimap in a portal or a new scroll container would
|
|
155
|
+
// sever that ancestry and silently break the playhead.
|
|
156
|
+
const main = containerRef.current?.closest("main") as HTMLElement | null;
|
|
157
|
+
if (!main) return;
|
|
158
|
+
|
|
159
|
+
// Crop the observation region by the sticky header at the top and keep the
|
|
160
|
+
// whole viewport below it. Every row visible below the header intersects
|
|
161
|
+
// this region; the topmost (smallest offsetTop) is the row sitting at the
|
|
162
|
+
// header line — the playhead. Using the full area (not a thin band) means
|
|
163
|
+
// the playhead never blanks out in the gaps between sparse rows, so the
|
|
164
|
+
// scroll→timeline highlight — and the day-follow that rides on it — stays
|
|
165
|
+
// continuous. Still event-driven: a row only fires when it enters or
|
|
166
|
+
// leaves, so cost scales with scroll distance, not row count.
|
|
167
|
+
const rootMargin = `-${headerOffset}px 0px 0px 0px`;
|
|
168
|
+
|
|
169
|
+
// Rows currently visible below the header. The topmost is the playhead.
|
|
170
|
+
const visibleRows = new Set<HTMLElement>();
|
|
171
|
+
|
|
172
|
+
const publishTopmost = () => {
|
|
173
|
+
let value: number | null = null;
|
|
174
|
+
if (visibleRows.size > 0) {
|
|
175
|
+
let topmost: HTMLElement | null = null;
|
|
176
|
+
let topmostOff = Infinity;
|
|
177
|
+
for (const el of visibleRows) {
|
|
178
|
+
const t = el.offsetTop;
|
|
179
|
+
if (t < topmostOff) {
|
|
180
|
+
topmostOff = t;
|
|
181
|
+
topmost = el;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (topmost) {
|
|
185
|
+
const tOff = Number(topmost.getAttribute("data-sl-toffset"));
|
|
186
|
+
value = Number.isNaN(tOff) ? null : tOff;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
setPlayheadMs(value);
|
|
190
|
+
onPlayheadChangeRef.current?.(value);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const observer = new IntersectionObserver(
|
|
194
|
+
(entries) => {
|
|
195
|
+
for (const e of entries) {
|
|
196
|
+
const el = e.target as HTMLElement;
|
|
197
|
+
if (e.isIntersecting) visibleRows.add(el);
|
|
198
|
+
else visibleRows.delete(el);
|
|
199
|
+
}
|
|
200
|
+
publishTopmost();
|
|
201
|
+
},
|
|
202
|
+
{ root: main, rootMargin, threshold: 0 },
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Observe all current rows. Re-observes whenever the row count
|
|
206
|
+
// changes (i.e. filter switch, turn expand/collapse).
|
|
207
|
+
const rows = main.querySelectorAll<HTMLDivElement>("[data-sl-row-index]");
|
|
208
|
+
for (const r of rows) observer.observe(r);
|
|
209
|
+
|
|
210
|
+
return () => observer.disconnect();
|
|
211
|
+
}, [displayRows.length, headerOffset]);
|
|
212
|
+
|
|
213
|
+
// Offset window for the rendered timeline. `winEnd` is the absolute offset
|
|
214
|
+
// of the right edge (session/day end); `safeDur` is the window LENGTH used
|
|
215
|
+
// as the proportional-width divisor. For a single-day session (dayWindow
|
|
216
|
+
// null) these collapse to [0, durationMs] — identical to the old behavior.
|
|
217
|
+
const winStart = dayWindow?.startMs ?? 0;
|
|
218
|
+
const winEnd = dayWindow?.endMs ?? durationMs;
|
|
219
|
+
const safeDur = Math.max(winEnd - winStart, 1);
|
|
220
|
+
|
|
221
|
+
// Subagent/workflow lanes are scoped to the same window as the main
|
|
222
|
+
// timeline so a day view doesn't reserve empty lanes for runs that happened
|
|
223
|
+
// on other days. Overlap test, not containment — a run straddling the
|
|
224
|
+
// window edge still shows.
|
|
225
|
+
const winSubagents = useMemo(
|
|
226
|
+
() =>
|
|
227
|
+
(subagents ?? []).filter(
|
|
228
|
+
(s) =>
|
|
229
|
+
s.startTOffsetMs !== undefined &&
|
|
230
|
+
s.endTOffsetMs !== undefined &&
|
|
231
|
+
s.endTOffsetMs >= winStart &&
|
|
232
|
+
s.startTOffsetMs <= winEnd,
|
|
233
|
+
),
|
|
234
|
+
[subagents, winStart, winEnd],
|
|
235
|
+
);
|
|
236
|
+
const winWorkflows = useMemo(
|
|
237
|
+
() =>
|
|
238
|
+
(workflows ?? []).filter(
|
|
239
|
+
(w) =>
|
|
240
|
+
w.startTOffsetMs !== undefined &&
|
|
241
|
+
w.startTOffsetMs <= winEnd &&
|
|
242
|
+
(w.endTOffsetMs ?? winEnd) >= winStart,
|
|
243
|
+
),
|
|
244
|
+
[workflows, winStart, winEnd],
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
/* Build segments from the same display-row stream the transcript
|
|
248
|
+
uses. Collapsed turns become one wide segment; expanded-turn
|
|
249
|
+
headers are skipped; presentation rows (whether standalone or
|
|
250
|
+
indented children of an expanded turn) become atomic segments. */
|
|
251
|
+
const segs: MinimapSeg[] = useMemo(() => {
|
|
252
|
+
const out: MinimapSeg[] = [];
|
|
253
|
+
|
|
254
|
+
// Flatten to (item, start) pairs with the expanded-turn headers removed
|
|
255
|
+
// and collapsed turns preserved as single-item entries.
|
|
256
|
+
type WithTime =
|
|
257
|
+
| { kind: "row"; row: PresentationRow; start: number; gapMs: number }
|
|
258
|
+
| {
|
|
259
|
+
kind: "turn";
|
|
260
|
+
turn: TurnMegaRow;
|
|
261
|
+
start: number;
|
|
262
|
+
gapMs: number;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const withTime: WithTime[] = [];
|
|
266
|
+
for (const d of displayRows) {
|
|
267
|
+
if (d.kind === "turn-expanded-header") continue;
|
|
268
|
+
if (d.kind === "turn-expanded-footer") continue;
|
|
269
|
+
if (d.kind === "turn-collapsed") {
|
|
270
|
+
if (d.turn.tOffsetMs === undefined) continue;
|
|
271
|
+
if (d.turn.tOffsetMs < winStart || d.turn.tOffsetMs > winEnd) continue;
|
|
272
|
+
withTime.push({
|
|
273
|
+
kind: "turn",
|
|
274
|
+
turn: d.turn,
|
|
275
|
+
start: d.turn.tOffsetMs,
|
|
276
|
+
gapMs: d.turn.rows[0]?.gapMs ?? 0,
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
// presentation row
|
|
280
|
+
if (d.row.tOffsetMs === undefined) continue;
|
|
281
|
+
if (d.row.tOffsetMs < winStart || d.row.tOffsetMs > winEnd) continue;
|
|
282
|
+
withTime.push({
|
|
283
|
+
kind: "row",
|
|
284
|
+
row: d.row,
|
|
285
|
+
start: d.row.tOffsetMs,
|
|
286
|
+
gapMs: d.row.gapMs ?? 0,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A collapsed turn is one block spanning its whole duration; the idle
|
|
292
|
+
// *inside* it (model thinking, a long tool call, a workflow the turn is
|
|
293
|
+
// waiting on) is part of that turn, not separate dead air. Without this,
|
|
294
|
+
// a turn with several internal gaps renders as a turn stub followed by a
|
|
295
|
+
// run of back-to-back hatched idle bars — there's no activity segment
|
|
296
|
+
// between them because the turn is a single item — and the turn block is
|
|
297
|
+
// clipped at the first gap instead of spanning across a workflow that ran
|
|
298
|
+
// inside it. Suppress any idle band that begins within a collapsed turn so
|
|
299
|
+
// the turn owns its internal time; between-turn idle (which begins at/after
|
|
300
|
+
// a turn's end) is untouched.
|
|
301
|
+
const turnSpans = withTime.flatMap((w) =>
|
|
302
|
+
w.kind === "turn"
|
|
303
|
+
? [{ start: w.start, end: w.start + (w.turn.durationMs ?? 0) }]
|
|
304
|
+
: [],
|
|
305
|
+
);
|
|
306
|
+
const startsInsideTurn = (t: number) =>
|
|
307
|
+
turnSpans.some((s) => t >= s.start && t < s.end);
|
|
308
|
+
|
|
309
|
+
// Build a sorted copy of idle bands so we can clip row/turn ends to
|
|
310
|
+
// the start of the next idle band (or the next row, whichever comes
|
|
311
|
+
// first). This stops a row's rectangle from spanning across an idle
|
|
312
|
+
// gap and visually swallowing it.
|
|
313
|
+
const idleBands = (rawIdleBands ?? [])
|
|
314
|
+
.filter((b) => b.end > winStart && b.start < winEnd)
|
|
315
|
+
.filter((b) => !startsInsideTurn(b.start))
|
|
316
|
+
.sort((a, b) => a.start - b.start);
|
|
317
|
+
const nextIdleStartFrom = (t: number): number | undefined => {
|
|
318
|
+
for (const b of idleBands) {
|
|
319
|
+
if (b.start >= t) return b.start;
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i < withTime.length; i++) {
|
|
325
|
+
const item = withTime[i];
|
|
326
|
+
const next = withTime[i + 1];
|
|
327
|
+
const start = item.start;
|
|
328
|
+
const nextRowStart = next?.start;
|
|
329
|
+
const nextIdleStart = nextIdleStartFrom(start + 1);
|
|
330
|
+
// Cap end to whichever comes first: next visible row, next idle band,
|
|
331
|
+
// or the window's right edge (so a day's last segment doesn't bleed
|
|
332
|
+
// past the scoped window into the next day).
|
|
333
|
+
const cap = (raw: number) =>
|
|
334
|
+
Math.min(
|
|
335
|
+
raw,
|
|
336
|
+
nextRowStart ?? Number.POSITIVE_INFINITY,
|
|
337
|
+
nextIdleStart ?? Number.POSITIVE_INFINITY,
|
|
338
|
+
winEnd,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (item.kind === "turn") {
|
|
342
|
+
const turnEnd = start + (item.turn.durationMs ?? 0);
|
|
343
|
+
out.push({
|
|
344
|
+
kind: "turn",
|
|
345
|
+
turn: item.turn,
|
|
346
|
+
primaryIndex: item.turn.firstPrimaryIndex,
|
|
347
|
+
start,
|
|
348
|
+
end: Math.max(cap(turnEnd), start + 1),
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
const end = cap(nextRowStart ?? start + 800);
|
|
352
|
+
out.push({
|
|
353
|
+
kind: "row",
|
|
354
|
+
row: item.row,
|
|
355
|
+
primaryIndex: rowPrimaryIndex(item.row),
|
|
356
|
+
start,
|
|
357
|
+
end: Math.max(end, start + 1),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Idle bands are computed from raw event timestamps (5s threshold) up
|
|
363
|
+
// in the parent — append them and let the sort below interleave by
|
|
364
|
+
// start time so they slot between rows correctly.
|
|
365
|
+
for (const band of idleBands) {
|
|
366
|
+
out.push({
|
|
367
|
+
kind: "idle",
|
|
368
|
+
start: band.start,
|
|
369
|
+
end: band.end,
|
|
370
|
+
durationMs: band.durationMs,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
out.sort((a, b) => a.start - b.start);
|
|
374
|
+
|
|
375
|
+
// Fuse consecutive idle segments. Two idle bands with no activity segment
|
|
376
|
+
// between them should read as ONE idle block — e.g. a between-turn gap that
|
|
377
|
+
// rawIdleBands split in two because a lone meta/summary anchor sits in the
|
|
378
|
+
// middle (the part before it classifies as in-turn, the part after as
|
|
379
|
+
// between-turn). Safe to do here (post-window, post-sort): only segments
|
|
380
|
+
// genuinely adjacent in render order collapse, so a real row between two
|
|
381
|
+
// idle bands still keeps them apart.
|
|
382
|
+
const fused: MinimapSeg[] = [];
|
|
383
|
+
for (const seg of out) {
|
|
384
|
+
const prev = fused[fused.length - 1];
|
|
385
|
+
if (prev && prev.kind === "idle" && seg.kind === "idle") {
|
|
386
|
+
prev.end = Math.max(prev.end, seg.end);
|
|
387
|
+
prev.durationMs = (prev.durationMs ?? 0) + (seg.durationMs ?? 0);
|
|
388
|
+
} else {
|
|
389
|
+
fused.push(seg.kind === "idle" ? { ...seg } : seg);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// A windowed timeline shouldn't open on dead air. The gap leading INTO the
|
|
394
|
+
// first real row/turn — e.g. the overnight idle before a day's first
|
|
395
|
+
// message, which at day scope can be 60-70% of the whole bar — is the
|
|
396
|
+
// previous period's tail, not this view's content. Drop leading idle so the
|
|
397
|
+
// first activity anchors the left edge; a gap that FOLLOWS the first
|
|
398
|
+
// activity is real in-view dead air and is left in place.
|
|
399
|
+
while (fused.length && fused[0]!.kind === "idle") fused.shift();
|
|
400
|
+
|
|
401
|
+
// Cap the number of rendered segments. Each <rect> carries two
|
|
402
|
+
// event handlers (onMouseEnter + onClick), so 2000 raw-events in
|
|
403
|
+
// "All events" mode becomes 4000 DOM event listeners — enough to
|
|
404
|
+
// stall Chrome during paint. Sampling to MAX contiguous buckets
|
|
405
|
+
// loses per-item hover precision but keeps the overall shape and
|
|
406
|
+
// click-to-navigate behavior (clicks map to the first row inside
|
|
407
|
+
// the bucket, which is the right anchor for "jump here" UX).
|
|
408
|
+
const MAX_SEGMENTS = 600;
|
|
409
|
+
if (fused.length <= MAX_SEGMENTS) return fused;
|
|
410
|
+
|
|
411
|
+
const step = Math.ceil(fused.length / MAX_SEGMENTS);
|
|
412
|
+
const bucketed: MinimapSeg[] = [];
|
|
413
|
+
for (let i = 0; i < fused.length; i += step) {
|
|
414
|
+
const first = fused[i]!;
|
|
415
|
+
const last = fused[Math.min(i + step - 1, fused.length - 1)]!;
|
|
416
|
+
// Merge bucket: keep the first seg's kind + primaryIndex (so clicks
|
|
417
|
+
// jump to the first item of the bucket), but extend the end range
|
|
418
|
+
// to cover the whole bucket's time span.
|
|
419
|
+
if (first.kind === "idle") {
|
|
420
|
+
bucketed.push({
|
|
421
|
+
kind: "idle",
|
|
422
|
+
start: first.start,
|
|
423
|
+
end: last.end,
|
|
424
|
+
durationMs: last.end - first.start,
|
|
425
|
+
});
|
|
426
|
+
} else if (first.kind === "turn") {
|
|
427
|
+
bucketed.push({
|
|
428
|
+
kind: "turn",
|
|
429
|
+
turn: first.turn,
|
|
430
|
+
primaryIndex: first.primaryIndex,
|
|
431
|
+
start: first.start,
|
|
432
|
+
end: last.end,
|
|
433
|
+
});
|
|
434
|
+
} else {
|
|
435
|
+
bucketed.push({
|
|
436
|
+
kind: "row",
|
|
437
|
+
row: first.row,
|
|
438
|
+
primaryIndex: first.primaryIndex,
|
|
439
|
+
start: first.start,
|
|
440
|
+
end: last.end,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return bucketed;
|
|
445
|
+
}, [displayRows, safeDur, rawIdleBands, winStart, winEnd]);
|
|
446
|
+
|
|
447
|
+
/* Sequential layout with a minimum displayed width per segment.
|
|
448
|
+
- Raw proportional width = (seg.end - seg.start) / safeDur * WIDTH
|
|
449
|
+
- Enforced width = max(raw, MIN_DISPLAY_WIDTH)
|
|
450
|
+
- x positions are cumulative (not time-proportional), so tiny events
|
|
451
|
+
stay visible next to a 24-minute turn.
|
|
452
|
+
- If the cumulative width exceeds WIDTH, proportionally shrink so it
|
|
453
|
+
all fits. Time ordering is preserved; exact time-to-pixel mapping
|
|
454
|
+
is sacrificed. */
|
|
455
|
+
const positions: { x: number; w: number }[] = useMemo(() => {
|
|
456
|
+
if (segs.length === 0) return [];
|
|
457
|
+
const raws = segs.map((s) => ((s.end - s.start) / safeDur) * WIDTH);
|
|
458
|
+
const enforced = raws.map((w) => Math.max(w, MIN_DISPLAY_WIDTH));
|
|
459
|
+
const total = enforced.reduce((a, b) => a + b, 0);
|
|
460
|
+
const scale = WIDTH / total;
|
|
461
|
+
const out: { x: number; w: number }[] = [];
|
|
462
|
+
let cursor = 0;
|
|
463
|
+
for (const w of enforced) {
|
|
464
|
+
const scaled = w * scale;
|
|
465
|
+
out.push({ x: cursor, w: scaled });
|
|
466
|
+
cursor += scaled;
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}, [segs, safeDur]);
|
|
470
|
+
|
|
471
|
+
/** Map a time offset (ms) to an x coordinate in the relaxed layout.
|
|
472
|
+
* Walks segments sequentially and interpolates within whichever one
|
|
473
|
+
* contains the target ms. Used for the playhead indicator. */
|
|
474
|
+
const msToX = (ms: number): number => {
|
|
475
|
+
if (segs.length === 0) return 0;
|
|
476
|
+
if (ms <= segs[0].start) return positions[0]?.x ?? 0;
|
|
477
|
+
for (let i = 0; i < segs.length; i++) {
|
|
478
|
+
const s = segs[i];
|
|
479
|
+
const p = positions[i];
|
|
480
|
+
if (!p) continue;
|
|
481
|
+
if (ms < s.start) return p.x;
|
|
482
|
+
if (ms <= s.end) {
|
|
483
|
+
const segDur = s.end - s.start;
|
|
484
|
+
const frac = segDur > 0 ? (ms - s.start) / segDur : 0;
|
|
485
|
+
return p.x + frac * p.w;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const last = positions[positions.length - 1];
|
|
489
|
+
return last ? last.x + last.w : WIDTH;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// ---- Sub-agent lane assignment -------------------------------------
|
|
493
|
+
// Place each subagent in the lowest lane index where it doesn't
|
|
494
|
+
// collide with the most recent bar in that lane. Greedy left→right
|
|
495
|
+
// sweep — O(N×L) where L is the number of lanes (small in practice).
|
|
496
|
+
// Returns a Map agentId → laneIndex plus the total lane count.
|
|
497
|
+
const { laneOf, laneCount } = useMemo(() => {
|
|
498
|
+
if (winSubagents.length === 0) {
|
|
499
|
+
return { laneOf: new Map<string, number>(), laneCount: 0 };
|
|
500
|
+
}
|
|
501
|
+
const sorted = [...winSubagents].sort(
|
|
502
|
+
(a, b) => (a.startTOffsetMs ?? 0) - (b.startTOffsetMs ?? 0),
|
|
503
|
+
);
|
|
504
|
+
const laneEnds: number[] = [];
|
|
505
|
+
const laneOf = new Map<string, number>();
|
|
506
|
+
for (const s of sorted) {
|
|
507
|
+
const start = s.startTOffsetMs ?? 0;
|
|
508
|
+
const end = s.endTOffsetMs ?? start + 1;
|
|
509
|
+
let assigned = -1;
|
|
510
|
+
for (let i = 0; i < laneEnds.length; i++) {
|
|
511
|
+
if (laneEnds[i]! <= start) {
|
|
512
|
+
assigned = i;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (assigned === -1) {
|
|
517
|
+
assigned = laneEnds.length;
|
|
518
|
+
laneEnds.push(0);
|
|
519
|
+
}
|
|
520
|
+
laneEnds[assigned] = end;
|
|
521
|
+
laneOf.set(s.agentId, assigned);
|
|
522
|
+
}
|
|
523
|
+
return { laneOf, laneCount: laneEnds.length };
|
|
524
|
+
}, [winSubagents]);
|
|
525
|
+
|
|
526
|
+
const SUB_BLOCK_TOP = MAIN_H + SUB_LANE_GAP;
|
|
527
|
+
const SUB_LANES_H = laneCount > 0 ? laneCount * SUB_LANE_H : 0;
|
|
528
|
+
|
|
529
|
+
// ---- Workflow lane assignment --------------------------------------
|
|
530
|
+
// Same greedy sweep as subagents. Workflow runs are mostly sequential
|
|
531
|
+
// milestones, so this usually collapses to one lane — but overlap is
|
|
532
|
+
// handled if two runs ever ran concurrently.
|
|
533
|
+
const { wfLaneOf, wfLaneCount } = useMemo(() => {
|
|
534
|
+
// Only place workflows we can actually draw (startTOffsetMs known). A
|
|
535
|
+
// running run has no end yet — it extends to the session end (safeDur),
|
|
536
|
+
// matching the open-ended bar the render draws. Counting only placeable
|
|
537
|
+
// runs keeps wfLaneCount in lockstep with what renders, so no empty
|
|
538
|
+
// reserved band appears.
|
|
539
|
+
const placeable = winWorkflows;
|
|
540
|
+
if (placeable.length === 0) {
|
|
541
|
+
return { wfLaneOf: new Map<string, number>(), wfLaneCount: 0 };
|
|
542
|
+
}
|
|
543
|
+
const sorted = [...placeable].sort(
|
|
544
|
+
(a, b) => (a.startTOffsetMs ?? 0) - (b.startTOffsetMs ?? 0),
|
|
545
|
+
);
|
|
546
|
+
const laneEnds: number[] = [];
|
|
547
|
+
const wfLaneOf = new Map<string, number>();
|
|
548
|
+
for (const w of sorted) {
|
|
549
|
+
const start = w.startTOffsetMs ?? 0;
|
|
550
|
+
const end = w.endTOffsetMs ?? winEnd;
|
|
551
|
+
let assigned = -1;
|
|
552
|
+
for (let i = 0; i < laneEnds.length; i++) {
|
|
553
|
+
if (laneEnds[i]! <= start) {
|
|
554
|
+
assigned = i;
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (assigned === -1) {
|
|
559
|
+
assigned = laneEnds.length;
|
|
560
|
+
laneEnds.push(0);
|
|
561
|
+
}
|
|
562
|
+
laneEnds[assigned] = Math.max(end, start + 1);
|
|
563
|
+
wfLaneOf.set(w.runId, assigned);
|
|
564
|
+
}
|
|
565
|
+
return { wfLaneOf, wfLaneCount: laneEnds.length };
|
|
566
|
+
}, [winWorkflows, winEnd]);
|
|
567
|
+
|
|
568
|
+
const SUB_BLOCK_BOTTOM = MAIN_H + (laneCount > 0 ? SUB_LANE_GAP + SUB_LANES_H : 0);
|
|
569
|
+
const WF_BLOCK_TOP = SUB_BLOCK_BOTTOM + (wfLaneCount > 0 ? SUB_LANE_GAP : 0);
|
|
570
|
+
const WF_LANES_H = wfLaneCount > 0 ? wfLaneCount * WF_LANE_H : 0;
|
|
571
|
+
const TOTAL_H = SUB_BLOCK_BOTTOM + (wfLaneCount > 0 ? SUB_LANE_GAP + WF_LANES_H : 0);
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<div
|
|
575
|
+
ref={containerRef}
|
|
576
|
+
style={{
|
|
577
|
+
padding: "8px 10px",
|
|
578
|
+
position: "relative",
|
|
579
|
+
background: "var(--af-surface)",
|
|
580
|
+
border: "1px solid var(--af-border-subtle)",
|
|
581
|
+
borderRadius: 8,
|
|
582
|
+
marginTop: 2,
|
|
583
|
+
}}
|
|
584
|
+
onMouseLeave={() => setHover(null)}
|
|
585
|
+
>
|
|
586
|
+
<svg
|
|
587
|
+
viewBox={`0 0 ${WIDTH} ${TOTAL_H}`}
|
|
588
|
+
preserveAspectRatio="none"
|
|
589
|
+
style={{
|
|
590
|
+
width: "100%",
|
|
591
|
+
height: TOTAL_H,
|
|
592
|
+
display: "block",
|
|
593
|
+
overflow: "visible",
|
|
594
|
+
}}
|
|
595
|
+
>
|
|
596
|
+
<defs>
|
|
597
|
+
<pattern
|
|
598
|
+
id="stripes"
|
|
599
|
+
patternUnits="userSpaceOnUse"
|
|
600
|
+
width="6"
|
|
601
|
+
height="6"
|
|
602
|
+
patternTransform="rotate(45)"
|
|
603
|
+
>
|
|
604
|
+
<rect width="6" height="6" fill="rgba(120, 115, 108, 0.04)" />
|
|
605
|
+
<line x1="0" y1="0" x2="0" y2="6" stroke="rgba(120, 115, 108, 0.28)" strokeWidth="2" />
|
|
606
|
+
</pattern>
|
|
607
|
+
</defs>
|
|
608
|
+
|
|
609
|
+
{/* Segments — x/w come from the relaxed sequential layout, NOT
|
|
610
|
+
from raw time proportions. See the positions memo above. */}
|
|
611
|
+
{segs.map((seg, i) => {
|
|
612
|
+
const pos = positions[i];
|
|
613
|
+
if (!pos) return null;
|
|
614
|
+
const xRaw = pos.x;
|
|
615
|
+
const wRaw = pos.w;
|
|
616
|
+
|
|
617
|
+
if (seg.kind === "idle") {
|
|
618
|
+
// Idle block: fill its span minus a gap on each side, with a
|
|
619
|
+
// subtle border so the diagonal stripes read as a distinct block.
|
|
620
|
+
const w = Math.max(wRaw - GAP, MIN_BLOCK);
|
|
621
|
+
return (
|
|
622
|
+
<rect
|
|
623
|
+
key={`idle-${i}`}
|
|
624
|
+
x={xRaw + GAP / 2}
|
|
625
|
+
y={BAR_TOP + 1}
|
|
626
|
+
width={w}
|
|
627
|
+
height={BAR_H - 2}
|
|
628
|
+
fill="url(#stripes)"
|
|
629
|
+
stroke="rgba(120, 115, 108, 0.35)"
|
|
630
|
+
strokeWidth="0.6"
|
|
631
|
+
rx="3"
|
|
632
|
+
onMouseEnter={(e) =>
|
|
633
|
+
setHover({
|
|
634
|
+
clientX: e.clientX,
|
|
635
|
+
idleMs: seg.durationMs,
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
/>
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Resolve the theme + selection state. Turn segments use the
|
|
643
|
+
// "agent" theme (a turn is the agent's work wrapped as one unit).
|
|
644
|
+
const rowKind: PresentationRowKind = seg.kind === "turn" ? "agent" : seg.row.kind;
|
|
645
|
+
const theme = ROLE_THEMES[rowKind];
|
|
646
|
+
const importance = ROW_IMPORTANCE[rowKind];
|
|
647
|
+
const h = BAR_H * importance;
|
|
648
|
+
const y = BAR_TOP + (BAR_H - h) / 2;
|
|
649
|
+
const isSelected = selectedIndex === seg.primaryIndex;
|
|
650
|
+
|
|
651
|
+
// Error/interrupt: render as a THIN vertical bar regardless of
|
|
652
|
+
// actual span. Consecutive errors become a visible comb of
|
|
653
|
+
// narrow strokes separated by gaps, matching Claude's UI.
|
|
654
|
+
const isThin =
|
|
655
|
+
seg.kind === "row" && (seg.row.kind === "error" || seg.row.kind === "interrupt");
|
|
656
|
+
let w: number;
|
|
657
|
+
let x: number;
|
|
658
|
+
if (isThin) {
|
|
659
|
+
w = Math.min(Math.max(wRaw - GAP, 2), THIN_BAR_MAX);
|
|
660
|
+
x = xRaw + Math.max((wRaw - w) / 2, GAP / 2);
|
|
661
|
+
} else {
|
|
662
|
+
w = Math.max(wRaw - GAP, MIN_BLOCK);
|
|
663
|
+
x = xRaw + GAP / 2;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const ringPad = 2.5;
|
|
667
|
+
const onHover = (e: React.MouseEvent) =>
|
|
668
|
+
seg.kind === "turn"
|
|
669
|
+
? setHover({ clientX: e.clientX, turn: seg.turn })
|
|
670
|
+
: setHover({ clientX: e.clientX, row: seg.row });
|
|
671
|
+
return (
|
|
672
|
+
<g key={`seg-${seg.primaryIndex}-${i}`}>
|
|
673
|
+
<rect
|
|
674
|
+
x={x}
|
|
675
|
+
y={y}
|
|
676
|
+
width={w}
|
|
677
|
+
height={h}
|
|
678
|
+
fill={theme.mini}
|
|
679
|
+
rx="3"
|
|
680
|
+
style={{ cursor: "pointer" }}
|
|
681
|
+
onClick={() => onSelect(seg.primaryIndex)}
|
|
682
|
+
onMouseEnter={onHover}
|
|
683
|
+
/>
|
|
684
|
+
{isSelected && (
|
|
685
|
+
<rect
|
|
686
|
+
x={x - ringPad}
|
|
687
|
+
y={y - ringPad}
|
|
688
|
+
width={w + ringPad * 2}
|
|
689
|
+
height={h + ringPad * 2}
|
|
690
|
+
fill="none"
|
|
691
|
+
stroke="#5C84C3"
|
|
692
|
+
strokeWidth="2"
|
|
693
|
+
rx="5"
|
|
694
|
+
pointerEvents="none"
|
|
695
|
+
/>
|
|
696
|
+
)}
|
|
697
|
+
</g>
|
|
698
|
+
);
|
|
699
|
+
})}
|
|
700
|
+
|
|
701
|
+
{/* Sub-agent lanes — one row per lane, colored by agentType.
|
|
702
|
+
Background-mode runs are highlighted with a brighter fill +
|
|
703
|
+
extra dashed outline so you can spot true parallelism at a
|
|
704
|
+
glance. Bars use the same msToX mapping as the main timeline
|
|
705
|
+
so the subagent's start/end aligns vertically with whatever
|
|
706
|
+
was happening in the main session at that time. */}
|
|
707
|
+
{laneCount > 0 && winSubagents.length > 0 && (
|
|
708
|
+
<g>
|
|
709
|
+
{/* Faint divider line above the subagent lane block */}
|
|
710
|
+
<line
|
|
711
|
+
x1={0}
|
|
712
|
+
x2={WIDTH}
|
|
713
|
+
y1={MAIN_H + SUB_LANE_GAP / 2}
|
|
714
|
+
y2={MAIN_H + SUB_LANE_GAP / 2}
|
|
715
|
+
stroke="var(--af-border-subtle)"
|
|
716
|
+
strokeWidth="0.6"
|
|
717
|
+
strokeDasharray="2 4"
|
|
718
|
+
/>
|
|
719
|
+
{winSubagents.map((s) => {
|
|
720
|
+
const lane = laneOf.get(s.agentId) ?? 0;
|
|
721
|
+
if (s.startTOffsetMs === undefined || s.endTOffsetMs === undefined) return null;
|
|
722
|
+
const startX = msToX(s.startTOffsetMs);
|
|
723
|
+
const endX = msToX(s.endTOffsetMs);
|
|
724
|
+
// Minimum 8px so a 78-second subagent in a 16-hour session is
|
|
725
|
+
// still wide enough to read + hit-test. Long subagents render
|
|
726
|
+
// at their actual proportional width.
|
|
727
|
+
const w = Math.max(endX - startX, 8);
|
|
728
|
+
const y = SUB_BLOCK_TOP + lane * SUB_LANE_H;
|
|
729
|
+
const h = SUB_LANE_H - 2;
|
|
730
|
+
const fill = subagentColor(s.agentType, s.runInBackground);
|
|
731
|
+
const isSelected = selectedSubagentId === s.agentId;
|
|
732
|
+
return (
|
|
733
|
+
<g key={s.agentId}>
|
|
734
|
+
<rect
|
|
735
|
+
x={startX}
|
|
736
|
+
y={y}
|
|
737
|
+
width={w}
|
|
738
|
+
height={h}
|
|
739
|
+
fill={fill}
|
|
740
|
+
stroke={s.runInBackground ? "#fff" : "transparent"}
|
|
741
|
+
strokeWidth={s.runInBackground ? 0.5 : 0}
|
|
742
|
+
strokeDasharray={s.runInBackground ? "1.5 1.5" : undefined}
|
|
743
|
+
rx="2"
|
|
744
|
+
style={{ cursor: "pointer" }}
|
|
745
|
+
onMouseEnter={(e) => setHover({ clientX: e.clientX, subagent: s })}
|
|
746
|
+
onClick={() => onSelectSubagent?.(isSelected ? null : s.agentId)}
|
|
747
|
+
/>
|
|
748
|
+
{isSelected && (
|
|
749
|
+
<rect
|
|
750
|
+
x={startX - 2}
|
|
751
|
+
y={y - 2}
|
|
752
|
+
width={w + 4}
|
|
753
|
+
height={h + 4}
|
|
754
|
+
fill="none"
|
|
755
|
+
stroke="var(--af-accent)"
|
|
756
|
+
strokeWidth="1.5"
|
|
757
|
+
rx="4"
|
|
758
|
+
pointerEvents="none"
|
|
759
|
+
/>
|
|
760
|
+
)}
|
|
761
|
+
</g>
|
|
762
|
+
);
|
|
763
|
+
})}
|
|
764
|
+
</g>
|
|
765
|
+
)}
|
|
766
|
+
|
|
767
|
+
{/* Workflow lanes — one bar per dynamic-workflow run, on the same
|
|
768
|
+
time axis as the main timeline, colored by status (orange tier,
|
|
769
|
+
distinct from the blue/purple subagents). The agent count is the
|
|
770
|
+
headline — a single Workflow tool call hides a whole fleet. */}
|
|
771
|
+
{wfLaneCount > 0 && winWorkflows.length > 0 && (
|
|
772
|
+
<g>
|
|
773
|
+
<line
|
|
774
|
+
x1={0}
|
|
775
|
+
x2={WIDTH}
|
|
776
|
+
y1={WF_BLOCK_TOP - SUB_LANE_GAP / 2}
|
|
777
|
+
y2={WF_BLOCK_TOP - SUB_LANE_GAP / 2}
|
|
778
|
+
stroke="var(--af-border-subtle)"
|
|
779
|
+
strokeWidth="0.6"
|
|
780
|
+
strokeDasharray="2 4"
|
|
781
|
+
/>
|
|
782
|
+
{winWorkflows.map((w) => {
|
|
783
|
+
if (w.startTOffsetMs === undefined) return null;
|
|
784
|
+
const lane = wfLaneOf.get(w.runId) ?? 0;
|
|
785
|
+
const startX = msToX(w.startTOffsetMs);
|
|
786
|
+
// A still-running run has no endTOffsetMs — draw it open-ended to
|
|
787
|
+
// the session end so it's visible (and dashed, to read as ongoing)
|
|
788
|
+
// rather than vanishing while still occupying a reserved lane.
|
|
789
|
+
const inProgress = w.endTOffsetMs === undefined;
|
|
790
|
+
const endX = msToX(w.endTOffsetMs ?? winEnd);
|
|
791
|
+
const wWidth = Math.max(endX - startX, 8);
|
|
792
|
+
const y = WF_BLOCK_TOP + lane * WF_LANE_H;
|
|
793
|
+
const h = WF_LANE_H - 2;
|
|
794
|
+
const fill = workflowColor(w.status);
|
|
795
|
+
// Center the agent count label inside the bar when there's room.
|
|
796
|
+
const label = `${w.agentCount}`;
|
|
797
|
+
const showLabel = wWidth > 22;
|
|
798
|
+
return (
|
|
799
|
+
<g key={w.runId} style={{ cursor: "pointer" }}>
|
|
800
|
+
<rect
|
|
801
|
+
x={startX}
|
|
802
|
+
y={y}
|
|
803
|
+
width={wWidth}
|
|
804
|
+
height={h}
|
|
805
|
+
fill={fill}
|
|
806
|
+
fillOpacity={inProgress ? 0.55 : 1}
|
|
807
|
+
stroke={inProgress ? fill : "transparent"}
|
|
808
|
+
strokeWidth={inProgress ? 0.8 : 0}
|
|
809
|
+
strokeDasharray={inProgress ? "2 2" : undefined}
|
|
810
|
+
rx="2"
|
|
811
|
+
onMouseEnter={(e) => setHover({ clientX: e.clientX, workflow: w })}
|
|
812
|
+
onClick={() => onWorkflowClick?.(w.runId)}
|
|
813
|
+
/>
|
|
814
|
+
{showLabel && (
|
|
815
|
+
<text
|
|
816
|
+
x={startX + wWidth / 2}
|
|
817
|
+
y={y + h / 2 + 3}
|
|
818
|
+
textAnchor="middle"
|
|
819
|
+
fontSize="8.5"
|
|
820
|
+
fontWeight="700"
|
|
821
|
+
fill="#fff"
|
|
822
|
+
pointerEvents="none"
|
|
823
|
+
style={{ fontFamily: "var(--font-mono)" }}
|
|
824
|
+
>
|
|
825
|
+
{label}
|
|
826
|
+
</text>
|
|
827
|
+
)}
|
|
828
|
+
</g>
|
|
829
|
+
);
|
|
830
|
+
})}
|
|
831
|
+
</g>
|
|
832
|
+
)}
|
|
833
|
+
|
|
834
|
+
{/* PR markers — diamond + vertical line at each `gh pr create` */}
|
|
835
|
+
{prMarkers?.map((pr, i) => {
|
|
836
|
+
if (pr.tOffsetMs === undefined) return null;
|
|
837
|
+
if (pr.tOffsetMs < winStart || pr.tOffsetMs > winEnd) return null;
|
|
838
|
+
const x = msToX(pr.tOffsetMs);
|
|
839
|
+
return (
|
|
840
|
+
<g
|
|
841
|
+
key={`pr-${i}`}
|
|
842
|
+
style={{ cursor: "pointer" }}
|
|
843
|
+
onMouseEnter={(e) => setHover({ clientX: e.clientX, pr })}
|
|
844
|
+
>
|
|
845
|
+
<line
|
|
846
|
+
x1={x}
|
|
847
|
+
x2={x}
|
|
848
|
+
y1={0}
|
|
849
|
+
y2={TOTAL_H}
|
|
850
|
+
stroke="#8b5cf6"
|
|
851
|
+
strokeWidth="1.5"
|
|
852
|
+
strokeDasharray="4 3"
|
|
853
|
+
opacity="0.7"
|
|
854
|
+
/>
|
|
855
|
+
{/* Invisible wider hit area for easier hovering */}
|
|
856
|
+
<rect
|
|
857
|
+
x={x - 8}
|
|
858
|
+
y={0}
|
|
859
|
+
width={16}
|
|
860
|
+
height={TOTAL_H}
|
|
861
|
+
fill="transparent"
|
|
862
|
+
/>
|
|
863
|
+
{/* Diamond marker at mid-height */}
|
|
864
|
+
<polygon
|
|
865
|
+
points={`${x},${MAIN_H / 2 - 5} ${x + 5},${MAIN_H / 2} ${x},${MAIN_H / 2 + 5} ${x - 5},${MAIN_H / 2}`}
|
|
866
|
+
fill="#8b5cf6"
|
|
867
|
+
stroke="var(--af-surface)"
|
|
868
|
+
strokeWidth="1.5"
|
|
869
|
+
/>
|
|
870
|
+
</g>
|
|
871
|
+
);
|
|
872
|
+
})}
|
|
873
|
+
|
|
874
|
+
{coldResumeMarkers?.map((cr, i) => {
|
|
875
|
+
if (cr.tOffsetMs < winStart || cr.tOffsetMs > winEnd) return null;
|
|
876
|
+
const x = msToX(cr.tOffsetMs);
|
|
877
|
+
return (
|
|
878
|
+
<g
|
|
879
|
+
key={`cold-${i}`}
|
|
880
|
+
style={{ cursor: "pointer" }}
|
|
881
|
+
onMouseEnter={(e) => setHover({ clientX: e.clientX, cold: cr })}
|
|
882
|
+
>
|
|
883
|
+
<line
|
|
884
|
+
x1={x}
|
|
885
|
+
x2={x}
|
|
886
|
+
y1={0}
|
|
887
|
+
y2={TOTAL_H}
|
|
888
|
+
stroke="#D97706"
|
|
889
|
+
strokeWidth="1.5"
|
|
890
|
+
strokeDasharray="4 3"
|
|
891
|
+
opacity="0.75"
|
|
892
|
+
pointerEvents="none"
|
|
893
|
+
/>
|
|
894
|
+
{/* Hover hit-area limited to the diamond itself — a wider/taller
|
|
895
|
+
target used to swallow hovers+clicks meant for adjacent
|
|
896
|
+
user-prompt markers sitting right beside the cache-rebuild
|
|
897
|
+
point. */}
|
|
898
|
+
<rect
|
|
899
|
+
x={x - 6}
|
|
900
|
+
y={MAIN_H / 2 - 8}
|
|
901
|
+
width={12}
|
|
902
|
+
height={16}
|
|
903
|
+
fill="transparent"
|
|
904
|
+
/>
|
|
905
|
+
<polygon
|
|
906
|
+
points={`${x},${MAIN_H / 2 - 5} ${x + 5},${MAIN_H / 2} ${x},${MAIN_H / 2 + 5} ${x - 5},${MAIN_H / 2}`}
|
|
907
|
+
fill="#D97706"
|
|
908
|
+
stroke="var(--af-surface)"
|
|
909
|
+
strokeWidth="1.5"
|
|
910
|
+
pointerEvents="none"
|
|
911
|
+
/>
|
|
912
|
+
</g>
|
|
913
|
+
);
|
|
914
|
+
})}
|
|
915
|
+
|
|
916
|
+
{/* Playhead — positioned against the relaxed layout, not raw time.
|
|
917
|
+
Spans the full SVG height (main + subagent lanes) so you can see
|
|
918
|
+
which subagents were running at the current scroll position. */}
|
|
919
|
+
{playheadMs !== null && (
|
|
920
|
+
<line
|
|
921
|
+
x1={msToX(playheadMs)}
|
|
922
|
+
x2={msToX(playheadMs)}
|
|
923
|
+
y1={0}
|
|
924
|
+
y2={TOTAL_H}
|
|
925
|
+
stroke="#0F172A"
|
|
926
|
+
strokeWidth="1.25"
|
|
927
|
+
strokeDasharray="2 2"
|
|
928
|
+
opacity="0.65"
|
|
929
|
+
/>
|
|
930
|
+
)}
|
|
931
|
+
</svg>
|
|
932
|
+
|
|
933
|
+
{/* Hover card */}
|
|
934
|
+
{hover && (
|
|
935
|
+
<MinimapHoverCard
|
|
936
|
+
containerRef={containerRef}
|
|
937
|
+
clientX={hover.clientX}
|
|
938
|
+
row={hover.row}
|
|
939
|
+
turn={hover.turn}
|
|
940
|
+
idleMs={hover.idleMs}
|
|
941
|
+
subagent={hover.subagent}
|
|
942
|
+
workflow={hover.workflow}
|
|
943
|
+
pr={hover.pr}
|
|
944
|
+
cold={hover.cold}
|
|
945
|
+
model={model}
|
|
946
|
+
/>
|
|
947
|
+
)}
|
|
948
|
+
|
|
949
|
+
{/* Sub-agent legend strip — only when there are subagent lanes. */}
|
|
950
|
+
{laneCount > 0 && subagents && (
|
|
951
|
+
<div
|
|
952
|
+
style={{
|
|
953
|
+
display: "flex",
|
|
954
|
+
alignItems: "center",
|
|
955
|
+
gap: 10,
|
|
956
|
+
marginTop: 6,
|
|
957
|
+
fontSize: 10,
|
|
958
|
+
color: "var(--af-text-tertiary)",
|
|
959
|
+
}}
|
|
960
|
+
>
|
|
961
|
+
<span style={{ fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em" }}>
|
|
962
|
+
Sub-agents
|
|
963
|
+
</span>
|
|
964
|
+
<span>·</span>
|
|
965
|
+
<span>
|
|
966
|
+
{subagents.length} run{subagents.length === 1 ? "" : "s"}
|
|
967
|
+
</span>
|
|
968
|
+
<span style={{ marginLeft: 8, display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
969
|
+
{Array.from(new Set(subagents.map((s) => s.agentType))).map((type) => (
|
|
970
|
+
<span
|
|
971
|
+
key={type}
|
|
972
|
+
style={{ display: "inline-flex", alignItems: "center", gap: 4 }}
|
|
973
|
+
>
|
|
974
|
+
<span
|
|
975
|
+
style={{
|
|
976
|
+
display: "inline-block",
|
|
977
|
+
width: 8,
|
|
978
|
+
height: 8,
|
|
979
|
+
borderRadius: 2,
|
|
980
|
+
background: subagentColor(type, false),
|
|
981
|
+
}}
|
|
982
|
+
/>
|
|
983
|
+
{type}
|
|
984
|
+
</span>
|
|
985
|
+
))}
|
|
986
|
+
{subagents.some((s) => s.runInBackground) && (
|
|
987
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
988
|
+
<span
|
|
989
|
+
style={{
|
|
990
|
+
display: "inline-block",
|
|
991
|
+
width: 8,
|
|
992
|
+
height: 8,
|
|
993
|
+
borderRadius: 2,
|
|
994
|
+
background: "transparent",
|
|
995
|
+
border: "1px dashed var(--af-text-secondary)",
|
|
996
|
+
}}
|
|
997
|
+
/>
|
|
998
|
+
background
|
|
999
|
+
</span>
|
|
1000
|
+
)}
|
|
1001
|
+
</span>
|
|
1002
|
+
</div>
|
|
1003
|
+
)}
|
|
1004
|
+
</div>
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function MinimapHoverCard({
|
|
1009
|
+
containerRef,
|
|
1010
|
+
clientX,
|
|
1011
|
+
row,
|
|
1012
|
+
turn,
|
|
1013
|
+
idleMs,
|
|
1014
|
+
subagent,
|
|
1015
|
+
workflow,
|
|
1016
|
+
pr,
|
|
1017
|
+
cold,
|
|
1018
|
+
model,
|
|
1019
|
+
}: {
|
|
1020
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
1021
|
+
clientX: number;
|
|
1022
|
+
row?: PresentationRow;
|
|
1023
|
+
turn?: TurnMegaRow;
|
|
1024
|
+
idleMs?: number;
|
|
1025
|
+
subagent?: SubagentRun;
|
|
1026
|
+
workflow?: WorkflowRun;
|
|
1027
|
+
pr?: PrMarker;
|
|
1028
|
+
cold?: { tOffsetMs: number; info: NonNullable<SessionEvent["coldResume"]> };
|
|
1029
|
+
model?: string;
|
|
1030
|
+
}) {
|
|
1031
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
1032
|
+
const localX = rect ? clientX - rect.left : 0;
|
|
1033
|
+
const left = Math.min(Math.max(localX - 140, 8), (rect?.width ?? 1400) - 300);
|
|
1034
|
+
// Always open below the minimap. The minimap lives inside the sticky
|
|
1035
|
+
// header, so there's never reliable space above it for a tooltip.
|
|
1036
|
+
const posStyle = { top: "calc(100% + 8px)", left };
|
|
1037
|
+
|
|
1038
|
+
if (workflow) {
|
|
1039
|
+
const w = workflow;
|
|
1040
|
+
const startOff = formatOffset(w.startTOffsetMs);
|
|
1041
|
+
const endOff = formatOffset(w.endTOffsetMs);
|
|
1042
|
+
const dur = w.durationMs !== undefined ? formatGap(w.durationMs) : "";
|
|
1043
|
+
return (
|
|
1044
|
+
<div
|
|
1045
|
+
style={{
|
|
1046
|
+
position: "absolute",
|
|
1047
|
+
...posStyle,
|
|
1048
|
+
zIndex: 100,
|
|
1049
|
+
background: "#0F172A",
|
|
1050
|
+
color: "#F1F5F9",
|
|
1051
|
+
borderRadius: 10,
|
|
1052
|
+
padding: "12px 14px",
|
|
1053
|
+
fontSize: 11,
|
|
1054
|
+
pointerEvents: "none",
|
|
1055
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
1056
|
+
maxWidth: 440,
|
|
1057
|
+
minWidth: 280,
|
|
1058
|
+
lineHeight: 1.45,
|
|
1059
|
+
}}
|
|
1060
|
+
>
|
|
1061
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
|
|
1062
|
+
<span
|
|
1063
|
+
style={{
|
|
1064
|
+
fontSize: 10,
|
|
1065
|
+
fontWeight: 600,
|
|
1066
|
+
padding: "2px 8px",
|
|
1067
|
+
borderRadius: 4,
|
|
1068
|
+
background: workflowColor(w.status),
|
|
1069
|
+
color: "#fff",
|
|
1070
|
+
textTransform: "uppercase",
|
|
1071
|
+
letterSpacing: "0.04em",
|
|
1072
|
+
}}
|
|
1073
|
+
>
|
|
1074
|
+
workflow · {w.status}
|
|
1075
|
+
</span>
|
|
1076
|
+
<span
|
|
1077
|
+
style={{
|
|
1078
|
+
fontFamily: "var(--font-mono)",
|
|
1079
|
+
fontSize: 10,
|
|
1080
|
+
opacity: 0.7,
|
|
1081
|
+
marginLeft: "auto",
|
|
1082
|
+
}}
|
|
1083
|
+
>
|
|
1084
|
+
{startOff} → {endOff} · {dur}
|
|
1085
|
+
</span>
|
|
1086
|
+
</div>
|
|
1087
|
+
<div style={{ fontWeight: 600, marginBottom: 4, fontFamily: "var(--font-mono)" }}>
|
|
1088
|
+
{w.name}
|
|
1089
|
+
</div>
|
|
1090
|
+
{w.description && (
|
|
1091
|
+
<div style={{ opacity: 0.8, marginBottom: 6 }}>{w.description}</div>
|
|
1092
|
+
)}
|
|
1093
|
+
<div
|
|
1094
|
+
style={{
|
|
1095
|
+
fontSize: 10,
|
|
1096
|
+
opacity: 0.75,
|
|
1097
|
+
display: "flex",
|
|
1098
|
+
gap: 8,
|
|
1099
|
+
flexWrap: "wrap",
|
|
1100
|
+
paddingTop: 6,
|
|
1101
|
+
borderTop: "1px solid rgba(241,245,249,0.08)",
|
|
1102
|
+
fontFamily: "var(--font-mono)",
|
|
1103
|
+
}}
|
|
1104
|
+
>
|
|
1105
|
+
<span style={{ color: "#FB923C", fontWeight: 600 }}>{w.agentCount} agents</span>
|
|
1106
|
+
<span>·</span>
|
|
1107
|
+
<span>{w.toolCallCount.toLocaleString()} tools</span>
|
|
1108
|
+
<span>·</span>
|
|
1109
|
+
<span>{formatTokens(w.totalTokens)} tok</span>
|
|
1110
|
+
{w.phases.length > 0 && (
|
|
1111
|
+
<>
|
|
1112
|
+
<span>·</span>
|
|
1113
|
+
<span>{w.phases.length} phases</span>
|
|
1114
|
+
</>
|
|
1115
|
+
)}
|
|
1116
|
+
</div>
|
|
1117
|
+
<div style={{ marginTop: 6, fontSize: 10, opacity: 0.55, fontStyle: "italic" }}>
|
|
1118
|
+
Click to open this run in the Workflows tab.
|
|
1119
|
+
</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (subagent) {
|
|
1125
|
+
const startOff = formatOffset(subagent.startTOffsetMs);
|
|
1126
|
+
const endOff = formatOffset(subagent.endTOffsetMs);
|
|
1127
|
+
const dur = subagent.durationMs !== undefined ? formatGap(subagent.durationMs) : "";
|
|
1128
|
+
const totalIn =
|
|
1129
|
+
subagent.totalUsage.input + subagent.totalUsage.cacheRead + subagent.totalUsage.cacheWrite;
|
|
1130
|
+
return (
|
|
1131
|
+
<div
|
|
1132
|
+
style={{
|
|
1133
|
+
position: "absolute",
|
|
1134
|
+
...posStyle,
|
|
1135
|
+
zIndex: 100,
|
|
1136
|
+
background: "#0F172A",
|
|
1137
|
+
color: "#F1F5F9",
|
|
1138
|
+
borderRadius: 10,
|
|
1139
|
+
padding: "12px 14px",
|
|
1140
|
+
fontSize: 11,
|
|
1141
|
+
pointerEvents: "none",
|
|
1142
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
1143
|
+
maxWidth: 440,
|
|
1144
|
+
minWidth: 280,
|
|
1145
|
+
lineHeight: 1.45,
|
|
1146
|
+
}}
|
|
1147
|
+
>
|
|
1148
|
+
<div
|
|
1149
|
+
style={{
|
|
1150
|
+
display: "flex",
|
|
1151
|
+
alignItems: "center",
|
|
1152
|
+
gap: 8,
|
|
1153
|
+
marginBottom: 8,
|
|
1154
|
+
}}
|
|
1155
|
+
>
|
|
1156
|
+
<span
|
|
1157
|
+
style={{
|
|
1158
|
+
fontSize: 10,
|
|
1159
|
+
fontWeight: 600,
|
|
1160
|
+
padding: "2px 8px",
|
|
1161
|
+
borderRadius: 4,
|
|
1162
|
+
background: subagentColor(subagent.agentType, subagent.runInBackground),
|
|
1163
|
+
color: "#fff",
|
|
1164
|
+
}}
|
|
1165
|
+
>
|
|
1166
|
+
{subagent.agentType}
|
|
1167
|
+
</span>
|
|
1168
|
+
{subagent.runInBackground && (
|
|
1169
|
+
<span
|
|
1170
|
+
style={{
|
|
1171
|
+
fontSize: 9,
|
|
1172
|
+
fontWeight: 600,
|
|
1173
|
+
padding: "1px 6px",
|
|
1174
|
+
borderRadius: 3,
|
|
1175
|
+
background: "rgba(251, 191, 36, 0.18)",
|
|
1176
|
+
color: "#FBBF24",
|
|
1177
|
+
textTransform: "uppercase",
|
|
1178
|
+
letterSpacing: "0.04em",
|
|
1179
|
+
}}
|
|
1180
|
+
>
|
|
1181
|
+
background
|
|
1182
|
+
</span>
|
|
1183
|
+
)}
|
|
1184
|
+
<span
|
|
1185
|
+
style={{
|
|
1186
|
+
fontFamily: "var(--font-mono)",
|
|
1187
|
+
fontSize: 10,
|
|
1188
|
+
opacity: 0.7,
|
|
1189
|
+
marginLeft: "auto",
|
|
1190
|
+
}}
|
|
1191
|
+
>
|
|
1192
|
+
{startOff} → {endOff} · {dur}
|
|
1193
|
+
</span>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div style={{ fontWeight: 500, marginBottom: 6 }}>{subagent.description}</div>
|
|
1196
|
+
<div
|
|
1197
|
+
style={{
|
|
1198
|
+
fontSize: 10,
|
|
1199
|
+
opacity: 0.65,
|
|
1200
|
+
display: "flex",
|
|
1201
|
+
gap: 8,
|
|
1202
|
+
paddingTop: 6,
|
|
1203
|
+
borderTop: "1px solid rgba(241,245,249,0.08)",
|
|
1204
|
+
}}
|
|
1205
|
+
>
|
|
1206
|
+
<span>{subagent.eventCount} events</span>
|
|
1207
|
+
<span>·</span>
|
|
1208
|
+
<span style={{ fontFamily: "var(--font-mono)" }}>
|
|
1209
|
+
{formatTokens(totalIn)}/{formatTokens(subagent.totalUsage.output)} tok
|
|
1210
|
+
</span>
|
|
1211
|
+
</div>
|
|
1212
|
+
{subagent.finalPreview && (
|
|
1213
|
+
<div
|
|
1214
|
+
style={{
|
|
1215
|
+
marginTop: 6,
|
|
1216
|
+
opacity: 0.78,
|
|
1217
|
+
fontStyle: "italic",
|
|
1218
|
+
display: "-webkit-box",
|
|
1219
|
+
WebkitLineClamp: 2,
|
|
1220
|
+
WebkitBoxOrient: "vertical",
|
|
1221
|
+
overflow: "hidden",
|
|
1222
|
+
}}
|
|
1223
|
+
>
|
|
1224
|
+
→ {subagent.finalPreview}
|
|
1225
|
+
</div>
|
|
1226
|
+
)}
|
|
1227
|
+
</div>
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (cold) {
|
|
1232
|
+
const { trigger, gapMs, writeTokens, compact } = cold.info;
|
|
1233
|
+
const isCompact = trigger === "compact";
|
|
1234
|
+
const estUsd = estimateCost(
|
|
1235
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
1236
|
+
model,
|
|
1237
|
+
);
|
|
1238
|
+
let label: string;
|
|
1239
|
+
if (!isCompact) label = "⚡ CACHE REBUILD";
|
|
1240
|
+
else if (compact?.trigger === "auto") label = "⚡ AUTO-COMPACT";
|
|
1241
|
+
else label = "⚡ /COMPACT";
|
|
1242
|
+
const detailLine = isCompact
|
|
1243
|
+
? `${formatTokens(writeTokens)} rewritten · pre-compact ${formatTokens(compact?.preTokens ?? 0)}`
|
|
1244
|
+
: `${formatTokens(writeTokens)} rewritten · idle ${formatGap(gapMs)}`;
|
|
1245
|
+
const hint = isCompact
|
|
1246
|
+
? "Conversation was summarized; prefix had to be rewritten into a fresh cache."
|
|
1247
|
+
: "Prompt cache expired during idle; resuming within 5 min keeps it warm.";
|
|
1248
|
+
return (
|
|
1249
|
+
<div
|
|
1250
|
+
style={{
|
|
1251
|
+
position: "absolute",
|
|
1252
|
+
...posStyle,
|
|
1253
|
+
zIndex: 100,
|
|
1254
|
+
background: "#0F172A",
|
|
1255
|
+
color: "#F1F5F9",
|
|
1256
|
+
borderRadius: 10,
|
|
1257
|
+
padding: "10px 14px",
|
|
1258
|
+
fontSize: 11,
|
|
1259
|
+
pointerEvents: "none",
|
|
1260
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
1261
|
+
maxWidth: 340,
|
|
1262
|
+
minWidth: 220,
|
|
1263
|
+
lineHeight: 1.45,
|
|
1264
|
+
}}
|
|
1265
|
+
>
|
|
1266
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
|
1267
|
+
<span
|
|
1268
|
+
style={{
|
|
1269
|
+
fontSize: 10,
|
|
1270
|
+
fontWeight: 600,
|
|
1271
|
+
padding: "2px 8px",
|
|
1272
|
+
borderRadius: 4,
|
|
1273
|
+
background: "#D97706",
|
|
1274
|
+
color: "#fff",
|
|
1275
|
+
}}
|
|
1276
|
+
>
|
|
1277
|
+
{label}
|
|
1278
|
+
</span>
|
|
1279
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.7 }}>
|
|
1280
|
+
at {formatOffset(cold.tOffsetMs)}
|
|
1281
|
+
</span>
|
|
1282
|
+
</div>
|
|
1283
|
+
<div style={{ fontWeight: 500, fontFamily: "var(--font-mono)" }}>
|
|
1284
|
+
{detailLine}
|
|
1285
|
+
{estUsd >= 0.005 && ` · est. ${formatCost(estUsd)}`}
|
|
1286
|
+
</div>
|
|
1287
|
+
<div
|
|
1288
|
+
style={{
|
|
1289
|
+
marginTop: 4,
|
|
1290
|
+
fontSize: 10,
|
|
1291
|
+
opacity: 0.65,
|
|
1292
|
+
fontStyle: "italic",
|
|
1293
|
+
whiteSpace: "normal",
|
|
1294
|
+
}}
|
|
1295
|
+
>
|
|
1296
|
+
{hint}
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (pr) {
|
|
1303
|
+
const label = pr.title
|
|
1304
|
+
? `PR${pr.prNumber ? ` #${pr.prNumber}` : ""}: ${pr.title}`
|
|
1305
|
+
: pr.prNumber
|
|
1306
|
+
? `PR #${pr.prNumber}`
|
|
1307
|
+
: "PR created";
|
|
1308
|
+
return (
|
|
1309
|
+
<div
|
|
1310
|
+
style={{
|
|
1311
|
+
position: "absolute",
|
|
1312
|
+
...posStyle,
|
|
1313
|
+
zIndex: 100,
|
|
1314
|
+
background: "#0F172A",
|
|
1315
|
+
color: "#F1F5F9",
|
|
1316
|
+
borderRadius: 10,
|
|
1317
|
+
padding: "10px 14px",
|
|
1318
|
+
fontSize: 11,
|
|
1319
|
+
pointerEvents: "none",
|
|
1320
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
1321
|
+
maxWidth: 360,
|
|
1322
|
+
minWidth: 200,
|
|
1323
|
+
lineHeight: 1.45,
|
|
1324
|
+
}}
|
|
1325
|
+
>
|
|
1326
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
|
1327
|
+
<span
|
|
1328
|
+
style={{
|
|
1329
|
+
fontSize: 10,
|
|
1330
|
+
fontWeight: 600,
|
|
1331
|
+
padding: "2px 8px",
|
|
1332
|
+
borderRadius: 4,
|
|
1333
|
+
background: "#8b5cf6",
|
|
1334
|
+
color: "#fff",
|
|
1335
|
+
}}
|
|
1336
|
+
>
|
|
1337
|
+
PR
|
|
1338
|
+
</span>
|
|
1339
|
+
{pr.tOffsetMs !== undefined && (
|
|
1340
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.7 }}>
|
|
1341
|
+
at {formatOffset(pr.tOffsetMs)}
|
|
1342
|
+
</span>
|
|
1343
|
+
)}
|
|
1344
|
+
</div>
|
|
1345
|
+
<div style={{ fontWeight: 500 }}>{label}</div>
|
|
1346
|
+
</div>
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (idleMs !== undefined) {
|
|
1351
|
+
return (
|
|
1352
|
+
<div
|
|
1353
|
+
style={{
|
|
1354
|
+
position: "absolute",
|
|
1355
|
+
...posStyle,
|
|
1356
|
+
zIndex: 100,
|
|
1357
|
+
background: "#0F172A",
|
|
1358
|
+
color: "#F1F5F9",
|
|
1359
|
+
borderRadius: 10,
|
|
1360
|
+
padding: "8px 12px",
|
|
1361
|
+
fontSize: 11,
|
|
1362
|
+
pointerEvents: "none",
|
|
1363
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
1364
|
+
}}
|
|
1365
|
+
>
|
|
1366
|
+
<div style={{ fontWeight: 600, marginBottom: 2 }}>Session idle</div>
|
|
1367
|
+
<div style={{ opacity: 0.78, fontFamily: "var(--font-mono)" }}>{formatGap(idleMs)}</div>
|
|
1368
|
+
</div>
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (turn) {
|
|
1373
|
+
const theme = ROLE_THEMES.agent;
|
|
1374
|
+
const s = turn.summary;
|
|
1375
|
+
const dur = turn.durationMs !== undefined ? formatGap(turn.durationMs) : "";
|
|
1376
|
+
const startOff = formatOffset(turn.tOffsetMs);
|
|
1377
|
+
const endOff =
|
|
1378
|
+
turn.tOffsetMs !== undefined && turn.durationMs !== undefined
|
|
1379
|
+
? formatOffset(turn.tOffsetMs + turn.durationMs)
|
|
1380
|
+
: undefined;
|
|
1381
|
+
const hasFirstLast =
|
|
1382
|
+
s.firstAgentPreview && s.finalAgentPreview && s.firstAgentPreview !== s.finalAgentPreview;
|
|
1383
|
+
// Top tools aggregated summary line (up to 3 entries)
|
|
1384
|
+
const topTools = s.toolNames
|
|
1385
|
+
.slice(0, 3)
|
|
1386
|
+
.map((t) =>
|
|
1387
|
+
t.count > 1 ? `${shortenToolName(t.name)} ×${t.count}` : shortenToolName(t.name),
|
|
1388
|
+
)
|
|
1389
|
+
.join(" · ");
|
|
1390
|
+
|
|
1391
|
+
return (
|
|
1392
|
+
<div
|
|
1393
|
+
style={{
|
|
1394
|
+
position: "absolute",
|
|
1395
|
+
...posStyle,
|
|
1396
|
+
zIndex: 100,
|
|
1397
|
+
background: "#0F172A",
|
|
1398
|
+
color: "#F1F5F9",
|
|
1399
|
+
borderRadius: 10,
|
|
1400
|
+
padding: "12px 14px",
|
|
1401
|
+
fontSize: 11,
|
|
1402
|
+
pointerEvents: "none",
|
|
1403
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
1404
|
+
maxWidth: 440,
|
|
1405
|
+
minWidth: 280,
|
|
1406
|
+
lineHeight: 1.45,
|
|
1407
|
+
}}
|
|
1408
|
+
>
|
|
1409
|
+
{/* Header: Turn pill + start→end range */}
|
|
1410
|
+
<div
|
|
1411
|
+
style={{
|
|
1412
|
+
display: "flex",
|
|
1413
|
+
alignItems: "center",
|
|
1414
|
+
gap: 8,
|
|
1415
|
+
marginBottom: 8,
|
|
1416
|
+
}}
|
|
1417
|
+
>
|
|
1418
|
+
<span
|
|
1419
|
+
style={{
|
|
1420
|
+
fontSize: 10,
|
|
1421
|
+
fontWeight: 600,
|
|
1422
|
+
padding: "2px 8px",
|
|
1423
|
+
borderRadius: 4,
|
|
1424
|
+
background: theme.mini,
|
|
1425
|
+
color: "#fff",
|
|
1426
|
+
}}
|
|
1427
|
+
>
|
|
1428
|
+
Turn
|
|
1429
|
+
</span>
|
|
1430
|
+
<span
|
|
1431
|
+
style={{
|
|
1432
|
+
fontFamily: "var(--font-mono)",
|
|
1433
|
+
fontSize: 10,
|
|
1434
|
+
opacity: 0.7,
|
|
1435
|
+
}}
|
|
1436
|
+
>
|
|
1437
|
+
{endOff ? `${startOff} → ${endOff}` : startOff}
|
|
1438
|
+
</span>
|
|
1439
|
+
{dur && (
|
|
1440
|
+
<span
|
|
1441
|
+
style={{
|
|
1442
|
+
fontFamily: "var(--font-mono)",
|
|
1443
|
+
fontSize: 10,
|
|
1444
|
+
opacity: 0.55,
|
|
1445
|
+
}}
|
|
1446
|
+
>
|
|
1447
|
+
· {dur}
|
|
1448
|
+
</span>
|
|
1449
|
+
)}
|
|
1450
|
+
</div>
|
|
1451
|
+
|
|
1452
|
+
{/* First agent message */}
|
|
1453
|
+
{s.firstAgentPreview && (
|
|
1454
|
+
<div
|
|
1455
|
+
style={{
|
|
1456
|
+
opacity: 0.95,
|
|
1457
|
+
fontWeight: 500,
|
|
1458
|
+
display: "-webkit-box",
|
|
1459
|
+
WebkitLineClamp: 2,
|
|
1460
|
+
WebkitBoxOrient: "vertical",
|
|
1461
|
+
overflow: "hidden",
|
|
1462
|
+
marginBottom: 6,
|
|
1463
|
+
}}
|
|
1464
|
+
>
|
|
1465
|
+
{s.firstAgentPreview}
|
|
1466
|
+
</div>
|
|
1467
|
+
)}
|
|
1468
|
+
|
|
1469
|
+
{/* Middle: stats + top tools */}
|
|
1470
|
+
<div
|
|
1471
|
+
style={{
|
|
1472
|
+
fontSize: 10,
|
|
1473
|
+
opacity: 0.65,
|
|
1474
|
+
marginBottom: hasFirstLast ? 6 : 0,
|
|
1475
|
+
borderTop: s.firstAgentPreview ? "1px solid rgba(241,245,249,0.08)" : undefined,
|
|
1476
|
+
paddingTop: s.firstAgentPreview ? 6 : 0,
|
|
1477
|
+
}}
|
|
1478
|
+
>
|
|
1479
|
+
{s.agentMessages} msg · {s.toolCalls} tools
|
|
1480
|
+
{s.errors > 0 ? ` · ${s.errors} err` : ""}
|
|
1481
|
+
{topTools && (
|
|
1482
|
+
<>
|
|
1483
|
+
<span style={{ opacity: 0.5, margin: "0 4px" }}>·</span>
|
|
1484
|
+
<span style={{ fontFamily: "var(--font-mono)" }}>{topTools}</span>
|
|
1485
|
+
{s.toolNames.length > 3 && (
|
|
1486
|
+
<span style={{ opacity: 0.65 }}> +{s.toolNames.length - 3}</span>
|
|
1487
|
+
)}
|
|
1488
|
+
</>
|
|
1489
|
+
)}
|
|
1490
|
+
</div>
|
|
1491
|
+
|
|
1492
|
+
{/* Last agent message */}
|
|
1493
|
+
{hasFirstLast && s.finalAgentPreview && (
|
|
1494
|
+
<div
|
|
1495
|
+
style={{
|
|
1496
|
+
opacity: 0.92,
|
|
1497
|
+
display: "flex",
|
|
1498
|
+
gap: 6,
|
|
1499
|
+
alignItems: "flex-start",
|
|
1500
|
+
borderTop: "1px solid rgba(241,245,249,0.08)",
|
|
1501
|
+
paddingTop: 6,
|
|
1502
|
+
}}
|
|
1503
|
+
>
|
|
1504
|
+
<span style={{ opacity: 0.5 }}>→</span>
|
|
1505
|
+
<span
|
|
1506
|
+
style={{
|
|
1507
|
+
display: "-webkit-box",
|
|
1508
|
+
WebkitLineClamp: 2,
|
|
1509
|
+
WebkitBoxOrient: "vertical",
|
|
1510
|
+
overflow: "hidden",
|
|
1511
|
+
}}
|
|
1512
|
+
>
|
|
1513
|
+
{s.finalAgentPreview}
|
|
1514
|
+
</span>
|
|
1515
|
+
</div>
|
|
1516
|
+
)}
|
|
1517
|
+
</div>
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (!row) return null;
|
|
1522
|
+
|
|
1523
|
+
const theme = ROLE_THEMES[row.kind];
|
|
1524
|
+
const preview = rowPreview(row);
|
|
1525
|
+
|
|
1526
|
+
return (
|
|
1527
|
+
<div
|
|
1528
|
+
style={{
|
|
1529
|
+
position: "absolute",
|
|
1530
|
+
...posStyle,
|
|
1531
|
+
zIndex: 100,
|
|
1532
|
+
background: "#0F172A",
|
|
1533
|
+
color: "#F1F5F9",
|
|
1534
|
+
borderRadius: 10,
|
|
1535
|
+
padding: "10px 14px",
|
|
1536
|
+
fontSize: 11,
|
|
1537
|
+
pointerEvents: "none",
|
|
1538
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
1539
|
+
maxWidth: 360,
|
|
1540
|
+
minWidth: 220,
|
|
1541
|
+
}}
|
|
1542
|
+
>
|
|
1543
|
+
<div
|
|
1544
|
+
style={{
|
|
1545
|
+
display: "flex",
|
|
1546
|
+
alignItems: "center",
|
|
1547
|
+
gap: 8,
|
|
1548
|
+
marginBottom: 6,
|
|
1549
|
+
}}
|
|
1550
|
+
>
|
|
1551
|
+
<span
|
|
1552
|
+
style={{
|
|
1553
|
+
fontSize: 10,
|
|
1554
|
+
fontWeight: 600,
|
|
1555
|
+
padding: "2px 8px",
|
|
1556
|
+
borderRadius: 4,
|
|
1557
|
+
background: theme.mini,
|
|
1558
|
+
color: "#fff",
|
|
1559
|
+
}}
|
|
1560
|
+
>
|
|
1561
|
+
{theme.label}
|
|
1562
|
+
</span>
|
|
1563
|
+
<span
|
|
1564
|
+
style={{
|
|
1565
|
+
fontFamily: "var(--font-mono)",
|
|
1566
|
+
fontSize: 10,
|
|
1567
|
+
opacity: 0.65,
|
|
1568
|
+
}}
|
|
1569
|
+
>
|
|
1570
|
+
{formatOffset(row.tOffsetMs)}
|
|
1571
|
+
</span>
|
|
1572
|
+
</div>
|
|
1573
|
+
<div
|
|
1574
|
+
style={{
|
|
1575
|
+
lineHeight: 1.45,
|
|
1576
|
+
opacity: 0.92,
|
|
1577
|
+
display: "-webkit-box",
|
|
1578
|
+
WebkitLineClamp: 3,
|
|
1579
|
+
WebkitBoxOrient: "vertical",
|
|
1580
|
+
overflow: "hidden",
|
|
1581
|
+
}}
|
|
1582
|
+
>
|
|
1583
|
+
{preview}
|
|
1584
|
+
</div>
|
|
1585
|
+
</div>
|
|
1586
|
+
);
|
|
1587
|
+
}
|