fleetlens 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/build-manifest.json +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.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/page.js +3 -3
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +3 -3
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +3 -2
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.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]__0op9af~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__05lbtny._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ms7h0~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08cr2u9._.js → [root-of-the-server]__0p~g4m6._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wvzyl7._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0ph19-k._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/_0u29mdd._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +1 -1
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +6 -0
- package/app/apps/web/.next/static/chunks/08983lz4~hffx.js +6 -0
- package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +1 -0
- package/app/apps/web/.next/static/chunks/0p1lpoh02zh8h.js +10 -0
- package/app/apps/web/.next/static/chunks/{03fw7f2obcvq~.js → 14q3xsfpkvuef.js} +2 -2
- package/app/apps/web/app/layout.tsx +2 -0
- package/app/apps/web/app/projects/[slug]/page.tsx +2 -0
- package/app/apps/web/app/sessions/[id]/page.tsx +27 -1
- package/app/apps/web/app/sessions/[id]/session-view.tsx +372 -793
- package/app/apps/web/app/sessions/[id]/team-tab/adapter.ts +633 -0
- package/app/apps/web/app/sessions/[id]/team-tab/format.ts +20 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +682 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-tab-client.tsx +78 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-table.tsx +796 -0
- package/app/apps/web/app/sessions/[id]/team-tab/turn-drawer.tsx +805 -0
- package/app/apps/web/app/sessions/[id]/tool-cards.tsx +461 -0
- package/app/apps/web/app/sessions/[id]/turn-steps.tsx +306 -0
- package/app/apps/web/app/sessions/sessions-grid.tsx +36 -5
- package/app/apps/web/components/live-sessions-widget.tsx +20 -6
- package/app/apps/web/components/team-badge.tsx +90 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/index.js +71 -13
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b..h_8._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0cdd~8n._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~a~v00._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/_0zv~jv-._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/_12dtryh._.js +0 -12
- package/app/apps/web/.next/static/chunks/07oyxajlp3dvb.js +0 -6
- package/app/apps/web/.next/static/chunks/0a24elrqoqqep.js +0 -10
- package/app/apps/web/.next/static/chunks/0i4zodv86~~~i.js +0 -1
- package/app/apps/web/.next/static/chunks/0rbm60-qyshdm.js +0 -6
- /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → 0fPDRTdMQ-Y62h6fHNJXL}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → 0fPDRTdMQ-Y62h6fHNJXL}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → 0fPDRTdMQ-Y62h6fHNJXL}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
7
|
+
import { formatGap } from "@/lib/format";
|
|
8
|
+
import type { TimelineData, TeamTrack, TeamTurn, XAnchor, MinimapIdleBand } from "./adapter";
|
|
9
|
+
import { xOfMs, msOfXFrac } from "./adapter";
|
|
10
|
+
import { formatTeammatePreview } from "./format";
|
|
11
|
+
|
|
12
|
+
const LANE_HEIGHT = 24;
|
|
13
|
+
const LANE_GAP = 2;
|
|
14
|
+
const LABEL_WIDTH = 100;
|
|
15
|
+
export const DEFAULT_VISIBLE_LANES = 4;
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
data: TimelineData;
|
|
19
|
+
playheadMs: number | null;
|
|
20
|
+
onSeek: (tsMs: number, trackId?: string) => void;
|
|
21
|
+
expanded: boolean;
|
|
22
|
+
onToggleExpanded: () => void;
|
|
23
|
+
/** Member track ids currently visible in the body table (mid-point inside
|
|
24
|
+
* the horizontal viewport). When the minimap is collapsed, the lane set
|
|
25
|
+
* mirrors this — lead first, then whichever members are currently in the
|
|
26
|
+
* table's viewport. Falls back to "first N members by start time" when
|
|
27
|
+
* empty, so the initial render is sane. */
|
|
28
|
+
visibleTrackIds: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type HoverState = {
|
|
32
|
+
turn: TeamTurn;
|
|
33
|
+
track: TeamTrack;
|
|
34
|
+
clientX: number;
|
|
35
|
+
clientY: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function TeamMinimap({
|
|
39
|
+
data,
|
|
40
|
+
playheadMs,
|
|
41
|
+
onSeek,
|
|
42
|
+
expanded,
|
|
43
|
+
onToggleExpanded,
|
|
44
|
+
visibleTrackIds,
|
|
45
|
+
}: Props) {
|
|
46
|
+
const [hover, setHover] = useState<HoverState | null>(null);
|
|
47
|
+
const hasOverflow = data.tracks.length > DEFAULT_VISIBLE_LANES;
|
|
48
|
+
|
|
49
|
+
// Day-page mode: when the team span exceeds 24h, the adapter pre-computes
|
|
50
|
+
// per-day x-scales and we render one page at a time. The minimap's
|
|
51
|
+
// rangeStart/rangeEnd, xAnchors, and idle bands all come from the active
|
|
52
|
+
// page instead of the global ones.
|
|
53
|
+
const dayPages = data.dayPages ?? null;
|
|
54
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
55
|
+
// Follow the playhead: if the table scrolls into a different day, advance
|
|
56
|
+
// the minimap page to match so the user's focus stays in view.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!dayPages || playheadMs == null) return;
|
|
59
|
+
const match = dayPages.findIndex(
|
|
60
|
+
(p) => playheadMs >= p.startMs && playheadMs <= p.endMs,
|
|
61
|
+
);
|
|
62
|
+
if (match !== -1 && match !== pageIndex) setPageIndex(match);
|
|
63
|
+
}, [dayPages, playheadMs, pageIndex]);
|
|
64
|
+
// Clamp pageIndex in case the dataset shrinks between renders.
|
|
65
|
+
const safePageIndex = dayPages
|
|
66
|
+
? Math.min(Math.max(0, pageIndex), dayPages.length - 1)
|
|
67
|
+
: 0;
|
|
68
|
+
const activePage = dayPages ? dayPages[safePageIndex] : null;
|
|
69
|
+
|
|
70
|
+
const activeXAnchors: XAnchor[] = activePage ? activePage.xAnchors : data.xAnchors;
|
|
71
|
+
const activeIdleBands: MinimapIdleBand[] = activePage
|
|
72
|
+
? activePage.minimapIdleBands
|
|
73
|
+
: data.minimapIdleBands;
|
|
74
|
+
const rangeStartMs = activePage ? activePage.startMs : data.firstEventMs;
|
|
75
|
+
const rangeEndMs = activePage ? activePage.endMs : data.lastEventMs;
|
|
76
|
+
|
|
77
|
+
// Compute which tracks are "active" (shown at full height). The minimap
|
|
78
|
+
// always renders every track so CSS transitions can animate lanes in and
|
|
79
|
+
// out when the active set changes; non-active lanes collapse to height 0
|
|
80
|
+
// via transition instead of unmounting.
|
|
81
|
+
// - expanded → every track active
|
|
82
|
+
// - collapsed + table has reported visible members → lead + those
|
|
83
|
+
// - collapsed + no report yet → lead + first N-1 members (initial)
|
|
84
|
+
let activeIds: Set<string>;
|
|
85
|
+
if (expanded || !hasOverflow) {
|
|
86
|
+
activeIds = new Set(data.tracks.map((t) => t.id));
|
|
87
|
+
} else {
|
|
88
|
+
const lead = data.tracks[0];
|
|
89
|
+
const members = data.tracks.slice(1);
|
|
90
|
+
const pickMembers =
|
|
91
|
+
visibleTrackIds.length > 0
|
|
92
|
+
? members
|
|
93
|
+
.filter((t) => visibleTrackIds.includes(t.id))
|
|
94
|
+
.slice(0, DEFAULT_VISIBLE_LANES - 1)
|
|
95
|
+
.map((t) => t.id)
|
|
96
|
+
: members.slice(0, DEFAULT_VISIBLE_LANES - 1).map((t) => t.id);
|
|
97
|
+
activeIds = new Set(pickMembers);
|
|
98
|
+
if (lead) activeIds.add(lead.id);
|
|
99
|
+
}
|
|
100
|
+
const hiddenCount = data.tracks.length - activeIds.size;
|
|
101
|
+
|
|
102
|
+
// Event-anchored x-scale — active intervals stay proportional with a floor,
|
|
103
|
+
// all-idle intervals (including multi-day overnight gaps) collapse into
|
|
104
|
+
// compact bands. Shared across lanes so cross-agent alignment holds.
|
|
105
|
+
// In day-page mode, xOf uses the current page's anchors so each day
|
|
106
|
+
// fills the minimap width instead of sharing a cramped global strip.
|
|
107
|
+
const xOf = (ms: number) => xOfMs(activeXAnchors, ms);
|
|
108
|
+
|
|
109
|
+
const onLaneClick = (
|
|
110
|
+
track: TeamTrack,
|
|
111
|
+
e: React.MouseEvent<HTMLDivElement>,
|
|
112
|
+
) => {
|
|
113
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
114
|
+
const x = Math.max(0, e.clientX - rect.left);
|
|
115
|
+
const frac = Math.min(1, x / Math.max(1, rect.width));
|
|
116
|
+
onSeek(msOfXFrac(activeXAnchors, frac), track.id);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Playhead only renders when it's inside the current page's window.
|
|
120
|
+
const playheadFrac =
|
|
121
|
+
playheadMs != null && playheadMs >= rangeStartMs && playheadMs <= rangeEndMs
|
|
122
|
+
? Math.max(0, Math.min(1, xOf(playheadMs)))
|
|
123
|
+
: null;
|
|
124
|
+
|
|
125
|
+
// Turn bars are clipped to the active page — bars whose entire range is
|
|
126
|
+
// outside the current day window are skipped, bars that straddle the
|
|
127
|
+
// boundary get clamped to the window's edges.
|
|
128
|
+
const clipTurn = (turn: TeamTurn) => {
|
|
129
|
+
if (turn.endMs < rangeStartMs || turn.startMs > rangeEndMs) return null;
|
|
130
|
+
return {
|
|
131
|
+
id: turn.id,
|
|
132
|
+
startMs: Math.max(turn.startMs, rangeStartMs),
|
|
133
|
+
endMs: Math.min(turn.endMs, rangeEndMs),
|
|
134
|
+
original: turn,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
style={{
|
|
141
|
+
position: "sticky",
|
|
142
|
+
top: 0,
|
|
143
|
+
zIndex: 10,
|
|
144
|
+
background: "var(--af-surface-elevated)",
|
|
145
|
+
border: "1px solid var(--af-border-subtle)",
|
|
146
|
+
borderRadius: 6,
|
|
147
|
+
padding: 8,
|
|
148
|
+
marginBottom: 8,
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<div
|
|
152
|
+
style={{
|
|
153
|
+
display: "flex",
|
|
154
|
+
justifyContent: "space-between",
|
|
155
|
+
alignItems: "center",
|
|
156
|
+
fontSize: 9,
|
|
157
|
+
color: "var(--af-text-tertiary)",
|
|
158
|
+
fontFamily: "ui-monospace, monospace",
|
|
159
|
+
marginBottom: 6,
|
|
160
|
+
paddingLeft: LABEL_WIDTH,
|
|
161
|
+
gap: 8,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<span>{formatEdge(rangeStartMs, data.multiDay)}</span>
|
|
165
|
+
{dayPages && dayPages.length > 1 && (
|
|
166
|
+
<div
|
|
167
|
+
style={{
|
|
168
|
+
display: "flex",
|
|
169
|
+
alignItems: "center",
|
|
170
|
+
gap: 4,
|
|
171
|
+
fontSize: 10,
|
|
172
|
+
fontWeight: 600,
|
|
173
|
+
color: "var(--af-text-secondary)",
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<button
|
|
177
|
+
onClick={() => setPageIndex((i) => Math.max(0, i - 1))}
|
|
178
|
+
disabled={safePageIndex === 0}
|
|
179
|
+
aria-label="Previous day"
|
|
180
|
+
style={pageBtnStyle(safePageIndex === 0)}
|
|
181
|
+
>
|
|
182
|
+
<ChevronLeft size={11} />
|
|
183
|
+
</button>
|
|
184
|
+
<span
|
|
185
|
+
style={{
|
|
186
|
+
minWidth: 64,
|
|
187
|
+
textAlign: "center",
|
|
188
|
+
fontFamily: "ui-monospace, monospace",
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{activePage?.label ?? ""}{" "}
|
|
192
|
+
<span style={{ opacity: 0.55 }}>
|
|
193
|
+
({safePageIndex + 1}/{dayPages.length})
|
|
194
|
+
</span>
|
|
195
|
+
</span>
|
|
196
|
+
<button
|
|
197
|
+
onClick={() =>
|
|
198
|
+
setPageIndex((i) => Math.min(dayPages.length - 1, i + 1))
|
|
199
|
+
}
|
|
200
|
+
disabled={safePageIndex === dayPages.length - 1}
|
|
201
|
+
aria-label="Next day"
|
|
202
|
+
style={pageBtnStyle(safePageIndex === dayPages.length - 1)}
|
|
203
|
+
>
|
|
204
|
+
<ChevronRight size={11} />
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
<span>{formatEdge(rangeEndMs, data.multiDay)}</span>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div
|
|
212
|
+
style={{
|
|
213
|
+
position: "relative",
|
|
214
|
+
display: "flex",
|
|
215
|
+
flexDirection: "column",
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
{/* Hatched idle bands spanning all lanes. Positioned as an overlay
|
|
219
|
+
so they line up exactly with the lane strips' x axis (both use
|
|
220
|
+
the shared xOfMs scale). Uses the active page's idle bands in
|
|
221
|
+
day-page mode. */}
|
|
222
|
+
{activeIdleBands.map((band, i) => (
|
|
223
|
+
<IdleBandOverlay key={i} band={band} />
|
|
224
|
+
))}
|
|
225
|
+
{data.tracks.map((t) => {
|
|
226
|
+
const isActive = activeIds.has(t.id);
|
|
227
|
+
return (
|
|
228
|
+
<div
|
|
229
|
+
key={t.id}
|
|
230
|
+
style={{
|
|
231
|
+
display: "flex",
|
|
232
|
+
alignItems: "center",
|
|
233
|
+
gap: 6,
|
|
234
|
+
// The collapsed lanes animate to zero height with opacity fade
|
|
235
|
+
// so the minimap visually slides when its active set changes
|
|
236
|
+
// (e.g. in response to the table's horizontal scroll or the
|
|
237
|
+
// "Show all" toggle).
|
|
238
|
+
maxHeight: isActive ? LANE_HEIGHT : 0,
|
|
239
|
+
opacity: isActive ? 1 : 0,
|
|
240
|
+
marginBottom: isActive ? LANE_GAP : 0,
|
|
241
|
+
overflow: "hidden",
|
|
242
|
+
transition:
|
|
243
|
+
"max-height 280ms ease, opacity 280ms ease, margin-bottom 280ms ease",
|
|
244
|
+
pointerEvents: isActive ? "auto" : "none",
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
<Link
|
|
248
|
+
href={`/sessions/${t.id}`}
|
|
249
|
+
style={{
|
|
250
|
+
width: LABEL_WIDTH - 6,
|
|
251
|
+
fontSize: 10,
|
|
252
|
+
fontFamily: "ui-monospace, monospace",
|
|
253
|
+
color: t.color,
|
|
254
|
+
fontWeight: t.isLead ? 700 : 500,
|
|
255
|
+
overflow: "hidden",
|
|
256
|
+
textOverflow: "ellipsis",
|
|
257
|
+
whiteSpace: "nowrap",
|
|
258
|
+
textDecoration: "none",
|
|
259
|
+
}}
|
|
260
|
+
title={`${t.label} — open session`}
|
|
261
|
+
>
|
|
262
|
+
{t.isLead ? "LEAD" : t.label}
|
|
263
|
+
</Link>
|
|
264
|
+
<div
|
|
265
|
+
style={{
|
|
266
|
+
flex: 1,
|
|
267
|
+
position: "relative",
|
|
268
|
+
height: LANE_HEIGHT,
|
|
269
|
+
background: "var(--af-surface-hover)",
|
|
270
|
+
borderRadius: 3,
|
|
271
|
+
overflow: "hidden",
|
|
272
|
+
cursor: "pointer",
|
|
273
|
+
padding: "3px 2px",
|
|
274
|
+
boxSizing: "border-box",
|
|
275
|
+
}}
|
|
276
|
+
onClick={(e) => onLaneClick(t, e)}
|
|
277
|
+
>
|
|
278
|
+
{t.turns.map((turn) => {
|
|
279
|
+
const clipped = clipTurn(turn);
|
|
280
|
+
if (!clipped) return null;
|
|
281
|
+
const leftFrac = xOf(clipped.startMs);
|
|
282
|
+
const rightFrac = xOf(clipped.endMs);
|
|
283
|
+
const left = leftFrac * 100;
|
|
284
|
+
const width = Math.max(0.4, (rightFrac - leftFrac) * 100);
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
key={turn.id}
|
|
288
|
+
style={{
|
|
289
|
+
position: "absolute",
|
|
290
|
+
left: `calc(${left}% + 1px)`,
|
|
291
|
+
top: 3,
|
|
292
|
+
width: `calc(${width}% - 2px)`,
|
|
293
|
+
minWidth: 4,
|
|
294
|
+
height: LANE_HEIGHT - 10,
|
|
295
|
+
background: t.color,
|
|
296
|
+
opacity: 0.88,
|
|
297
|
+
borderRadius: 3,
|
|
298
|
+
border: `1px solid ${t.color}`,
|
|
299
|
+
boxSizing: "border-box",
|
|
300
|
+
}}
|
|
301
|
+
onMouseEnter={(e) =>
|
|
302
|
+
setHover({
|
|
303
|
+
turn: clipped.original,
|
|
304
|
+
track: t,
|
|
305
|
+
clientX: e.clientX,
|
|
306
|
+
clientY: e.clientY,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
onMouseMove={(e) =>
|
|
310
|
+
setHover((h) =>
|
|
311
|
+
h && h.turn.id === turn.id
|
|
312
|
+
? { ...h, clientX: e.clientX, clientY: e.clientY }
|
|
313
|
+
: h,
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
onMouseLeave={() => setHover(null)}
|
|
317
|
+
/>
|
|
318
|
+
);
|
|
319
|
+
})}
|
|
320
|
+
{t.subagents.map((sa, i) => {
|
|
321
|
+
if (sa.startMs == null) return null;
|
|
322
|
+
const saEnd = sa.startMs + (sa.durationMs ?? 0);
|
|
323
|
+
if (saEnd < rangeStartMs || sa.startMs > rangeEndMs) return null;
|
|
324
|
+
const clampedStart = Math.max(sa.startMs, rangeStartMs);
|
|
325
|
+
const clampedEnd = Math.min(saEnd, rangeEndMs);
|
|
326
|
+
const startFrac = xOf(clampedStart);
|
|
327
|
+
const endFrac = xOf(clampedEnd);
|
|
328
|
+
const left = startFrac * 100;
|
|
329
|
+
const w = Math.max(0.2, (endFrac - startFrac) * 100) || 0.4;
|
|
330
|
+
return (
|
|
331
|
+
<div
|
|
332
|
+
key={i}
|
|
333
|
+
style={{
|
|
334
|
+
position: "absolute",
|
|
335
|
+
left: `${left}%`,
|
|
336
|
+
bottom: 2,
|
|
337
|
+
width: `${w}%`,
|
|
338
|
+
height: 3,
|
|
339
|
+
background: t.color,
|
|
340
|
+
opacity: 0.5,
|
|
341
|
+
borderRadius: 1,
|
|
342
|
+
}}
|
|
343
|
+
title={`subagent: ${sa.agentType}`}
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
})}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
})}
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
{hasOverflow && (
|
|
354
|
+
<div
|
|
355
|
+
style={{
|
|
356
|
+
marginTop: 6,
|
|
357
|
+
paddingLeft: LABEL_WIDTH + 6,
|
|
358
|
+
display: "flex",
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
<button
|
|
362
|
+
onClick={onToggleExpanded}
|
|
363
|
+
style={{
|
|
364
|
+
background: "var(--af-surface-hover)",
|
|
365
|
+
border: "1px solid var(--af-border-subtle)",
|
|
366
|
+
color: "var(--af-text-secondary)",
|
|
367
|
+
fontSize: 10,
|
|
368
|
+
fontFamily: "ui-monospace, monospace",
|
|
369
|
+
padding: "2px 8px",
|
|
370
|
+
borderRadius: 10,
|
|
371
|
+
cursor: "pointer",
|
|
372
|
+
lineHeight: 1.4,
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
{expanded
|
|
376
|
+
? "Show fewer"
|
|
377
|
+
: `+ ${hiddenCount} more agent${hiddenCount === 1 ? "" : "s"} — show all`}
|
|
378
|
+
</button>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{/* Playhead lives in an overlay that starts AFTER the label column so
|
|
383
|
+
the % coordinate matches the strips exactly. */}
|
|
384
|
+
{playheadFrac != null && (
|
|
385
|
+
<div
|
|
386
|
+
style={{
|
|
387
|
+
position: "absolute",
|
|
388
|
+
top: 24,
|
|
389
|
+
bottom: 8,
|
|
390
|
+
left: LABEL_WIDTH + 6,
|
|
391
|
+
right: 8,
|
|
392
|
+
pointerEvents: "none",
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
<div
|
|
396
|
+
style={{
|
|
397
|
+
position: "absolute",
|
|
398
|
+
top: 0,
|
|
399
|
+
bottom: 0,
|
|
400
|
+
left: `${playheadFrac * 100}%`,
|
|
401
|
+
width: 1,
|
|
402
|
+
background: "var(--af-text)",
|
|
403
|
+
opacity: 0.7,
|
|
404
|
+
}}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
|
|
409
|
+
{hover && <HoverCard hover={hover} multiDay={data.multiDay} />}
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function HoverCard({
|
|
415
|
+
hover,
|
|
416
|
+
multiDay,
|
|
417
|
+
}: {
|
|
418
|
+
hover: HoverState;
|
|
419
|
+
multiDay: boolean;
|
|
420
|
+
}) {
|
|
421
|
+
const { turn, track } = hover;
|
|
422
|
+
const summary = turn.megaRow.summary;
|
|
423
|
+
const tmMsg =
|
|
424
|
+
turn.userPrompt && "event" in turn.userPrompt
|
|
425
|
+
? turn.userPrompt.event.teammateMessage
|
|
426
|
+
: undefined;
|
|
427
|
+
const userLabel = tmMsg ? `FROM ${tmMsg.teammateId}` : "HUMAN";
|
|
428
|
+
const userText = tmMsg
|
|
429
|
+
? formatTeammatePreview(tmMsg)
|
|
430
|
+
: turn.userPrompt && turn.userPrompt.kind === "user"
|
|
431
|
+
? (turn.userPrompt.displayPreview ?? turn.userPrompt.event.preview ?? "")
|
|
432
|
+
: "";
|
|
433
|
+
const firstAgent = summary.firstAgentPreview ?? "";
|
|
434
|
+
const finalAgent = summary.finalAgentPreview ?? "";
|
|
435
|
+
const showFinal = finalAgent && finalAgent !== firstAgent;
|
|
436
|
+
const tools = summary.toolNames.slice(0, 4);
|
|
437
|
+
|
|
438
|
+
// Position card near the cursor, biased above-and-right to avoid covering
|
|
439
|
+
// the bar itself. Clamp to viewport via simple offsets.
|
|
440
|
+
const left = Math.min(window.innerWidth - 360, hover.clientX + 12);
|
|
441
|
+
const top = Math.max(8, hover.clientY - 12);
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<div
|
|
445
|
+
style={{
|
|
446
|
+
position: "fixed",
|
|
447
|
+
left,
|
|
448
|
+
top,
|
|
449
|
+
width: 340,
|
|
450
|
+
background: "var(--af-surface)",
|
|
451
|
+
border: "1px solid var(--af-border-subtle)",
|
|
452
|
+
borderLeft: `3px solid ${track.color}`,
|
|
453
|
+
borderRadius: 4,
|
|
454
|
+
padding: "8px 10px",
|
|
455
|
+
fontSize: 10,
|
|
456
|
+
lineHeight: 1.4,
|
|
457
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.25)",
|
|
458
|
+
zIndex: 100,
|
|
459
|
+
pointerEvents: "none",
|
|
460
|
+
}}
|
|
461
|
+
>
|
|
462
|
+
<div
|
|
463
|
+
style={{
|
|
464
|
+
display: "flex",
|
|
465
|
+
justifyContent: "space-between",
|
|
466
|
+
fontSize: 9,
|
|
467
|
+
fontFamily: "ui-monospace, monospace",
|
|
468
|
+
color: track.color,
|
|
469
|
+
fontWeight: 600,
|
|
470
|
+
textTransform: "uppercase",
|
|
471
|
+
letterSpacing: "0.04em",
|
|
472
|
+
marginBottom: 4,
|
|
473
|
+
}}
|
|
474
|
+
>
|
|
475
|
+
<span>{track.isLead ? "LEAD" : track.label}</span>
|
|
476
|
+
<span style={{ color: "var(--af-text-tertiary)", fontWeight: 500 }}>
|
|
477
|
+
{formatEdge(turn.startMs, multiDay)} → {formatEdge(turn.endMs, multiDay)} · {formatGap(turn.durationMs)}
|
|
478
|
+
</span>
|
|
479
|
+
</div>
|
|
480
|
+
<div
|
|
481
|
+
style={{
|
|
482
|
+
fontSize: 9,
|
|
483
|
+
color: "var(--af-text-tertiary)",
|
|
484
|
+
marginBottom: 6,
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
{summary.agentMessages} msg · {summary.toolCalls} tools
|
|
488
|
+
{summary.errors > 0 ? ` · ${summary.errors} err` : ""}
|
|
489
|
+
</div>
|
|
490
|
+
{userText && (
|
|
491
|
+
<div style={{ marginBottom: 5 }}>
|
|
492
|
+
<div
|
|
493
|
+
style={{
|
|
494
|
+
fontSize: 8,
|
|
495
|
+
color: tmMsg ? "var(--af-text-tertiary)" : track.color,
|
|
496
|
+
fontWeight: 600,
|
|
497
|
+
textTransform: "uppercase",
|
|
498
|
+
letterSpacing: "0.04em",
|
|
499
|
+
marginBottom: 1,
|
|
500
|
+
fontStyle: tmMsg ? "italic" : undefined,
|
|
501
|
+
}}
|
|
502
|
+
>
|
|
503
|
+
{userLabel}
|
|
504
|
+
</div>
|
|
505
|
+
<div
|
|
506
|
+
style={{
|
|
507
|
+
color: tmMsg ? "var(--af-text-tertiary)" : "var(--af-text)",
|
|
508
|
+
display: "-webkit-box",
|
|
509
|
+
WebkitLineClamp: 2,
|
|
510
|
+
WebkitBoxOrient: "vertical",
|
|
511
|
+
overflow: "hidden",
|
|
512
|
+
}}
|
|
513
|
+
>
|
|
514
|
+
{userText}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
{firstAgent && (
|
|
519
|
+
<div style={{ marginBottom: 5 }}>
|
|
520
|
+
<div
|
|
521
|
+
style={{
|
|
522
|
+
fontSize: 8,
|
|
523
|
+
color: track.color,
|
|
524
|
+
fontWeight: 600,
|
|
525
|
+
textTransform: "uppercase",
|
|
526
|
+
letterSpacing: "0.04em",
|
|
527
|
+
marginBottom: 1,
|
|
528
|
+
}}
|
|
529
|
+
>
|
|
530
|
+
AGENT
|
|
531
|
+
</div>
|
|
532
|
+
<div
|
|
533
|
+
style={{
|
|
534
|
+
color: "var(--af-text)",
|
|
535
|
+
display: "-webkit-box",
|
|
536
|
+
WebkitLineClamp: 3,
|
|
537
|
+
WebkitBoxOrient: "vertical",
|
|
538
|
+
overflow: "hidden",
|
|
539
|
+
}}
|
|
540
|
+
>
|
|
541
|
+
{firstAgent}
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
)}
|
|
545
|
+
{tools.length > 0 && (
|
|
546
|
+
<div
|
|
547
|
+
style={{
|
|
548
|
+
display: "flex",
|
|
549
|
+
gap: 3,
|
|
550
|
+
flexWrap: "wrap",
|
|
551
|
+
marginBottom: showFinal ? 5 : 0,
|
|
552
|
+
}}
|
|
553
|
+
>
|
|
554
|
+
{tools.map((t) => (
|
|
555
|
+
<span
|
|
556
|
+
key={t.name}
|
|
557
|
+
style={{
|
|
558
|
+
fontSize: 8,
|
|
559
|
+
padding: "1px 5px",
|
|
560
|
+
background: "var(--af-surface-hover)",
|
|
561
|
+
borderRadius: 2,
|
|
562
|
+
color: "var(--af-text-tertiary)",
|
|
563
|
+
}}
|
|
564
|
+
>
|
|
565
|
+
{t.name}
|
|
566
|
+
{t.count > 1 ? ` ×${t.count}` : ""}
|
|
567
|
+
</span>
|
|
568
|
+
))}
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
{showFinal && (
|
|
572
|
+
<div>
|
|
573
|
+
<div
|
|
574
|
+
style={{
|
|
575
|
+
fontSize: 8,
|
|
576
|
+
color: track.color,
|
|
577
|
+
fontWeight: 600,
|
|
578
|
+
textTransform: "uppercase",
|
|
579
|
+
letterSpacing: "0.04em",
|
|
580
|
+
marginBottom: 1,
|
|
581
|
+
}}
|
|
582
|
+
>
|
|
583
|
+
RESULT
|
|
584
|
+
</div>
|
|
585
|
+
<div
|
|
586
|
+
style={{
|
|
587
|
+
color: "var(--af-text)",
|
|
588
|
+
display: "-webkit-box",
|
|
589
|
+
WebkitLineClamp: 2,
|
|
590
|
+
WebkitBoxOrient: "vertical",
|
|
591
|
+
overflow: "hidden",
|
|
592
|
+
}}
|
|
593
|
+
>
|
|
594
|
+
{finalAgent}
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
)}
|
|
598
|
+
</div>
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function pageBtnStyle(disabled: boolean): React.CSSProperties {
|
|
603
|
+
return {
|
|
604
|
+
background: "transparent",
|
|
605
|
+
border: "1px solid var(--af-border-subtle)",
|
|
606
|
+
color: disabled ? "var(--af-text-tertiary)" : "var(--af-text-secondary)",
|
|
607
|
+
width: 18,
|
|
608
|
+
height: 18,
|
|
609
|
+
borderRadius: 3,
|
|
610
|
+
display: "flex",
|
|
611
|
+
alignItems: "center",
|
|
612
|
+
justifyContent: "center",
|
|
613
|
+
cursor: disabled ? "default" : "pointer",
|
|
614
|
+
opacity: disabled ? 0.4 : 1,
|
|
615
|
+
padding: 0,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function formatEdge(ms: number, multiDay: boolean): string {
|
|
620
|
+
const d = new Date(ms);
|
|
621
|
+
if (!multiDay) {
|
|
622
|
+
return d.toLocaleTimeString([], {
|
|
623
|
+
hour: "2-digit",
|
|
624
|
+
minute: "2-digit",
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const date = d.toLocaleDateString([], { month: "short", day: "numeric" });
|
|
628
|
+
const time = d.toLocaleTimeString([], {
|
|
629
|
+
hour: "2-digit",
|
|
630
|
+
minute: "2-digit",
|
|
631
|
+
});
|
|
632
|
+
return `${date} ${time}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function IdleBandOverlay({ band }: { band: MinimapIdleBand }) {
|
|
636
|
+
const [hovered, setHovered] = useState(false);
|
|
637
|
+
const left = band.xFracStart * 100;
|
|
638
|
+
const width = Math.max(0.5, (band.xFracEnd - band.xFracStart) * 100);
|
|
639
|
+
return (
|
|
640
|
+
<div
|
|
641
|
+
onMouseEnter={() => setHovered(true)}
|
|
642
|
+
onMouseLeave={() => setHovered(false)}
|
|
643
|
+
style={{
|
|
644
|
+
position: "absolute",
|
|
645
|
+
top: 0,
|
|
646
|
+
bottom: 0,
|
|
647
|
+
left: `calc(${LABEL_WIDTH}px + ${left}% * (100% - ${LABEL_WIDTH}px) / 100%)`,
|
|
648
|
+
width: `calc(${width}% * (100% - ${LABEL_WIDTH}px) / 100%)`,
|
|
649
|
+
background:
|
|
650
|
+
"repeating-linear-gradient(135deg, transparent 0, transparent 4px, var(--af-border-subtle) 4px, var(--af-border-subtle) 6px)",
|
|
651
|
+
zIndex: 1,
|
|
652
|
+
cursor: "help",
|
|
653
|
+
}}
|
|
654
|
+
>
|
|
655
|
+
{hovered && (
|
|
656
|
+
<div
|
|
657
|
+
style={{
|
|
658
|
+
position: "absolute",
|
|
659
|
+
left: "50%",
|
|
660
|
+
bottom: "calc(100% + 6px)",
|
|
661
|
+
transform: "translateX(-50%)",
|
|
662
|
+
background: "#0F172A",
|
|
663
|
+
color: "#F1F5F9",
|
|
664
|
+
borderRadius: 8,
|
|
665
|
+
padding: "6px 10px",
|
|
666
|
+
fontSize: 11,
|
|
667
|
+
pointerEvents: "none",
|
|
668
|
+
boxShadow: "0 4px 16px rgba(15,23,42,0.22)",
|
|
669
|
+
zIndex: 20,
|
|
670
|
+
whiteSpace: "nowrap",
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
<div style={{ fontWeight: 600, marginBottom: 1 }}>Idle</div>
|
|
674
|
+
<div style={{ opacity: 0.78, fontFamily: "ui-monospace, monospace", fontSize: 10 }}>
|
|
675
|
+
{band.label}
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
)}
|
|
679
|
+
</div>
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|