fleetlens 0.13.1 → 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 +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.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.nft.json +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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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/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/_0xu2pvc._.js +1 -1
- 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_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/middleware-build-manifest.js +3 -3
- 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/{0vz4~a~~hq399.js → 02xxpexzk3rf-.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/app/parallelism/gantt-chart.tsx +8 -1
- package/app/apps/web/app/sessions/[id]/session-view/minimap.tsx +1587 -0
- package/app/apps/web/app/sessions/[id]/session-view/workflows-panel.tsx +944 -0
- package/app/apps/web/app/sessions/[id]/session-view.tsx +909 -3223
- package/app/apps/web/components/tail-mode.tsx +7 -1
- package/app/apps/web/lib/changelog-data.generated.ts +1 -1
- package/app/apps/web/lib/format.ts +17 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +4 -4
- package/dist/index.js +11 -11
- package/package.json +1 -1
- package/app/apps/web/.next/static/chunks/02bv_ejzhhzvp.js +0 -10
- package/app/apps/web/.next/static/chunks/0q8dg4txlzno0.js +0 -1
- /package/app/apps/web/.next/static/{VEfemGVAQ0xCHjj15orZU → bZs5UyzsYBqC6MFY5n-M_}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{VEfemGVAQ0xCHjj15orZU → bZs5UyzsYBqC6MFY5n-M_}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{VEfemGVAQ0xCHjj15orZU → bZs5UyzsYBqC6MFY5n-M_}/_ssgManifest.js +0 -0
|
@@ -15,16 +15,13 @@ import {
|
|
|
15
15
|
GitPullRequest,
|
|
16
16
|
Workflow as WorkflowIcon,
|
|
17
17
|
Wrench,
|
|
18
|
-
X,
|
|
19
18
|
} from "lucide-react";
|
|
20
19
|
import ReactMarkdown from "react-markdown";
|
|
21
20
|
import remarkGfm from "remark-gfm";
|
|
22
21
|
import type {
|
|
23
22
|
SessionDetail,
|
|
24
23
|
SessionEvent,
|
|
25
|
-
SubagentRun,
|
|
26
24
|
WorkflowRun,
|
|
27
|
-
PrMarker,
|
|
28
25
|
} from "@claude-lens/parser";
|
|
29
26
|
import type { Entry } from "@claude-lens/entries";
|
|
30
27
|
import { OutcomePill } from "@/components/outcome-pill";
|
|
@@ -41,9 +38,8 @@ import {
|
|
|
41
38
|
import {
|
|
42
39
|
rowPrimaryIndex,
|
|
43
40
|
type DisplayRow,
|
|
44
|
-
type MinimapSeg,
|
|
45
41
|
} from "./session-view/types";
|
|
46
|
-
import {
|
|
42
|
+
import { Minimap } from "./session-view/minimap";
|
|
47
43
|
import {
|
|
48
44
|
formatDayKey,
|
|
49
45
|
flattenMegaRows,
|
|
@@ -64,13 +60,11 @@ import {
|
|
|
64
60
|
} from "./session-view/header-stats";
|
|
65
61
|
import { Drawer } from "./session-view/drawer";
|
|
66
62
|
import {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
TokenLine,
|
|
71
|
-
} from "./session-view/workflows-shared";
|
|
63
|
+
WorkflowsPanel,
|
|
64
|
+
WorkflowAgentDrawer,
|
|
65
|
+
} from "./session-view/workflows-panel";
|
|
72
66
|
import { SubagentDrawer } from "./session-view/subagent-drawer";
|
|
73
|
-
import { estimateCost, formatCost, formatGap, formatOffset, formatRelative, formatTokens, shortId } from "@/lib/format";
|
|
67
|
+
import { estimateCost, formatCost, formatGap, formatOffset, formatRelative, formatResumeStamp, formatTokens, shortId } from "@/lib/format";
|
|
74
68
|
import { LiveBadge } from "@/components/live-badge";
|
|
75
69
|
import { AskButton, AskDrawer } from "@/components/ask";
|
|
76
70
|
import { TailMode } from "@/components/tail-mode";
|
|
@@ -447,17 +441,24 @@ export function SessionView({
|
|
|
447
441
|
* between-turn idle because no agent activity occurred between.)
|
|
448
442
|
*
|
|
449
443
|
* Side effect: the leading warm-up gap before the first agent response
|
|
450
|
-
* drops out unless it crosses
|
|
444
|
+
* drops out unless it crosses the idle threshold — which the first 6s in a
|
|
451
445
|
* typical Codex session does not. */
|
|
452
446
|
const rawIdleBands = useMemo(() => {
|
|
453
447
|
// Anchor on ANY timestamped event so agent-thinking, meta, and other
|
|
454
448
|
// intra-turn signals prove the parent is doing work. Only what we
|
|
455
449
|
// need to distinguish is "user" (turn boundary) vs everything else.
|
|
456
450
|
const isUserRole = (role: string) => role === "user";
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
451
|
+
// Two thresholds, because an in-turn pause and a between-turn pause mean
|
|
452
|
+
// different things. MID-TURN the agent is actively working — a long Bash,
|
|
453
|
+
// a build, model thinking between tool calls — so a 30s–2min gap is NOT
|
|
454
|
+
// idle; only a genuine multi-minute stall is. We match the parser's
|
|
455
|
+
// active-segment gap (3 min) there. BETWEEN turns the user has actually
|
|
456
|
+
// stepped away, so a lower bar is right. The old single 30s threshold drew
|
|
457
|
+
// every tool-call pause as a "Session idle" sliver, shredding a long turn
|
|
458
|
+
// into dozens of fragments and clipping the turn's minimap block at the
|
|
459
|
+
// first sliver (so the block stopped short of a workflow that ran inside it).
|
|
460
|
+
const IN_TURN_IDLE_MS = 180_000;
|
|
461
|
+
const BETWEEN_TURN_IDLE_MS = 120_000;
|
|
461
462
|
const anchors: { ms: number; role: string }[] = [];
|
|
462
463
|
for (const e of events) {
|
|
463
464
|
if (!e.timestamp) continue;
|
|
@@ -467,7 +468,14 @@ export function SessionView({
|
|
|
467
468
|
}
|
|
468
469
|
if (anchors.length < 2) return [];
|
|
469
470
|
anchors.sort((a, b) => a.ms - b.ms);
|
|
470
|
-
|
|
471
|
+
// anchors[0].ms (sorted ascending) is the global-min timestamp — exactly the
|
|
472
|
+
// origin the parser measures tOffsetMs from (tOffsetMs = ms - min(all ts)), so
|
|
473
|
+
// band offsets (anchor.ms - sessionStartMs) land in the SAME space as the day
|
|
474
|
+
// window. The old `- firstInFile.tOffsetMs` term subtracted a DIFFERENT event's
|
|
475
|
+
// offset when the JSONL starts out of chronological order, skewing every band
|
|
476
|
+
// by their gap and leaking the between-day idle past the minimap's strict
|
|
477
|
+
// day-window edge filter (a ~40h "Session idle" band dominating the day view).
|
|
478
|
+
const sessionStartMs = anchors[0]!.ms;
|
|
471
479
|
|
|
472
480
|
// "Busy" spans = delegated agent work that means the parent isn't idle:
|
|
473
481
|
// subagent runs AND dynamic-workflow execution. Both are carved out of the
|
|
@@ -528,7 +536,7 @@ export function SessionView({
|
|
|
528
536
|
// "Awaiting first response" — true after a user message, false once an
|
|
529
537
|
// agent or tool-call anchor proves the model started replying.
|
|
530
538
|
// Thinking and meta events don't clear it, so a user→thinking→agent
|
|
531
|
-
// sequence where thinking→agent crosses
|
|
539
|
+
// sequence where thinking→agent crosses the idle threshold still reads as
|
|
532
540
|
// response latency rather than idle.
|
|
533
541
|
let awaitingFirstResponse = false;
|
|
534
542
|
if (anchors[0]) {
|
|
@@ -545,20 +553,19 @@ export function SessionView({
|
|
|
545
553
|
if (isUserRole(curRole)) awaitingFirstResponse = true;
|
|
546
554
|
else if (curRole === "agent" || curRole === "tool-call") awaitingFirstResponse = false;
|
|
547
555
|
|
|
548
|
-
|
|
556
|
+
const betweenTurn = isUserRole(curRole);
|
|
557
|
+
if (rawGap <= (betweenTurn ? BETWEEN_TURN_IDLE_MS : IN_TURN_IDLE_MS)) continue;
|
|
549
558
|
if (skipAsLatency) continue;
|
|
550
559
|
|
|
551
560
|
const gStart = anchors[i - 1]!.ms - sessionStartMs;
|
|
552
561
|
const gEnd = anchors[i]!.ms - sessionStartMs;
|
|
553
|
-
const betweenTurn = isUserRole(curRole);
|
|
554
562
|
|
|
555
563
|
if (!betweenTurn) {
|
|
556
564
|
// In-turn gap: the parent is mid-turn. If any subagent run overlaps,
|
|
557
565
|
// the parent is waiting on delegated work — not idle. Drop the
|
|
558
566
|
// whole gap rather than carving out startup/teardown slivers that
|
|
559
567
|
// would clutter the minimap with thin stripes. Gaps with no
|
|
560
|
-
// subagent overlap (
|
|
561
|
-
// calls) still emit as a single band.
|
|
568
|
+
// subagent overlap (a genuine multi-minute stall) emit as one band.
|
|
562
569
|
if (overlapsSubagent(gStart, gEnd)) continue;
|
|
563
570
|
bands.push({ start: gStart, end: gEnd, durationMs: gEnd - gStart });
|
|
564
571
|
continue;
|
|
@@ -569,11 +576,13 @@ export function SessionView({
|
|
|
569
576
|
// Each surviving slice becomes its own band.
|
|
570
577
|
for (const slice of carveOutSubagents(gStart, gEnd)) {
|
|
571
578
|
const dur = slice.end - slice.start;
|
|
572
|
-
if (dur >
|
|
579
|
+
if (dur > BETWEEN_TURN_IDLE_MS) {
|
|
573
580
|
bands.push({ start: slice.start, end: slice.end, durationMs: dur });
|
|
574
581
|
}
|
|
575
582
|
}
|
|
576
583
|
}
|
|
584
|
+
|
|
585
|
+
bands.sort((a, b) => a.start - b.start);
|
|
577
586
|
return bands;
|
|
578
587
|
}, [events, session.subagents, session.workflows]);
|
|
579
588
|
|
|
@@ -639,11 +648,23 @@ export function SessionView({
|
|
|
639
648
|
// mismatch; the effect re-pins it when the component is reused across
|
|
640
649
|
// /sessions/[id] navigations. "all" shows the full session.
|
|
641
650
|
const didInitialDayJump = useRef(false);
|
|
651
|
+
// A `?day=YYYY-MM-DD` query param (set by links from the Day / Concurrency
|
|
652
|
+
// views) pins the opening day to that day instead of the most recent one.
|
|
653
|
+
// Validated against the session's real day buckets and ignored if it doesn't
|
|
654
|
+
// match. Read in effects (not the lazy initializer) so SSR and the first
|
|
655
|
+
// client render agree — the param-driven selection happens client-side.
|
|
656
|
+
const wantedDayFromUrl = (days: typeof sessionDays): string | null => {
|
|
657
|
+
if (typeof window === "undefined") return null;
|
|
658
|
+
const w = new URLSearchParams(window.location.search).get("day");
|
|
659
|
+
return w && days.some((d) => d.key === w) ? w : null;
|
|
660
|
+
};
|
|
661
|
+
const lastOrAll = (days: typeof sessionDays) =>
|
|
662
|
+
days.length ? days[days.length - 1]!.key : "all";
|
|
642
663
|
const [selectedDayKey, setSelectedDayKey] = useState<string>(
|
|
643
|
-
() => (sessionDays
|
|
664
|
+
() => lastOrAll(sessionDays),
|
|
644
665
|
);
|
|
645
666
|
useEffect(() => {
|
|
646
|
-
setSelectedDayKey(sessionDays
|
|
667
|
+
setSelectedDayKey(wantedDayFromUrl(sessionDays) ?? lastOrAll(sessionDays));
|
|
647
668
|
didInitialDayJump.current = false;
|
|
648
669
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
649
670
|
}, [session.sessionId]);
|
|
@@ -659,8 +680,10 @@ export function SessionView({
|
|
|
659
680
|
selectedDayKeyRef.current = selectedDayKey;
|
|
660
681
|
|
|
661
682
|
// Initial jump: on first paint of a multi-day session, snap straight to the
|
|
662
|
-
//
|
|
663
|
-
//
|
|
683
|
+
// requested day (?day=) or, by default, the most recent one (the lazy
|
|
684
|
+
// selectedDayKey is the last day; the re-pin effect above re-points it to the
|
|
685
|
+
// requested day client-side, so the minimap matches). Routed through gotoDay
|
|
686
|
+
// rather than scrolling to the day's
|
|
664
687
|
// strip directly: in "turns" mode the most recent day can be absorbed into a
|
|
665
688
|
// turn anchored on an earlier day and so have NO inline strip — gotoDay finds
|
|
666
689
|
// the day's first event, expands its turn, and falls back to the row ref, so
|
|
@@ -676,12 +699,21 @@ export function SessionView({
|
|
|
676
699
|
useEffect(() => {
|
|
677
700
|
if (tab !== "transcript" || didInitialDayJump.current) return;
|
|
678
701
|
const days = sessionDaysRef.current;
|
|
679
|
-
if (days.length <= 1
|
|
702
|
+
if (days.length <= 1) {
|
|
703
|
+
didInitialDayJump.current = true;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const wanted = wantedDayFromUrl(days);
|
|
707
|
+
// Live tail: let TailMode land on the live tail unless an explicit EARLIER
|
|
708
|
+
// day was requested (?day=). A request for the live/last day needs no jump
|
|
709
|
+
// — the tail is already in it — and jumping would fight TailMode's mount
|
|
710
|
+
// scroll and flip live-follow off.
|
|
711
|
+
if (isSessionLive && (!wanted || wanted === days[days.length - 1]!.key)) {
|
|
680
712
|
didInitialDayJump.current = true;
|
|
681
713
|
return;
|
|
682
714
|
}
|
|
683
715
|
didInitialDayJump.current = true;
|
|
684
|
-
gotoDay(days[days.length - 1]!.key);
|
|
716
|
+
gotoDay(wanted ?? days[days.length - 1]!.key);
|
|
685
717
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
686
718
|
}, [tab, sessionDays]);
|
|
687
719
|
|
|
@@ -818,17 +850,40 @@ export function SessionView({
|
|
|
818
850
|
if (key === "all") return;
|
|
819
851
|
const d = sessionDays.find((x) => x.key === key);
|
|
820
852
|
if (!d) return;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
//
|
|
824
|
-
//
|
|
853
|
+
// Target the day's first PRESENTATION ROW, not its first raw event. A day can
|
|
854
|
+
// open on a meta/system/sidechain event that has no row, whose index then
|
|
855
|
+
// lands in neither turnByRowIndex nor rowRefs — so the jump silently no-ops
|
|
856
|
+
// (the bug where a multi-day session opened stuck at the very top). A row's
|
|
857
|
+
// primary index is the coordinate both that map and the scroll refs use.
|
|
858
|
+
let firstRow = allRows.find(
|
|
859
|
+
(r) => r.tOffsetMs !== undefined && r.tOffsetMs >= d.startMs,
|
|
860
|
+
);
|
|
861
|
+
// A day can consist entirely of background-agent / workflow events, which
|
|
862
|
+
// render in the Workflows tab — not the transcript — so NO presentation row
|
|
863
|
+
// falls inside it (sessionDays is built from all events, allRows only from
|
|
864
|
+
// the visible transcript). Fall back to the last rendered row so the jump
|
|
865
|
+
// lands on the closest content instead of silently no-oping (the bug where
|
|
866
|
+
// the view stayed pinned at the very top).
|
|
867
|
+
if (!firstRow) {
|
|
868
|
+
for (let i = allRows.length - 1; i >= 0; i--) {
|
|
869
|
+
if (allRows[i]!.tOffsetMs !== undefined) {
|
|
870
|
+
firstRow = allRows[i];
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (!firstRow) return;
|
|
876
|
+
const targetIndex = rowPrimaryIndex(firstRow);
|
|
877
|
+
// In turns mode that row may be inside a collapsed turn (often one anchored
|
|
878
|
+
// on the PREVIOUS day) — expand it so the row exists in the DOM before the
|
|
879
|
+
// scroll effect runs.
|
|
825
880
|
if (filter === "turns") {
|
|
826
|
-
const turnIdx = turnByRowIndex.get(
|
|
881
|
+
const turnIdx = turnByRowIndex.get(targetIndex);
|
|
827
882
|
if (turnIdx !== undefined && !expandedTurns.has(turnIdx)) {
|
|
828
883
|
setExpandedTurns((prev) => new Set(prev).add(turnIdx));
|
|
829
884
|
}
|
|
830
885
|
}
|
|
831
|
-
setDayScrollTarget((prev) => ({ index:
|
|
886
|
+
setDayScrollTarget((prev) => ({ index: targetIndex, key, block: "start", n: (prev?.n ?? 0) + 1 }));
|
|
832
887
|
}
|
|
833
888
|
|
|
834
889
|
/** Cross-day hop used by the inline day-boundary buttons. Selecting a day can
|
|
@@ -1298,7 +1353,6 @@ export function SessionView({
|
|
|
1298
1353
|
setSelectedWorkflowId(runId);
|
|
1299
1354
|
setTab("workflows");
|
|
1300
1355
|
}}
|
|
1301
|
-
collapsed={collapsed}
|
|
1302
1356
|
prMarkers={prMarkers}
|
|
1303
1357
|
coldResumeMarkers={coldResumeMarkers}
|
|
1304
1358
|
rawIdleBands={rawIdleBands}
|
|
@@ -1565,2157 +1619,675 @@ export function SessionView({
|
|
|
1565
1619
|
/* Mini-map */
|
|
1566
1620
|
/* ------------------------------------------------------------------ */
|
|
1567
1621
|
|
|
1622
|
+
// ROW_IMPORTANCE, Minimap, MinimapHoverCard moved to ./session-view/minimap.tsx
|
|
1623
|
+
// MinimapSeg moved to ./session-view/types.ts
|
|
1624
|
+
// subagentColor, workflowColor moved to ./session-view/colors.ts
|
|
1625
|
+
|
|
1568
1626
|
/* ------------------------------------------------------------------ */
|
|
1569
|
-
/*
|
|
1570
|
-
/* */
|
|
1571
|
-
/* Design: */
|
|
1572
|
-
/* - Renders ONE segment per presentation row (not per raw event) */
|
|
1573
|
-
/* so density matches what the user sees in the transcript. */
|
|
1574
|
-
/* - Variable heights by importance: User/Agent/Error/Interrupt */
|
|
1575
|
-
/* full height; Tool/Model reduced; idle = full w/ stripes. */
|
|
1576
|
-
/* - Minimum block width 5px so single events never disappear. */
|
|
1577
|
-
/* - Scroll playhead: thin vertical line tracks the current transcript*/
|
|
1578
|
-
/* scroll position inside the <main> container. */
|
|
1579
|
-
/* - Time axis ticks below the bar, auto-scaled by total duration. */
|
|
1580
|
-
/* - Rich hover card (role pill + preview) instead of bare SVG title. */
|
|
1627
|
+
/* Transcript list + rows */
|
|
1581
1628
|
/* ------------------------------------------------------------------ */
|
|
1582
1629
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
/** All rows render at full bar height — distinction comes from color/pattern
|
|
1586
|
-
* alone, matching Claude Managed Agents' unified-height timeline. */
|
|
1587
|
-
const ROW_IMPORTANCE: Record<PresentationRowKind, number> = {
|
|
1588
|
-
user: 1.0,
|
|
1589
|
-
agent: 1.0,
|
|
1590
|
-
interrupt: 1.0,
|
|
1591
|
-
error: 1.0,
|
|
1592
|
-
"tool-group": 1.0,
|
|
1593
|
-
model: 1.0,
|
|
1594
|
-
"task-notification": 1.0,
|
|
1595
|
-
};
|
|
1596
|
-
|
|
1597
|
-
function Minimap({
|
|
1630
|
+
function TranscriptList({
|
|
1598
1631
|
displayRows,
|
|
1599
|
-
|
|
1600
|
-
dayWindow,
|
|
1632
|
+
rowRefs,
|
|
1601
1633
|
selectedIndex,
|
|
1602
1634
|
onSelect,
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
collapsed,
|
|
1608
|
-
selectedSubagentId,
|
|
1609
|
-
onSelectSubagent,
|
|
1610
|
-
prMarkers,
|
|
1611
|
-
coldResumeMarkers,
|
|
1612
|
-
rawIdleBands,
|
|
1635
|
+
onToggleTurn,
|
|
1636
|
+
stickyOffset,
|
|
1637
|
+
isSessionLive,
|
|
1638
|
+
team,
|
|
1613
1639
|
model,
|
|
1614
|
-
|
|
1640
|
+
rawIdleBands,
|
|
1641
|
+
sessionDays,
|
|
1642
|
+
entryByDay,
|
|
1643
|
+
dayStripRefs,
|
|
1644
|
+
onJumpAcrossDay,
|
|
1615
1645
|
}: {
|
|
1616
1646
|
displayRows: DisplayRow[];
|
|
1617
|
-
|
|
1618
|
-
/** When set, scope the timeline to this offset window (one local day).
|
|
1619
|
-
* null renders the full session. */
|
|
1620
|
-
dayWindow?: { startMs: number; endMs: number } | null;
|
|
1647
|
+
rowRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
|
|
1621
1648
|
selectedIndex: number | null;
|
|
1622
|
-
onSelect: (i: number) => void;
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
collapsed?: boolean;
|
|
1628
|
-
selectedSubagentId?: string | null;
|
|
1629
|
-
onSelectSubagent?: (id: string | null) => void;
|
|
1630
|
-
prMarkers?: PrMarker[];
|
|
1631
|
-
coldResumeMarkers?: {
|
|
1632
|
-
tOffsetMs: number;
|
|
1633
|
-
info: NonNullable<SessionEvent["coldResume"]>;
|
|
1634
|
-
}[];
|
|
1635
|
-
/** Idle bands derived directly from raw event timestamps. Used in place
|
|
1636
|
-
* of the legacy "gap before user row" heuristic so Codex sessions
|
|
1637
|
-
* (which spend most of their time in agent reasoning, not user input)
|
|
1638
|
-
* surface their dead-air on the minimap. */
|
|
1639
|
-
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
1649
|
+
onSelect: (i: number | null) => void;
|
|
1650
|
+
onToggleTurn: (firstPrimaryIndex: number) => void;
|
|
1651
|
+
stickyOffset: number;
|
|
1652
|
+
isSessionLive?: boolean;
|
|
1653
|
+
team?: (TimelineData & { teamName: string }) | null;
|
|
1640
1654
|
model?: string;
|
|
1641
|
-
/**
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1644
|
-
|
|
1655
|
+
/** Anchor-to-anchor idle bands. The body emits IdleDivider before any
|
|
1656
|
+
* row whose start matches a band's end, so Codex sessions get the same
|
|
1657
|
+
* "Session idle X minutes" markers Claude has always had. */
|
|
1658
|
+
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
1659
|
+
/** Day buckets (sorted by startMs) + per-day digest cards. The body drops a
|
|
1660
|
+
* card at each day's first row so per-day perception sits inline with that
|
|
1661
|
+
* day's work instead of stacked at the top. */
|
|
1662
|
+
sessionDays: { key: string; startMs: number; endMs: number; count: number }[];
|
|
1663
|
+
entryByDay: Map<string, Entry>;
|
|
1664
|
+
dayStripRefs: React.MutableRefObject<Record<string, HTMLDivElement | null>>;
|
|
1665
|
+
/** Hop to an adjacent day from an inline day-boundary button.
|
|
1666
|
+
* (selectKey, scrollKey, block, fallbackRowIndex). */
|
|
1667
|
+
onJumpAcrossDay: (
|
|
1668
|
+
selectKey: string,
|
|
1669
|
+
scrollKey: string,
|
|
1670
|
+
block: "start" | "end",
|
|
1671
|
+
fallbackIndex: number,
|
|
1672
|
+
) => void;
|
|
1645
1673
|
}) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
* workflow tier reads as distinct. */
|
|
1655
|
-
const WF_LANE_H = 13;
|
|
1656
|
-
const BAR_TOP = 3;
|
|
1657
|
-
const BAR_BOT = MAIN_H - 3;
|
|
1658
|
-
const BAR_H = BAR_BOT - BAR_TOP;
|
|
1659
|
-
/** Gap subtracted from each segment's width so blocks never touch. */
|
|
1660
|
-
const GAP = 3;
|
|
1661
|
-
const MIN_BLOCK = 6;
|
|
1662
|
-
/** Minimum displayed segment width in SVG units. Every segment gets at
|
|
1663
|
-
* least this much space regardless of time duration, so tool-group
|
|
1664
|
-
* blocks inside a 24-minute session are still visible + clickable. */
|
|
1665
|
-
const MIN_DISPLAY_WIDTH = 12;
|
|
1666
|
-
/** Error/interrupt rendered as thin vertical bars capped at this width. */
|
|
1667
|
-
const THIN_BAR_MAX = 5;
|
|
1668
|
-
|
|
1669
|
-
const [hover, setHover] = useState<{
|
|
1670
|
-
clientX: number;
|
|
1671
|
-
row?: PresentationRow;
|
|
1672
|
-
turn?: TurnMegaRow;
|
|
1673
|
-
idleMs?: number;
|
|
1674
|
-
subagent?: SubagentRun;
|
|
1675
|
-
workflow?: WorkflowRun;
|
|
1676
|
-
pr?: PrMarker;
|
|
1677
|
-
cold?: {
|
|
1678
|
-
tOffsetMs: number;
|
|
1679
|
-
info: NonNullable<SessionEvent["coldResume"]>;
|
|
1680
|
-
};
|
|
1681
|
-
} | null>(null);
|
|
1682
|
-
const [playheadMs, setPlayheadMs] = useState<number | null>(null);
|
|
1683
|
-
// Read inside the observer so the latest callback fires without re-subscribing
|
|
1684
|
-
// the IntersectionObserver (its effect deps stay [displayRows.length, headerOffset]).
|
|
1685
|
-
const onPlayheadChangeRef = useRef(onPlayheadChange);
|
|
1686
|
-
onPlayheadChangeRef.current = onPlayheadChange;
|
|
1687
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
1688
|
-
|
|
1689
|
-
/* Playhead — track which transcript row is at the top of the viewport.
|
|
1690
|
-
*
|
|
1691
|
-
* The old implementation ran `querySelectorAll("[data-sl-row-index]")`
|
|
1692
|
-
* on every scroll event. For a session with ~2000 rows that's thousands
|
|
1693
|
-
* of DOM queries per second during scroll — enough to pin a CPU core
|
|
1694
|
-
* and make Chrome ask to kill the tab. IntersectionObserver is event-
|
|
1695
|
-
* driven: the browser only notifies us when rows enter or leave the region
|
|
1696
|
-
* below the sticky header, so the cost scales with *changes* instead of
|
|
1697
|
-
* with scroll rate × row count.
|
|
1698
|
-
*/
|
|
1699
|
-
useEffect(() => {
|
|
1700
|
-
const main = containerRef.current?.closest("main") as HTMLElement | null;
|
|
1701
|
-
if (!main) return;
|
|
1702
|
-
|
|
1703
|
-
// Crop the observation region by the sticky header at the top and keep the
|
|
1704
|
-
// whole viewport below it. Every row visible below the header intersects
|
|
1705
|
-
// this region; the topmost (smallest offsetTop) is the row sitting at the
|
|
1706
|
-
// header line — the playhead. Using the full area (not a thin band) means
|
|
1707
|
-
// the playhead never blanks out in the gaps between sparse rows, so the
|
|
1708
|
-
// scroll→timeline highlight — and the day-follow that rides on it — stays
|
|
1709
|
-
// continuous. Still event-driven: a row only fires when it enters or
|
|
1710
|
-
// leaves, so cost scales with scroll distance, not row count.
|
|
1711
|
-
const rootMargin = `-${headerOffset}px 0px 0px 0px`;
|
|
1712
|
-
|
|
1713
|
-
// Rows currently visible below the header. The topmost is the playhead.
|
|
1714
|
-
const visibleRows = new Set<HTMLElement>();
|
|
1715
|
-
|
|
1716
|
-
const publishTopmost = () => {
|
|
1717
|
-
let value: number | null = null;
|
|
1718
|
-
if (visibleRows.size > 0) {
|
|
1719
|
-
let topmost: HTMLElement | null = null;
|
|
1720
|
-
let topmostOff = Infinity;
|
|
1721
|
-
for (const el of visibleRows) {
|
|
1722
|
-
const t = el.offsetTop;
|
|
1723
|
-
if (t < topmostOff) {
|
|
1724
|
-
topmostOff = t;
|
|
1725
|
-
topmost = el;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
if (topmost) {
|
|
1729
|
-
const tOff = Number(topmost.getAttribute("data-sl-toffset"));
|
|
1730
|
-
value = Number.isNaN(tOff) ? null : tOff;
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
setPlayheadMs(value);
|
|
1734
|
-
onPlayheadChangeRef.current?.(value);
|
|
1735
|
-
};
|
|
1736
|
-
|
|
1737
|
-
const observer = new IntersectionObserver(
|
|
1738
|
-
(entries) => {
|
|
1739
|
-
for (const e of entries) {
|
|
1740
|
-
const el = e.target as HTMLElement;
|
|
1741
|
-
if (e.isIntersecting) visibleRows.add(el);
|
|
1742
|
-
else visibleRows.delete(el);
|
|
1743
|
-
}
|
|
1744
|
-
publishTopmost();
|
|
1745
|
-
},
|
|
1746
|
-
{ root: main, rootMargin, threshold: 0 },
|
|
1747
|
-
);
|
|
1748
|
-
|
|
1749
|
-
// Observe all current rows. Re-observes whenever the row count
|
|
1750
|
-
// changes (i.e. filter switch, turn expand/collapse).
|
|
1751
|
-
const rows = main.querySelectorAll<HTMLDivElement>("[data-sl-row-index]");
|
|
1752
|
-
for (const r of rows) observer.observe(r);
|
|
1753
|
-
|
|
1754
|
-
return () => observer.disconnect();
|
|
1755
|
-
}, [displayRows.length, headerOffset]);
|
|
1756
|
-
|
|
1757
|
-
// Offset window for the rendered timeline. `winEnd` is the absolute offset
|
|
1758
|
-
// of the right edge (session/day end); `safeDur` is the window LENGTH used
|
|
1759
|
-
// as the proportional-width divisor. For a single-day session (dayWindow
|
|
1760
|
-
// null) these collapse to [0, durationMs] — identical to the old behavior.
|
|
1761
|
-
const winStart = dayWindow?.startMs ?? 0;
|
|
1762
|
-
const winEnd = dayWindow?.endMs ?? durationMs;
|
|
1763
|
-
const safeDur = Math.max(winEnd - winStart, 1);
|
|
1764
|
-
|
|
1765
|
-
// Subagent/workflow lanes are scoped to the same window as the main
|
|
1766
|
-
// timeline so a day view doesn't reserve empty lanes for runs that happened
|
|
1767
|
-
// on other days. Overlap test, not containment — a run straddling the
|
|
1768
|
-
// window edge still shows.
|
|
1769
|
-
const winSubagents = useMemo(
|
|
1770
|
-
() =>
|
|
1771
|
-
(subagents ?? []).filter(
|
|
1772
|
-
(s) =>
|
|
1773
|
-
s.startTOffsetMs !== undefined &&
|
|
1774
|
-
s.endTOffsetMs !== undefined &&
|
|
1775
|
-
s.endTOffsetMs >= winStart &&
|
|
1776
|
-
s.startTOffsetMs <= winEnd,
|
|
1777
|
-
),
|
|
1778
|
-
[subagents, winStart, winEnd],
|
|
1779
|
-
);
|
|
1780
|
-
const winWorkflows = useMemo(
|
|
1781
|
-
() =>
|
|
1782
|
-
(workflows ?? []).filter(
|
|
1783
|
-
(w) =>
|
|
1784
|
-
w.startTOffsetMs !== undefined &&
|
|
1785
|
-
w.startTOffsetMs <= winEnd &&
|
|
1786
|
-
(w.endTOffsetMs ?? winEnd) >= winStart,
|
|
1787
|
-
),
|
|
1788
|
-
[workflows, winStart, winEnd],
|
|
1789
|
-
);
|
|
1790
|
-
|
|
1791
|
-
/* Build segments from the same display-row stream the transcript
|
|
1792
|
-
uses. Collapsed turns become one wide segment; expanded-turn
|
|
1793
|
-
headers are skipped; presentation rows (whether standalone or
|
|
1794
|
-
indented children of an expanded turn) become atomic segments. */
|
|
1795
|
-
const segs: MinimapSeg[] = useMemo(() => {
|
|
1796
|
-
const out: MinimapSeg[] = [];
|
|
1797
|
-
|
|
1798
|
-
// Flatten to (item, start) pairs with the expanded-turn headers removed
|
|
1799
|
-
// and collapsed turns preserved as single-item entries.
|
|
1800
|
-
type WithTime =
|
|
1801
|
-
| { kind: "row"; row: PresentationRow; start: number; gapMs: number }
|
|
1802
|
-
| {
|
|
1803
|
-
kind: "turn";
|
|
1804
|
-
turn: TurnMegaRow;
|
|
1805
|
-
start: number;
|
|
1806
|
-
gapMs: number;
|
|
1807
|
-
};
|
|
1808
|
-
|
|
1809
|
-
const withTime: WithTime[] = [];
|
|
1810
|
-
for (const d of displayRows) {
|
|
1811
|
-
if (d.kind === "turn-expanded-header") continue;
|
|
1812
|
-
if (d.kind === "turn-expanded-footer") continue;
|
|
1813
|
-
if (d.kind === "turn-collapsed") {
|
|
1814
|
-
if (d.turn.tOffsetMs === undefined) continue;
|
|
1815
|
-
if (d.turn.tOffsetMs < winStart || d.turn.tOffsetMs > winEnd) continue;
|
|
1816
|
-
withTime.push({
|
|
1817
|
-
kind: "turn",
|
|
1818
|
-
turn: d.turn,
|
|
1819
|
-
start: d.turn.tOffsetMs,
|
|
1820
|
-
gapMs: d.turn.rows[0]?.gapMs ?? 0,
|
|
1821
|
-
});
|
|
1822
|
-
} else {
|
|
1823
|
-
// presentation row
|
|
1824
|
-
if (d.row.tOffsetMs === undefined) continue;
|
|
1825
|
-
if (d.row.tOffsetMs < winStart || d.row.tOffsetMs > winEnd) continue;
|
|
1826
|
-
withTime.push({
|
|
1827
|
-
kind: "row",
|
|
1828
|
-
row: d.row,
|
|
1829
|
-
start: d.row.tOffsetMs,
|
|
1830
|
-
gapMs: d.row.gapMs ?? 0,
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
// Build a sorted copy of idle bands so we can clip row/turn ends to
|
|
1836
|
-
// the start of the next idle band (or the next row, whichever comes
|
|
1837
|
-
// first). This stops a row's rectangle from spanning across an idle
|
|
1838
|
-
// gap and visually swallowing it.
|
|
1839
|
-
const idleBands = (rawIdleBands ?? [])
|
|
1840
|
-
.filter((b) => b.end > winStart && b.start < winEnd)
|
|
1841
|
-
.sort((a, b) => a.start - b.start);
|
|
1842
|
-
const nextIdleStartFrom = (t: number): number | undefined => {
|
|
1843
|
-
for (const b of idleBands) {
|
|
1844
|
-
if (b.start >= t) return b.start;
|
|
1845
|
-
}
|
|
1846
|
-
return undefined;
|
|
1847
|
-
};
|
|
1848
|
-
|
|
1849
|
-
for (let i = 0; i < withTime.length; i++) {
|
|
1850
|
-
const item = withTime[i];
|
|
1851
|
-
const next = withTime[i + 1];
|
|
1852
|
-
const start = item.start;
|
|
1853
|
-
const nextRowStart = next?.start;
|
|
1854
|
-
const nextIdleStart = nextIdleStartFrom(start + 1);
|
|
1855
|
-
// Cap end to whichever comes first: next visible row, next idle band,
|
|
1856
|
-
// or the window's right edge (so a day's last segment doesn't bleed
|
|
1857
|
-
// past the scoped window into the next day).
|
|
1858
|
-
const cap = (raw: number) =>
|
|
1859
|
-
Math.min(
|
|
1860
|
-
raw,
|
|
1861
|
-
nextRowStart ?? Number.POSITIVE_INFINITY,
|
|
1862
|
-
nextIdleStart ?? Number.POSITIVE_INFINITY,
|
|
1863
|
-
winEnd,
|
|
1864
|
-
);
|
|
1865
|
-
|
|
1866
|
-
if (item.kind === "turn") {
|
|
1867
|
-
const turnEnd = start + (item.turn.durationMs ?? 0);
|
|
1868
|
-
out.push({
|
|
1869
|
-
kind: "turn",
|
|
1870
|
-
turn: item.turn,
|
|
1871
|
-
primaryIndex: item.turn.firstPrimaryIndex,
|
|
1872
|
-
start,
|
|
1873
|
-
end: Math.max(cap(turnEnd), start + 1),
|
|
1874
|
-
});
|
|
1875
|
-
} else {
|
|
1876
|
-
const end = cap(nextRowStart ?? start + 800);
|
|
1877
|
-
out.push({
|
|
1878
|
-
kind: "row",
|
|
1879
|
-
row: item.row,
|
|
1880
|
-
primaryIndex: rowPrimaryIndex(item.row),
|
|
1881
|
-
start,
|
|
1882
|
-
end: Math.max(end, start + 1),
|
|
1883
|
-
});
|
|
1674
|
+
// Find the last collapsed turn index so we can mark it as in-progress
|
|
1675
|
+
// when the session is live.
|
|
1676
|
+
let lastCollapsedTurnIdx = -1;
|
|
1677
|
+
if (isSessionLive) {
|
|
1678
|
+
for (let i = displayRows.length - 1; i >= 0; i--) {
|
|
1679
|
+
if (displayRows[i].kind === "turn-collapsed") {
|
|
1680
|
+
lastCollapsedTurnIdx = i;
|
|
1681
|
+
break;
|
|
1884
1682
|
}
|
|
1885
1683
|
}
|
|
1684
|
+
}
|
|
1886
1685
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
}
|
|
1898
|
-
out.sort((a, b) => a.start - b.start);
|
|
1899
|
-
|
|
1900
|
-
// Cap the number of rendered segments. Each <rect> carries two
|
|
1901
|
-
// event handlers (onMouseEnter + onClick), so 2000 raw-events in
|
|
1902
|
-
// "All events" mode becomes 4000 DOM event listeners — enough to
|
|
1903
|
-
// stall Chrome during paint. Sampling to MAX contiguous buckets
|
|
1904
|
-
// loses per-item hover precision but keeps the overall shape and
|
|
1905
|
-
// click-to-navigate behavior (clicks map to the first row inside
|
|
1906
|
-
// the bucket, which is the right anchor for "jump here" UX).
|
|
1907
|
-
const MAX_SEGMENTS = 600;
|
|
1908
|
-
if (out.length <= MAX_SEGMENTS) return out;
|
|
1909
|
-
|
|
1910
|
-
const step = Math.ceil(out.length / MAX_SEGMENTS);
|
|
1911
|
-
const bucketed: MinimapSeg[] = [];
|
|
1912
|
-
for (let i = 0; i < out.length; i += step) {
|
|
1913
|
-
const first = out[i]!;
|
|
1914
|
-
const last = out[Math.min(i + step - 1, out.length - 1)]!;
|
|
1915
|
-
// Merge bucket: keep the first seg's kind + primaryIndex (so clicks
|
|
1916
|
-
// jump to the first item of the bucket), but extend the end range
|
|
1917
|
-
// to cover the whole bucket's time span.
|
|
1918
|
-
if (first.kind === "idle") {
|
|
1919
|
-
bucketed.push({
|
|
1920
|
-
kind: "idle",
|
|
1921
|
-
start: first.start,
|
|
1922
|
-
end: last.end,
|
|
1923
|
-
durationMs: last.end - first.start,
|
|
1924
|
-
});
|
|
1925
|
-
} else if (first.kind === "turn") {
|
|
1926
|
-
bucketed.push({
|
|
1927
|
-
kind: "turn",
|
|
1928
|
-
turn: first.turn,
|
|
1929
|
-
primaryIndex: first.primaryIndex,
|
|
1930
|
-
start: first.start,
|
|
1931
|
-
end: last.end,
|
|
1932
|
-
});
|
|
1933
|
-
} else {
|
|
1934
|
-
bucketed.push({
|
|
1935
|
-
kind: "row",
|
|
1936
|
-
row: first.row,
|
|
1937
|
-
primaryIndex: first.primaryIndex,
|
|
1938
|
-
start: first.start,
|
|
1939
|
-
end: last.end,
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
return bucketed;
|
|
1944
|
-
}, [displayRows, safeDur, rawIdleBands, winStart, winEnd]);
|
|
1945
|
-
|
|
1946
|
-
/* Sequential layout with a minimum displayed width per segment.
|
|
1947
|
-
- Raw proportional width = (seg.end - seg.start) / safeDur * WIDTH
|
|
1948
|
-
- Enforced width = max(raw, MIN_DISPLAY_WIDTH)
|
|
1949
|
-
- x positions are cumulative (not time-proportional), so tiny events
|
|
1950
|
-
stay visible next to a 24-minute turn.
|
|
1951
|
-
- If the cumulative width exceeds WIDTH, proportionally shrink so it
|
|
1952
|
-
all fits. Time ordering is preserved; exact time-to-pixel mapping
|
|
1953
|
-
is sacrificed. */
|
|
1954
|
-
const positions: { x: number; w: number }[] = useMemo(() => {
|
|
1955
|
-
if (segs.length === 0) return [];
|
|
1956
|
-
const raws = segs.map((s) => ((s.end - s.start) / safeDur) * WIDTH);
|
|
1957
|
-
const enforced = raws.map((w) => Math.max(w, MIN_DISPLAY_WIDTH));
|
|
1958
|
-
const total = enforced.reduce((a, b) => a + b, 0);
|
|
1959
|
-
const scale = WIDTH / total;
|
|
1960
|
-
const out: { x: number; w: number }[] = [];
|
|
1961
|
-
let cursor = 0;
|
|
1962
|
-
for (const w of enforced) {
|
|
1963
|
-
const scaled = w * scale;
|
|
1964
|
-
out.push({ x: cursor, w: scaled });
|
|
1965
|
-
cursor += scaled;
|
|
1686
|
+
const idleBandList = (rawIdleBands ?? []).slice().sort((a, b) => a.start - b.start);
|
|
1687
|
+
/** Returns the first matching idle band (or null) whose end falls within
|
|
1688
|
+
* 1s of the given row's tOffset. Tolerance absorbs Date.parse rounding
|
|
1689
|
+
* between anchor.ms and the presentation row's tOffsetMs. */
|
|
1690
|
+
const idleBandBeforeOffset = (
|
|
1691
|
+
tOff: number | undefined,
|
|
1692
|
+
): { start: number; durationMs: number } | null => {
|
|
1693
|
+
if (tOff === undefined) return null;
|
|
1694
|
+
for (const b of idleBandList) {
|
|
1695
|
+
if (Math.abs(b.end - tOff) < 1000) return b;
|
|
1966
1696
|
}
|
|
1967
|
-
return
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1697
|
+
return null;
|
|
1698
|
+
};
|
|
1699
|
+
/** Pull a t-offset off whichever display-row variant we have. */
|
|
1700
|
+
const rowTOffset = (d: DisplayRow): number | undefined => {
|
|
1701
|
+
if (d.kind === "turn-collapsed" || d.kind === "turn-expanded-header") return d.turn.tOffsetMs;
|
|
1702
|
+
if (d.kind === "turn-expanded-footer") return undefined;
|
|
1703
|
+
return d.row.tOffsetMs;
|
|
1704
|
+
};
|
|
1705
|
+
/** Absolute ISO timestamp of a row's primary event — used to date-stamp an
|
|
1706
|
+
* idle gap that resumes on a later day. */
|
|
1707
|
+
const rowTimestamp = (d: DisplayRow): string | undefined => {
|
|
1708
|
+
const r =
|
|
1709
|
+
d.kind === "presentation"
|
|
1710
|
+
? d.row
|
|
1711
|
+
: d.kind === "turn-collapsed" || d.kind === "turn-expanded-header"
|
|
1712
|
+
? d.turn.rows[0]
|
|
1713
|
+
: undefined;
|
|
1714
|
+
if (!r) return undefined;
|
|
1715
|
+
return r.kind === "tool-group" ? r.events[0]?.timestamp : r.event?.timestamp;
|
|
1716
|
+
};
|
|
1717
|
+
// A display row → its event PRIMARY index (what jumpAcrossDay's turn/row
|
|
1718
|
+
// lookups are keyed by — NOT the displayRows loop position).
|
|
1719
|
+
const rowPrimaryIndexOf = (d: DisplayRow): number =>
|
|
1720
|
+
d.kind === "presentation" ? rowPrimaryIndex(d.row) : d.turn.firstPrimaryIndex;
|
|
1721
|
+
/** Which local day a row falls in: the last day bucket whose start is at or
|
|
1722
|
+
* before the row's offset. sessionDays is pre-sorted by startMs. */
|
|
1723
|
+
const dayKeyForOffset = (tOff: number | undefined): string | null => {
|
|
1724
|
+
if (tOff === undefined) return null;
|
|
1725
|
+
let key: string | null = null;
|
|
1726
|
+
for (const dd of sessionDays) {
|
|
1727
|
+
if (dd.startMs <= tOff) key = dd.key;
|
|
1728
|
+
else break;
|
|
1986
1729
|
}
|
|
1987
|
-
|
|
1988
|
-
return last ? last.x + last.w : WIDTH;
|
|
1730
|
+
return key;
|
|
1989
1731
|
};
|
|
1990
1732
|
|
|
1991
|
-
|
|
1992
|
-
//
|
|
1993
|
-
//
|
|
1994
|
-
|
|
1995
|
-
//
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
// matching the open-ended bar the render draws. Counting only placeable
|
|
2036
|
-
// runs keeps wfLaneCount in lockstep with what renders, so no empty
|
|
2037
|
-
// reserved band appears.
|
|
2038
|
-
const placeable = winWorkflows;
|
|
2039
|
-
if (placeable.length === 0) {
|
|
2040
|
-
return { wfLaneOf: new Map<string, number>(), wfLaneCount: 0 };
|
|
2041
|
-
}
|
|
2042
|
-
const sorted = [...placeable].sort(
|
|
2043
|
-
(a, b) => (a.startTOffsetMs ?? 0) - (b.startTOffsetMs ?? 0),
|
|
2044
|
-
);
|
|
2045
|
-
const laneEnds: number[] = [];
|
|
2046
|
-
const wfLaneOf = new Map<string, number>();
|
|
2047
|
-
for (const w of sorted) {
|
|
2048
|
-
const start = w.startTOffsetMs ?? 0;
|
|
2049
|
-
const end = w.endTOffsetMs ?? winEnd;
|
|
2050
|
-
let assigned = -1;
|
|
2051
|
-
for (let i = 0; i < laneEnds.length; i++) {
|
|
2052
|
-
if (laneEnds[i]! <= start) {
|
|
2053
|
-
assigned = i;
|
|
2054
|
-
break;
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
if (assigned === -1) {
|
|
2058
|
-
assigned = laneEnds.length;
|
|
2059
|
-
laneEnds.push(0);
|
|
1733
|
+
const out: React.ReactNode[] = [];
|
|
1734
|
+
// Tracks the day of the last emitted row so a day's digest card is dropped
|
|
1735
|
+
// once, before that day's first non-indented row.
|
|
1736
|
+
let currentDayKey: string | null = null;
|
|
1737
|
+
// A user row and the turn-collapsed row that follows it share a
|
|
1738
|
+
// tOffsetMs (turn anchors at the user message — see buildMegaRows), so
|
|
1739
|
+
// both would match the same between-turn band without this guard. Track
|
|
1740
|
+
// emitted band starts and skip duplicates.
|
|
1741
|
+
const emittedBandStarts = new Set<number>();
|
|
1742
|
+
for (let i = 0; i < displayRows.length; i++) {
|
|
1743
|
+
const d = displayRows[i];
|
|
1744
|
+
// Emit a "Session idle" divider before any row that starts right after
|
|
1745
|
+
// an anchor-to-anchor idle band. Skips indented (child) rows so an
|
|
1746
|
+
// expanded turn doesn't render dividers for its inner steps.
|
|
1747
|
+
const indented = d.kind === "presentation" ? d.indented : false;
|
|
1748
|
+
if (!indented) {
|
|
1749
|
+
const band = idleBandBeforeOffset(rowTOffset(d));
|
|
1750
|
+
// Set when this boundary's idle divider already states the new day + time,
|
|
1751
|
+
// so the "Start of <new day>" marker below can be dropped as a redundant
|
|
1752
|
+
// restatement.
|
|
1753
|
+
let resumeStamped = false;
|
|
1754
|
+
if (
|
|
1755
|
+
band !== null &&
|
|
1756
|
+
band.durationMs > IDLE_THRESHOLD_MS &&
|
|
1757
|
+
!emittedBandStarts.has(band.start)
|
|
1758
|
+
) {
|
|
1759
|
+
emittedBandStarts.add(band.start);
|
|
1760
|
+
// When the gap resumes on a LATER day, "1d" is vaguer than the actual
|
|
1761
|
+
// resume moment — show that instead. Same day-key trigger as the day
|
|
1762
|
+
// boundary marker emitted just below, so the two agree. Sub-day gaps
|
|
1763
|
+
// keep the duration (the pause length is the useful signal there).
|
|
1764
|
+
const resumeDayKey = dayKeyForOffset(rowTOffset(d));
|
|
1765
|
+
const resumeTs =
|
|
1766
|
+
currentDayKey !== null && resumeDayKey !== null && resumeDayKey !== currentDayKey
|
|
1767
|
+
? rowTimestamp(d)
|
|
1768
|
+
: undefined;
|
|
1769
|
+
resumeStamped = resumeTs !== undefined;
|
|
1770
|
+
out.push(
|
|
1771
|
+
<IdleDivider
|
|
1772
|
+
key={`idle-before-${i}`}
|
|
1773
|
+
gapMs={band.durationMs}
|
|
1774
|
+
resumeStamp={resumeTs ? formatResumeStamp(resumeTs) : undefined}
|
|
1775
|
+
/>,
|
|
1776
|
+
);
|
|
2060
1777
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
viewBox={`0 0 ${WIDTH} ${TOTAL_H}`}
|
|
2087
|
-
preserveAspectRatio="none"
|
|
2088
|
-
style={{
|
|
2089
|
-
width: "100%",
|
|
2090
|
-
height: TOTAL_H,
|
|
2091
|
-
display: "block",
|
|
2092
|
-
overflow: "visible",
|
|
2093
|
-
}}
|
|
2094
|
-
>
|
|
2095
|
-
<defs>
|
|
2096
|
-
<pattern
|
|
2097
|
-
id="stripes"
|
|
2098
|
-
patternUnits="userSpaceOnUse"
|
|
2099
|
-
width="6"
|
|
2100
|
-
height="6"
|
|
2101
|
-
patternTransform="rotate(45)"
|
|
2102
|
-
>
|
|
2103
|
-
<rect width="6" height="6" fill="rgba(120, 115, 108, 0.04)" />
|
|
2104
|
-
<line x1="0" y1="0" x2="0" y2="6" stroke="rgba(120, 115, 108, 0.28)" strokeWidth="2" />
|
|
2105
|
-
</pattern>
|
|
2106
|
-
</defs>
|
|
2107
|
-
|
|
2108
|
-
{/* Segments — x/w come from the relaxed sequential layout, NOT
|
|
2109
|
-
from raw time proportions. See the positions memo above. */}
|
|
2110
|
-
{segs.map((seg, i) => {
|
|
2111
|
-
const pos = positions[i];
|
|
2112
|
-
if (!pos) return null;
|
|
2113
|
-
const xRaw = pos.x;
|
|
2114
|
-
const wRaw = pos.w;
|
|
2115
|
-
|
|
2116
|
-
if (seg.kind === "idle") {
|
|
2117
|
-
// Idle block: fill its span minus a gap on each side, with a
|
|
2118
|
-
// subtle border so the diagonal stripes read as a distinct block.
|
|
2119
|
-
const w = Math.max(wRaw - GAP, MIN_BLOCK);
|
|
2120
|
-
return (
|
|
2121
|
-
<rect
|
|
2122
|
-
key={`idle-${i}`}
|
|
2123
|
-
x={xRaw + GAP / 2}
|
|
2124
|
-
y={BAR_TOP + 1}
|
|
2125
|
-
width={w}
|
|
2126
|
-
height={BAR_H - 2}
|
|
2127
|
-
fill="url(#stripes)"
|
|
2128
|
-
stroke="rgba(120, 115, 108, 0.35)"
|
|
2129
|
-
strokeWidth="0.6"
|
|
2130
|
-
rx="3"
|
|
2131
|
-
onMouseEnter={(e) =>
|
|
2132
|
-
setHover({
|
|
2133
|
-
clientX: e.clientX,
|
|
2134
|
-
idleMs: seg.durationMs,
|
|
2135
|
-
})
|
|
2136
|
-
}
|
|
2137
|
-
/>
|
|
2138
|
-
);
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
// Resolve the theme + selection state. Turn segments use the
|
|
2142
|
-
// "agent" theme (a turn is the agent's work wrapped as one unit).
|
|
2143
|
-
const rowKind: PresentationRowKind = seg.kind === "turn" ? "agent" : seg.row.kind;
|
|
2144
|
-
const theme = ROLE_THEMES[rowKind];
|
|
2145
|
-
const importance = ROW_IMPORTANCE[rowKind];
|
|
2146
|
-
const h = BAR_H * importance;
|
|
2147
|
-
const y = BAR_TOP + (BAR_H - h) / 2;
|
|
2148
|
-
const isSelected = selectedIndex === seg.primaryIndex;
|
|
2149
|
-
|
|
2150
|
-
// Error/interrupt: render as a THIN vertical bar regardless of
|
|
2151
|
-
// actual span. Consecutive errors become a visible comb of
|
|
2152
|
-
// narrow strokes separated by gaps, matching Claude's UI.
|
|
2153
|
-
const isThin =
|
|
2154
|
-
seg.kind === "row" && (seg.row.kind === "error" || seg.row.kind === "interrupt");
|
|
2155
|
-
let w: number;
|
|
2156
|
-
let x: number;
|
|
2157
|
-
if (isThin) {
|
|
2158
|
-
w = Math.min(Math.max(wRaw - GAP, 2), THIN_BAR_MAX);
|
|
2159
|
-
x = xRaw + Math.max((wRaw - w) / 2, GAP / 2);
|
|
2160
|
-
} else {
|
|
2161
|
-
w = Math.max(wRaw - GAP, MIN_BLOCK);
|
|
2162
|
-
x = xRaw + GAP / 2;
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
const ringPad = 2.5;
|
|
2166
|
-
const onHover = (e: React.MouseEvent) =>
|
|
2167
|
-
seg.kind === "turn"
|
|
2168
|
-
? setHover({ clientX: e.clientX, turn: seg.turn })
|
|
2169
|
-
: setHover({ clientX: e.clientX, row: seg.row });
|
|
2170
|
-
return (
|
|
2171
|
-
<g key={`seg-${seg.primaryIndex}-${i}`}>
|
|
2172
|
-
<rect
|
|
2173
|
-
x={x}
|
|
2174
|
-
y={y}
|
|
2175
|
-
width={w}
|
|
2176
|
-
height={h}
|
|
2177
|
-
fill={theme.mini}
|
|
2178
|
-
rx="3"
|
|
2179
|
-
style={{ cursor: "pointer" }}
|
|
2180
|
-
onClick={() => onSelect(seg.primaryIndex)}
|
|
2181
|
-
onMouseEnter={onHover}
|
|
2182
|
-
/>
|
|
2183
|
-
{isSelected && (
|
|
2184
|
-
<rect
|
|
2185
|
-
x={x - ringPad}
|
|
2186
|
-
y={y - ringPad}
|
|
2187
|
-
width={w + ringPad * 2}
|
|
2188
|
-
height={h + ringPad * 2}
|
|
2189
|
-
fill="none"
|
|
2190
|
-
stroke="#5C84C3"
|
|
2191
|
-
strokeWidth="2"
|
|
2192
|
-
rx="5"
|
|
2193
|
-
pointerEvents="none"
|
|
2194
|
-
/>
|
|
2195
|
-
)}
|
|
2196
|
-
</g>
|
|
1778
|
+
// Day boundary: drop this day's digest card at its first row (after any
|
|
1779
|
+
// overnight idle divider that led into the day) so per-day perception
|
|
1780
|
+
// sits inline with that day's work, not stacked at the top. The boundary
|
|
1781
|
+
// also gets inline jump controls so you can page across days while
|
|
1782
|
+
// reading — "↓ start of next day" sits at the bottom of the day that just
|
|
1783
|
+
// ended, "↑ end of previous day" at the top of the new one.
|
|
1784
|
+
const dk = dayKeyForOffset(rowTOffset(d));
|
|
1785
|
+
if (dk && dk !== currentDayKey) {
|
|
1786
|
+
const endedKey = currentDayKey; // day that just ended; null on the first day
|
|
1787
|
+
currentDayKey = dk;
|
|
1788
|
+
// Fallback scroll target for days with no digest card: the day's first
|
|
1789
|
+
// row by PRIMARY index (i is the displayRows position — wrong key space).
|
|
1790
|
+
const firstIdx = rowPrimaryIndexOf(d);
|
|
1791
|
+
// Skip the "Start of <new day>" marker when the resume divider above
|
|
1792
|
+
// already names this day + time — it would just restate it. With no
|
|
1793
|
+
// resume divider (continuous or sub-threshold crossing) the marker is
|
|
1794
|
+
// the only thing labeling the boundary, so it stays.
|
|
1795
|
+
if (endedKey && !resumeStamped) {
|
|
1796
|
+
out.push(
|
|
1797
|
+
<DayJumpRow
|
|
1798
|
+
key={`daydown-${dk}`}
|
|
1799
|
+
dir="down"
|
|
1800
|
+
label={formatDayKey(dk)}
|
|
1801
|
+
onClick={() => onJumpAcrossDay(dk, dk, "start", firstIdx)}
|
|
1802
|
+
/>,
|
|
2197
1803
|
);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
{/* Faint divider line above the subagent lane block */}
|
|
2209
|
-
<line
|
|
2210
|
-
x1={0}
|
|
2211
|
-
x2={WIDTH}
|
|
2212
|
-
y1={MAIN_H + SUB_LANE_GAP / 2}
|
|
2213
|
-
y2={MAIN_H + SUB_LANE_GAP / 2}
|
|
2214
|
-
stroke="var(--af-border-subtle)"
|
|
2215
|
-
strokeWidth="0.6"
|
|
2216
|
-
strokeDasharray="2 4"
|
|
2217
|
-
/>
|
|
2218
|
-
{winSubagents.map((s) => {
|
|
2219
|
-
const lane = laneOf.get(s.agentId) ?? 0;
|
|
2220
|
-
if (s.startTOffsetMs === undefined || s.endTOffsetMs === undefined) return null;
|
|
2221
|
-
const startX = msToX(s.startTOffsetMs);
|
|
2222
|
-
const endX = msToX(s.endTOffsetMs);
|
|
2223
|
-
// Minimum 8px so a 78-second subagent in a 16-hour session is
|
|
2224
|
-
// still wide enough to read + hit-test. Long subagents render
|
|
2225
|
-
// at their actual proportional width.
|
|
2226
|
-
const w = Math.max(endX - startX, 8);
|
|
2227
|
-
const y = SUB_BLOCK_TOP + lane * SUB_LANE_H;
|
|
2228
|
-
const h = SUB_LANE_H - 2;
|
|
2229
|
-
const fill = subagentColor(s.agentType, s.runInBackground);
|
|
2230
|
-
const isSelected = selectedSubagentId === s.agentId;
|
|
2231
|
-
return (
|
|
2232
|
-
<g key={s.agentId}>
|
|
2233
|
-
<rect
|
|
2234
|
-
x={startX}
|
|
2235
|
-
y={y}
|
|
2236
|
-
width={w}
|
|
2237
|
-
height={h}
|
|
2238
|
-
fill={fill}
|
|
2239
|
-
stroke={s.runInBackground ? "#fff" : "transparent"}
|
|
2240
|
-
strokeWidth={s.runInBackground ? 0.5 : 0}
|
|
2241
|
-
strokeDasharray={s.runInBackground ? "1.5 1.5" : undefined}
|
|
2242
|
-
rx="2"
|
|
2243
|
-
style={{ cursor: "pointer" }}
|
|
2244
|
-
onMouseEnter={(e) => setHover({ clientX: e.clientX, subagent: s })}
|
|
2245
|
-
onClick={() => onSelectSubagent?.(isSelected ? null : s.agentId)}
|
|
2246
|
-
/>
|
|
2247
|
-
{isSelected && (
|
|
2248
|
-
<rect
|
|
2249
|
-
x={startX - 2}
|
|
2250
|
-
y={y - 2}
|
|
2251
|
-
width={w + 4}
|
|
2252
|
-
height={h + 4}
|
|
2253
|
-
fill="none"
|
|
2254
|
-
stroke="var(--af-accent)"
|
|
2255
|
-
strokeWidth="1.5"
|
|
2256
|
-
rx="4"
|
|
2257
|
-
pointerEvents="none"
|
|
2258
|
-
/>
|
|
2259
|
-
)}
|
|
2260
|
-
</g>
|
|
2261
|
-
);
|
|
2262
|
-
})}
|
|
2263
|
-
</g>
|
|
2264
|
-
)}
|
|
2265
|
-
|
|
2266
|
-
{/* Workflow lanes — one bar per dynamic-workflow run, on the same
|
|
2267
|
-
time axis as the main timeline, colored by status (orange tier,
|
|
2268
|
-
distinct from the blue/purple subagents). The agent count is the
|
|
2269
|
-
headline — a single Workflow tool call hides a whole fleet. */}
|
|
2270
|
-
{wfLaneCount > 0 && winWorkflows.length > 0 && (
|
|
2271
|
-
<g>
|
|
2272
|
-
<line
|
|
2273
|
-
x1={0}
|
|
2274
|
-
x2={WIDTH}
|
|
2275
|
-
y1={WF_BLOCK_TOP - SUB_LANE_GAP / 2}
|
|
2276
|
-
y2={WF_BLOCK_TOP - SUB_LANE_GAP / 2}
|
|
2277
|
-
stroke="var(--af-border-subtle)"
|
|
2278
|
-
strokeWidth="0.6"
|
|
2279
|
-
strokeDasharray="2 4"
|
|
2280
|
-
/>
|
|
2281
|
-
{winWorkflows.map((w) => {
|
|
2282
|
-
if (w.startTOffsetMs === undefined) return null;
|
|
2283
|
-
const lane = wfLaneOf.get(w.runId) ?? 0;
|
|
2284
|
-
const startX = msToX(w.startTOffsetMs);
|
|
2285
|
-
// A still-running run has no endTOffsetMs — draw it open-ended to
|
|
2286
|
-
// the session end so it's visible (and dashed, to read as ongoing)
|
|
2287
|
-
// rather than vanishing while still occupying a reserved lane.
|
|
2288
|
-
const inProgress = w.endTOffsetMs === undefined;
|
|
2289
|
-
const endX = msToX(w.endTOffsetMs ?? winEnd);
|
|
2290
|
-
const wWidth = Math.max(endX - startX, 8);
|
|
2291
|
-
const y = WF_BLOCK_TOP + lane * WF_LANE_H;
|
|
2292
|
-
const h = WF_LANE_H - 2;
|
|
2293
|
-
const fill = workflowColor(w.status);
|
|
2294
|
-
// Center the agent count label inside the bar when there's room.
|
|
2295
|
-
const label = `${w.agentCount}`;
|
|
2296
|
-
const showLabel = wWidth > 22;
|
|
2297
|
-
return (
|
|
2298
|
-
<g key={w.runId} style={{ cursor: "pointer" }}>
|
|
2299
|
-
<rect
|
|
2300
|
-
x={startX}
|
|
2301
|
-
y={y}
|
|
2302
|
-
width={wWidth}
|
|
2303
|
-
height={h}
|
|
2304
|
-
fill={fill}
|
|
2305
|
-
fillOpacity={inProgress ? 0.55 : 1}
|
|
2306
|
-
stroke={inProgress ? fill : "transparent"}
|
|
2307
|
-
strokeWidth={inProgress ? 0.8 : 0}
|
|
2308
|
-
strokeDasharray={inProgress ? "2 2" : undefined}
|
|
2309
|
-
rx="2"
|
|
2310
|
-
onMouseEnter={(e) => setHover({ clientX: e.clientX, workflow: w })}
|
|
2311
|
-
onClick={() => onWorkflowClick?.(w.runId)}
|
|
2312
|
-
/>
|
|
2313
|
-
{showLabel && (
|
|
2314
|
-
<text
|
|
2315
|
-
x={startX + wWidth / 2}
|
|
2316
|
-
y={y + h / 2 + 3}
|
|
2317
|
-
textAnchor="middle"
|
|
2318
|
-
fontSize="8.5"
|
|
2319
|
-
fontWeight="700"
|
|
2320
|
-
fill="#fff"
|
|
2321
|
-
pointerEvents="none"
|
|
2322
|
-
style={{ fontFamily: "var(--font-mono)" }}
|
|
2323
|
-
>
|
|
2324
|
-
{label}
|
|
2325
|
-
</text>
|
|
2326
|
-
)}
|
|
2327
|
-
</g>
|
|
2328
|
-
);
|
|
2329
|
-
})}
|
|
2330
|
-
</g>
|
|
2331
|
-
)}
|
|
2332
|
-
|
|
2333
|
-
{/* PR markers — diamond + vertical line at each `gh pr create` */}
|
|
2334
|
-
{prMarkers?.map((pr, i) => {
|
|
2335
|
-
if (pr.tOffsetMs === undefined) return null;
|
|
2336
|
-
if (pr.tOffsetMs < winStart || pr.tOffsetMs > winEnd) return null;
|
|
2337
|
-
const x = msToX(pr.tOffsetMs);
|
|
2338
|
-
return (
|
|
2339
|
-
<g
|
|
2340
|
-
key={`pr-${i}`}
|
|
2341
|
-
style={{ cursor: "pointer" }}
|
|
2342
|
-
onMouseEnter={(e) => setHover({ clientX: e.clientX, pr })}
|
|
1804
|
+
}
|
|
1805
|
+
const dayEntry = entryByDay.get(dk);
|
|
1806
|
+
if (dayEntry) {
|
|
1807
|
+
out.push(
|
|
1808
|
+
<div
|
|
1809
|
+
key={`daystrip-${dk}`}
|
|
1810
|
+
ref={(el) => {
|
|
1811
|
+
dayStripRefs.current[dk] = el;
|
|
1812
|
+
}}
|
|
1813
|
+
style={{ marginBottom: 12, scrollMarginTop: stickyOffset }}
|
|
2343
1814
|
>
|
|
2344
|
-
<
|
|
2345
|
-
|
|
2346
|
-
x2={x}
|
|
2347
|
-
y1={0}
|
|
2348
|
-
y2={TOTAL_H}
|
|
2349
|
-
stroke="#8b5cf6"
|
|
2350
|
-
strokeWidth="1.5"
|
|
2351
|
-
strokeDasharray="4 3"
|
|
2352
|
-
opacity="0.7"
|
|
2353
|
-
/>
|
|
2354
|
-
{/* Invisible wider hit area for easier hovering */}
|
|
2355
|
-
<rect
|
|
2356
|
-
x={x - 8}
|
|
2357
|
-
y={0}
|
|
2358
|
-
width={16}
|
|
2359
|
-
height={TOTAL_H}
|
|
2360
|
-
fill="transparent"
|
|
2361
|
-
/>
|
|
2362
|
-
{/* Diamond marker at mid-height */}
|
|
2363
|
-
<polygon
|
|
2364
|
-
points={`${x},${MAIN_H / 2 - 5} ${x + 5},${MAIN_H / 2} ${x},${MAIN_H / 2 + 5} ${x - 5},${MAIN_H / 2}`}
|
|
2365
|
-
fill="#8b5cf6"
|
|
2366
|
-
stroke="var(--af-surface)"
|
|
2367
|
-
strokeWidth="1.5"
|
|
2368
|
-
/>
|
|
2369
|
-
</g>
|
|
1815
|
+
<EntryDayStrip entry={dayEntry} />
|
|
1816
|
+
</div>,
|
|
2370
1817
|
);
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
onMouseEnter={(e) => setHover({ clientX: e.clientX, cold: cr })}
|
|
2381
|
-
>
|
|
2382
|
-
<line
|
|
2383
|
-
x1={x}
|
|
2384
|
-
x2={x}
|
|
2385
|
-
y1={0}
|
|
2386
|
-
y2={TOTAL_H}
|
|
2387
|
-
stroke="#D97706"
|
|
2388
|
-
strokeWidth="1.5"
|
|
2389
|
-
strokeDasharray="4 3"
|
|
2390
|
-
opacity="0.75"
|
|
2391
|
-
pointerEvents="none"
|
|
2392
|
-
/>
|
|
2393
|
-
{/* Hover hit-area limited to the diamond itself — a wider/taller
|
|
2394
|
-
target used to swallow hovers+clicks meant for adjacent
|
|
2395
|
-
user-prompt markers sitting right beside the cache-rebuild
|
|
2396
|
-
point. */}
|
|
2397
|
-
<rect
|
|
2398
|
-
x={x - 6}
|
|
2399
|
-
y={MAIN_H / 2 - 8}
|
|
2400
|
-
width={12}
|
|
2401
|
-
height={16}
|
|
2402
|
-
fill="transparent"
|
|
2403
|
-
/>
|
|
2404
|
-
<polygon
|
|
2405
|
-
points={`${x},${MAIN_H / 2 - 5} ${x + 5},${MAIN_H / 2} ${x},${MAIN_H / 2 + 5} ${x - 5},${MAIN_H / 2}`}
|
|
2406
|
-
fill="#D97706"
|
|
2407
|
-
stroke="var(--af-surface)"
|
|
2408
|
-
strokeWidth="1.5"
|
|
2409
|
-
pointerEvents="none"
|
|
2410
|
-
/>
|
|
2411
|
-
</g>
|
|
1818
|
+
}
|
|
1819
|
+
if (endedKey) {
|
|
1820
|
+
out.push(
|
|
1821
|
+
<DayJumpRow
|
|
1822
|
+
key={`dayup-${dk}`}
|
|
1823
|
+
dir="up"
|
|
1824
|
+
label={formatDayKey(endedKey)}
|
|
1825
|
+
onClick={() => onJumpAcrossDay(endedKey, dk, "end", firstIdx)}
|
|
1826
|
+
/>,
|
|
2412
1827
|
);
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
Spans the full SVG height (main + subagent lanes) so you can see
|
|
2417
|
-
which subagents were running at the current scroll position. */}
|
|
2418
|
-
{playheadMs !== null && (
|
|
2419
|
-
<line
|
|
2420
|
-
x1={msToX(playheadMs)}
|
|
2421
|
-
x2={msToX(playheadMs)}
|
|
2422
|
-
y1={0}
|
|
2423
|
-
y2={TOTAL_H}
|
|
2424
|
-
stroke="#0F172A"
|
|
2425
|
-
strokeWidth="1.25"
|
|
2426
|
-
strokeDasharray="2 2"
|
|
2427
|
-
opacity="0.65"
|
|
2428
|
-
/>
|
|
2429
|
-
)}
|
|
2430
|
-
</svg>
|
|
2431
|
-
|
|
2432
|
-
{/* Hover card */}
|
|
2433
|
-
{hover && (
|
|
2434
|
-
<MinimapHoverCard
|
|
2435
|
-
containerRef={containerRef}
|
|
2436
|
-
clientX={hover.clientX}
|
|
2437
|
-
row={hover.row}
|
|
2438
|
-
turn={hover.turn}
|
|
2439
|
-
idleMs={hover.idleMs}
|
|
2440
|
-
subagent={hover.subagent}
|
|
2441
|
-
workflow={hover.workflow}
|
|
2442
|
-
pr={hover.pr}
|
|
2443
|
-
cold={hover.cold}
|
|
2444
|
-
model={model}
|
|
2445
|
-
/>
|
|
2446
|
-
)}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
2447
1831
|
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
1832
|
+
// Collapsed turn summary row
|
|
1833
|
+
if (d.kind === "turn-collapsed") {
|
|
1834
|
+
const idx = d.turn.firstPrimaryIndex;
|
|
1835
|
+
const turnCold = findColdResumeInDisplayRow(d);
|
|
1836
|
+
out.push(
|
|
1837
|
+
<CollapsedTurnRow
|
|
1838
|
+
key={`turn-${idx}`}
|
|
1839
|
+
turn={d.turn}
|
|
1840
|
+
stickyOffset={stickyOffset}
|
|
1841
|
+
onClick={() => onToggleTurn(idx)}
|
|
1842
|
+
inProgress={i === lastCollapsedTurnIdx}
|
|
1843
|
+
team={team}
|
|
1844
|
+
coldResume={turnCold ?? undefined}
|
|
1845
|
+
model={model}
|
|
1846
|
+
refCb={(el) => {
|
|
1847
|
+
rowRefs.current[idx] = el;
|
|
2458
1848
|
}}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
1849
|
+
/>,
|
|
1850
|
+
);
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// Expanded turn header
|
|
1855
|
+
if (d.kind === "turn-expanded-header") {
|
|
1856
|
+
const turnCold = findColdResumeInDisplayRow(d);
|
|
1857
|
+
out.push(
|
|
1858
|
+
<ExpandedTurnHeader
|
|
1859
|
+
key={`turnhead-${d.turn.firstPrimaryIndex}`}
|
|
1860
|
+
turn={d.turn}
|
|
1861
|
+
onClick={() => onToggleTurn(d.turn.firstPrimaryIndex)}
|
|
1862
|
+
coldResume={turnCold ?? undefined}
|
|
1863
|
+
model={model}
|
|
1864
|
+
/>,
|
|
1865
|
+
);
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Expanded turn footer — matching collapse control at the bottom of
|
|
1870
|
+
// the expanded rows so the user can collapse right where their eye
|
|
1871
|
+
// lands after reading through the content.
|
|
1872
|
+
if (d.kind === "turn-expanded-footer") {
|
|
1873
|
+
out.push(
|
|
1874
|
+
<ExpandedTurnFooter
|
|
1875
|
+
key={`turnfoot-${d.turn.firstPrimaryIndex}`}
|
|
1876
|
+
turn={d.turn}
|
|
1877
|
+
onClick={() => onToggleTurn(d.turn.firstPrimaryIndex)}
|
|
1878
|
+
/>,
|
|
1879
|
+
);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// Normal presentation row (possibly indented as a child of an expanded
|
|
1884
|
+
// turn). Idle dividers are now emitted at the top of the loop from the
|
|
1885
|
+
// shared rawIdleBands list — no separate gap-before-user check needed.
|
|
1886
|
+
const r = d.row;
|
|
1887
|
+
const idx = rowPrimaryIndex(r);
|
|
1888
|
+
out.push(
|
|
1889
|
+
<TranscriptRow
|
|
1890
|
+
key={`row-${idx}`}
|
|
1891
|
+
row={r}
|
|
1892
|
+
selected={selectedIndex === idx}
|
|
1893
|
+
onSelect={() => onSelect(idx)}
|
|
1894
|
+
stickyOffset={stickyOffset}
|
|
1895
|
+
indented={d.indented}
|
|
1896
|
+
refCb={(el) => {
|
|
1897
|
+
rowRefs.current[idx] = el;
|
|
1898
|
+
}}
|
|
1899
|
+
/>,
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
return <div>{out}</div>;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function IdleDivider({ gapMs, resumeStamp }: { gapMs: number; resumeStamp?: string }) {
|
|
1906
|
+
return (
|
|
1907
|
+
<div
|
|
1908
|
+
style={{
|
|
1909
|
+
margin: "8px 0",
|
|
1910
|
+
padding: "6px 12px",
|
|
1911
|
+
textAlign: "center",
|
|
1912
|
+
background:
|
|
1913
|
+
"repeating-linear-gradient(135deg, rgba(107,101,96,0.04) 0px, rgba(107,101,96,0.04) 6px, rgba(107,101,96,0.12) 6px, rgba(107,101,96,0.12) 12px)",
|
|
1914
|
+
border: "1px solid var(--af-border-subtle)",
|
|
1915
|
+
borderRadius: 6,
|
|
1916
|
+
fontSize: 11,
|
|
1917
|
+
color: "var(--af-text-tertiary)",
|
|
1918
|
+
letterSpacing: "0.02em",
|
|
1919
|
+
}}
|
|
1920
|
+
>
|
|
1921
|
+
{resumeStamp ? `Session resumed ${resumeStamp}` : `Session idle · ${formatGap(gapMs)}`}
|
|
2503
1922
|
</div>
|
|
2504
1923
|
);
|
|
2505
1924
|
}
|
|
2506
1925
|
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
1926
|
+
/** Inline cross-day jump control rendered at each day boundary. "down" sits at
|
|
1927
|
+
* the bottom of the day that just ended and jumps to the start of the next day;
|
|
1928
|
+
* "up" sits at the top of the new day and jumps to the end of the previous one.
|
|
1929
|
+
* The day always follows the scroll position (see followDayToScroll); these are
|
|
1930
|
+
* a one-click shortcut across the long idle gap between days so you don't have
|
|
1931
|
+
* to hand-scroll the whole span. "down" (start of next day) lands that day's
|
|
1932
|
+
* first row at the header line so the day-follow agrees; "up" (end of previous
|
|
1933
|
+
* day) scrolls the new day's strip to the viewport bottom, leaving the previous
|
|
1934
|
+
* day's tail above the header line. */
|
|
1935
|
+
function DayJumpRow({
|
|
1936
|
+
dir,
|
|
1937
|
+
label,
|
|
1938
|
+
onClick,
|
|
2520
1939
|
}: {
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
turn?: TurnMegaRow;
|
|
2525
|
-
idleMs?: number;
|
|
2526
|
-
subagent?: SubagentRun;
|
|
2527
|
-
workflow?: WorkflowRun;
|
|
2528
|
-
pr?: PrMarker;
|
|
2529
|
-
cold?: { tOffsetMs: number; info: NonNullable<SessionEvent["coldResume"]> };
|
|
2530
|
-
model?: string;
|
|
1940
|
+
dir: "up" | "down";
|
|
1941
|
+
label: string;
|
|
1942
|
+
onClick: () => void;
|
|
2531
1943
|
}) {
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
if (workflow) {
|
|
2540
|
-
const w = workflow;
|
|
2541
|
-
const startOff = formatOffset(w.startTOffsetMs);
|
|
2542
|
-
const endOff = formatOffset(w.endTOffsetMs);
|
|
2543
|
-
const dur = w.durationMs !== undefined ? formatGap(w.durationMs) : "";
|
|
2544
|
-
return (
|
|
2545
|
-
<div
|
|
1944
|
+
return (
|
|
1945
|
+
<div style={{ display: "flex", justifyContent: "center", margin: "6px 0" }}>
|
|
1946
|
+
<button
|
|
1947
|
+
type="button"
|
|
1948
|
+
onClick={onClick}
|
|
1949
|
+
title={dir === "up" ? `Go to the end of ${label}` : `Go to the start of ${label}`}
|
|
2546
1950
|
style={{
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
background: "#0F172A",
|
|
2551
|
-
color: "#F1F5F9",
|
|
2552
|
-
borderRadius: 10,
|
|
2553
|
-
padding: "12px 14px",
|
|
1951
|
+
display: "inline-flex",
|
|
1952
|
+
alignItems: "center",
|
|
1953
|
+
gap: 6,
|
|
2554
1954
|
fontSize: 11,
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
1955
|
+
fontFamily: "var(--font-mono)",
|
|
1956
|
+
color: "var(--af-text-secondary)",
|
|
1957
|
+
background: "var(--af-surface)",
|
|
1958
|
+
border: "1px solid var(--af-border-subtle)",
|
|
1959
|
+
borderRadius: 100,
|
|
1960
|
+
padding: "3px 12px",
|
|
1961
|
+
cursor: "pointer",
|
|
2560
1962
|
}}
|
|
2561
1963
|
>
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
<div style={{ opacity: 0.8, marginBottom: 6 }}>{w.description}</div>
|
|
2593
|
-
)}
|
|
2594
|
-
<div
|
|
2595
|
-
style={{
|
|
2596
|
-
fontSize: 10,
|
|
2597
|
-
opacity: 0.75,
|
|
2598
|
-
display: "flex",
|
|
2599
|
-
gap: 8,
|
|
2600
|
-
flexWrap: "wrap",
|
|
2601
|
-
paddingTop: 6,
|
|
2602
|
-
borderTop: "1px solid rgba(241,245,249,0.08)",
|
|
2603
|
-
fontFamily: "var(--font-mono)",
|
|
2604
|
-
}}
|
|
2605
|
-
>
|
|
2606
|
-
<span style={{ color: "#FB923C", fontWeight: 600 }}>{w.agentCount} agents</span>
|
|
2607
|
-
<span>·</span>
|
|
2608
|
-
<span>{w.toolCallCount.toLocaleString()} tools</span>
|
|
2609
|
-
<span>·</span>
|
|
2610
|
-
<span>{formatTokens(w.totalTokens)} tok</span>
|
|
2611
|
-
{w.phases.length > 0 && (
|
|
2612
|
-
<>
|
|
2613
|
-
<span>·</span>
|
|
2614
|
-
<span>{w.phases.length} phases</span>
|
|
2615
|
-
</>
|
|
2616
|
-
)}
|
|
2617
|
-
</div>
|
|
2618
|
-
<div style={{ marginTop: 6, fontSize: 10, opacity: 0.55, fontStyle: "italic" }}>
|
|
2619
|
-
Click to open this run in the Workflows tab.
|
|
2620
|
-
</div>
|
|
2621
|
-
</div>
|
|
2622
|
-
);
|
|
1964
|
+
{dir === "up" ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
|
1965
|
+
{dir === "up" ? `End of ${label}` : `Start of ${label}`}
|
|
1966
|
+
</button>
|
|
1967
|
+
</div>
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function findColdResumeInDisplayRow(
|
|
1972
|
+
d: DisplayRow,
|
|
1973
|
+
): SessionEvent["coldResume"] | null {
|
|
1974
|
+
if (d.kind === "presentation") return findColdResumeInRow(d.row);
|
|
1975
|
+
if (d.kind === "turn-collapsed" || d.kind === "turn-expanded-header") {
|
|
1976
|
+
for (const r of d.turn.rows) {
|
|
1977
|
+
const hit = findColdResumeInRow(r);
|
|
1978
|
+
if (hit) return hit;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
function findColdResumeInRow(
|
|
1985
|
+
r: PresentationRow,
|
|
1986
|
+
): SessionEvent["coldResume"] | null {
|
|
1987
|
+
if (r.kind === "agent") {
|
|
1988
|
+
if (r.event.coldResume) return r.event.coldResume;
|
|
1989
|
+
for (const ge of r.groupedEvents) {
|
|
1990
|
+
if (ge.coldResume) return ge.coldResume;
|
|
1991
|
+
}
|
|
1992
|
+
} else if (r.kind === "tool-group") {
|
|
1993
|
+
for (const e of r.events) if (e.coldResume) return e.coldResume;
|
|
2623
1994
|
}
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
2624
1997
|
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
1998
|
+
function ColdResumeNotice({
|
|
1999
|
+
info,
|
|
2000
|
+
model,
|
|
2001
|
+
}: {
|
|
2002
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
2003
|
+
model?: string;
|
|
2004
|
+
}) {
|
|
2005
|
+
const { trigger, gapMs, writeTokens, writeRatio, compact } = info;
|
|
2006
|
+
const estUsd = estimateCost(
|
|
2007
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2008
|
+
model,
|
|
2009
|
+
);
|
|
2010
|
+
const isCompact = trigger === "compact";
|
|
2011
|
+
const fullyCold = writeRatio >= 0.9;
|
|
2012
|
+
let title: string;
|
|
2013
|
+
if (!isCompact) title = `Session resumed cold · idle ${formatGap(gapMs)}`;
|
|
2014
|
+
else if (compact?.trigger === "auto") title = "Auto-compact rebuilt the cache";
|
|
2015
|
+
else title = "Conversation compacted · cache rebuilt";
|
|
2016
|
+
const hint = isCompact
|
|
2017
|
+
? "Compaction summarizes the conversation, so the prefix must be rewritten into a fresh cache. Any /compact or auto-compact will cost this rewrite."
|
|
2018
|
+
: "Prompt cache expired during idle. Resuming within 5 min keeps the cache warm and avoids the rewrite tax.";
|
|
2019
|
+
return (
|
|
2020
|
+
<div
|
|
2021
|
+
style={{
|
|
2022
|
+
padding: "8px 12px",
|
|
2023
|
+
background: "rgba(217, 119, 6, 0.08)",
|
|
2024
|
+
border: "1px solid rgba(217, 119, 6, 0.35)",
|
|
2025
|
+
borderLeft: "3px solid rgba(217, 119, 6, 0.9)",
|
|
2026
|
+
borderRadius: 6,
|
|
2027
|
+
fontSize: 11.5,
|
|
2028
|
+
color: "var(--af-text-secondary)",
|
|
2029
|
+
lineHeight: 1.5,
|
|
2030
|
+
}}
|
|
2031
|
+
>
|
|
2632
2032
|
<div
|
|
2633
2033
|
style={{
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
color: "#
|
|
2639
|
-
|
|
2640
|
-
padding: "12px 14px",
|
|
2641
|
-
fontSize: 11,
|
|
2642
|
-
pointerEvents: "none",
|
|
2643
|
-
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
2644
|
-
maxWidth: 440,
|
|
2645
|
-
minWidth: 280,
|
|
2646
|
-
lineHeight: 1.45,
|
|
2647
|
-
}}
|
|
2648
|
-
>
|
|
2649
|
-
<div
|
|
2650
|
-
style={{
|
|
2651
|
-
display: "flex",
|
|
2652
|
-
alignItems: "center",
|
|
2653
|
-
gap: 8,
|
|
2654
|
-
marginBottom: 8,
|
|
2655
|
-
}}
|
|
2656
|
-
>
|
|
2657
|
-
<span
|
|
2658
|
-
style={{
|
|
2659
|
-
fontSize: 10,
|
|
2660
|
-
fontWeight: 600,
|
|
2661
|
-
padding: "2px 8px",
|
|
2662
|
-
borderRadius: 4,
|
|
2663
|
-
background: subagentColor(subagent.agentType, subagent.runInBackground),
|
|
2664
|
-
color: "#fff",
|
|
2665
|
-
}}
|
|
2666
|
-
>
|
|
2667
|
-
{subagent.agentType}
|
|
2668
|
-
</span>
|
|
2669
|
-
{subagent.runInBackground && (
|
|
2670
|
-
<span
|
|
2671
|
-
style={{
|
|
2672
|
-
fontSize: 9,
|
|
2673
|
-
fontWeight: 600,
|
|
2674
|
-
padding: "1px 6px",
|
|
2675
|
-
borderRadius: 3,
|
|
2676
|
-
background: "rgba(251, 191, 36, 0.18)",
|
|
2677
|
-
color: "#FBBF24",
|
|
2678
|
-
textTransform: "uppercase",
|
|
2679
|
-
letterSpacing: "0.04em",
|
|
2680
|
-
}}
|
|
2681
|
-
>
|
|
2682
|
-
background
|
|
2683
|
-
</span>
|
|
2684
|
-
)}
|
|
2685
|
-
<span
|
|
2686
|
-
style={{
|
|
2687
|
-
fontFamily: "var(--font-mono)",
|
|
2688
|
-
fontSize: 10,
|
|
2689
|
-
opacity: 0.7,
|
|
2690
|
-
marginLeft: "auto",
|
|
2691
|
-
}}
|
|
2692
|
-
>
|
|
2693
|
-
{startOff} → {endOff} · {dur}
|
|
2694
|
-
</span>
|
|
2695
|
-
</div>
|
|
2696
|
-
<div style={{ fontWeight: 500, marginBottom: 6 }}>{subagent.description}</div>
|
|
2697
|
-
<div
|
|
2698
|
-
style={{
|
|
2699
|
-
fontSize: 10,
|
|
2700
|
-
opacity: 0.65,
|
|
2701
|
-
display: "flex",
|
|
2702
|
-
gap: 8,
|
|
2703
|
-
paddingTop: 6,
|
|
2704
|
-
borderTop: "1px solid rgba(241,245,249,0.08)",
|
|
2705
|
-
}}
|
|
2706
|
-
>
|
|
2707
|
-
<span>{subagent.eventCount} events</span>
|
|
2708
|
-
<span>·</span>
|
|
2709
|
-
<span style={{ fontFamily: "var(--font-mono)" }}>
|
|
2710
|
-
{formatTokens(totalIn)}/{formatTokens(subagent.totalUsage.output)} tok
|
|
2711
|
-
</span>
|
|
2712
|
-
</div>
|
|
2713
|
-
{subagent.finalPreview && (
|
|
2714
|
-
<div
|
|
2715
|
-
style={{
|
|
2716
|
-
marginTop: 6,
|
|
2717
|
-
opacity: 0.78,
|
|
2718
|
-
fontStyle: "italic",
|
|
2719
|
-
display: "-webkit-box",
|
|
2720
|
-
WebkitLineClamp: 2,
|
|
2721
|
-
WebkitBoxOrient: "vertical",
|
|
2722
|
-
overflow: "hidden",
|
|
2723
|
-
}}
|
|
2724
|
-
>
|
|
2725
|
-
→ {subagent.finalPreview}
|
|
2726
|
-
</div>
|
|
2727
|
-
)}
|
|
2728
|
-
</div>
|
|
2729
|
-
);
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
if (cold) {
|
|
2733
|
-
const { trigger, gapMs, writeTokens, compact } = cold.info;
|
|
2734
|
-
const isCompact = trigger === "compact";
|
|
2735
|
-
const estUsd = estimateCost(
|
|
2736
|
-
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2737
|
-
model,
|
|
2738
|
-
);
|
|
2739
|
-
let label: string;
|
|
2740
|
-
if (!isCompact) label = "⚡ CACHE REBUILD";
|
|
2741
|
-
else if (compact?.trigger === "auto") label = "⚡ AUTO-COMPACT";
|
|
2742
|
-
else label = "⚡ /COMPACT";
|
|
2743
|
-
const detailLine = isCompact
|
|
2744
|
-
? `${formatTokens(writeTokens)} rewritten · pre-compact ${formatTokens(compact?.preTokens ?? 0)}`
|
|
2745
|
-
: `${formatTokens(writeTokens)} rewritten · idle ${formatGap(gapMs)}`;
|
|
2746
|
-
const hint = isCompact
|
|
2747
|
-
? "Conversation was summarized; prefix had to be rewritten into a fresh cache."
|
|
2748
|
-
: "Prompt cache expired during idle; resuming within 5 min keeps it warm.";
|
|
2749
|
-
return (
|
|
2750
|
-
<div
|
|
2751
|
-
style={{
|
|
2752
|
-
position: "absolute",
|
|
2753
|
-
...posStyle,
|
|
2754
|
-
zIndex: 100,
|
|
2755
|
-
background: "#0F172A",
|
|
2756
|
-
color: "#F1F5F9",
|
|
2757
|
-
borderRadius: 10,
|
|
2758
|
-
padding: "10px 14px",
|
|
2759
|
-
fontSize: 11,
|
|
2760
|
-
pointerEvents: "none",
|
|
2761
|
-
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
2762
|
-
maxWidth: 340,
|
|
2763
|
-
minWidth: 220,
|
|
2764
|
-
lineHeight: 1.45,
|
|
2034
|
+
display: "flex",
|
|
2035
|
+
alignItems: "center",
|
|
2036
|
+
gap: 8,
|
|
2037
|
+
fontWeight: 600,
|
|
2038
|
+
color: "#78350F",
|
|
2039
|
+
letterSpacing: "0.01em",
|
|
2765
2040
|
}}
|
|
2766
2041
|
>
|
|
2767
|
-
<
|
|
2768
|
-
|
|
2769
|
-
style={{
|
|
2770
|
-
fontSize: 10,
|
|
2771
|
-
fontWeight: 600,
|
|
2772
|
-
padding: "2px 8px",
|
|
2773
|
-
borderRadius: 4,
|
|
2774
|
-
background: "#D97706",
|
|
2775
|
-
color: "#fff",
|
|
2776
|
-
}}
|
|
2777
|
-
>
|
|
2778
|
-
{label}
|
|
2779
|
-
</span>
|
|
2780
|
-
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.7 }}>
|
|
2781
|
-
at {formatOffset(cold.tOffsetMs)}
|
|
2782
|
-
</span>
|
|
2783
|
-
</div>
|
|
2784
|
-
<div style={{ fontWeight: 500, fontFamily: "var(--font-mono)" }}>
|
|
2785
|
-
{detailLine}
|
|
2786
|
-
{estUsd >= 0.005 && ` · est. ${formatCost(estUsd)}`}
|
|
2787
|
-
</div>
|
|
2788
|
-
<div
|
|
2789
|
-
style={{
|
|
2790
|
-
marginTop: 4,
|
|
2791
|
-
fontSize: 10,
|
|
2792
|
-
opacity: 0.65,
|
|
2793
|
-
fontStyle: "italic",
|
|
2794
|
-
whiteSpace: "normal",
|
|
2795
|
-
}}
|
|
2796
|
-
>
|
|
2797
|
-
{hint}
|
|
2798
|
-
</div>
|
|
2042
|
+
<span style={{ fontSize: 13 }}>⚡</span>
|
|
2043
|
+
{title}
|
|
2799
2044
|
</div>
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
pointerEvents: "none",
|
|
2821
|
-
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
2822
|
-
maxWidth: 360,
|
|
2823
|
-
minWidth: 200,
|
|
2824
|
-
lineHeight: 1.45,
|
|
2825
|
-
}}
|
|
2826
|
-
>
|
|
2827
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
|
2828
|
-
<span
|
|
2829
|
-
style={{
|
|
2830
|
-
fontSize: 10,
|
|
2831
|
-
fontWeight: 600,
|
|
2832
|
-
padding: "2px 8px",
|
|
2833
|
-
borderRadius: 4,
|
|
2834
|
-
background: "#8b5cf6",
|
|
2835
|
-
color: "#fff",
|
|
2836
|
-
}}
|
|
2837
|
-
>
|
|
2838
|
-
PR
|
|
2839
|
-
</span>
|
|
2840
|
-
{pr.tOffsetMs !== undefined && (
|
|
2841
|
-
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.7 }}>
|
|
2842
|
-
at {formatOffset(pr.tOffsetMs)}
|
|
2843
|
-
</span>
|
|
2844
|
-
)}
|
|
2845
|
-
</div>
|
|
2846
|
-
<div style={{ fontWeight: 500 }}>{label}</div>
|
|
2045
|
+
<div style={{ marginTop: 4, fontFamily: "var(--font-mono)", fontSize: 11 }}>
|
|
2046
|
+
Rewrote <b>{formatTokens(writeTokens)}</b> tokens into prompt cache
|
|
2047
|
+
{estUsd >= 0.005 && (
|
|
2048
|
+
<>
|
|
2049
|
+
{" "}
|
|
2050
|
+
· est. <b>{formatCost(estUsd)}</b>
|
|
2051
|
+
</>
|
|
2052
|
+
)}
|
|
2053
|
+
{isCompact && compact && (
|
|
2054
|
+
<>
|
|
2055
|
+
{" "}
|
|
2056
|
+
· pre-compact <b>{formatTokens(compact.preTokens)}</b>
|
|
2057
|
+
</>
|
|
2058
|
+
)}
|
|
2059
|
+
{!isCompact && (
|
|
2060
|
+
<>
|
|
2061
|
+
{" "}
|
|
2062
|
+
· {fullyCold ? "fully cold" : "partial"} ({Math.round(writeRatio * 100)}% write)
|
|
2063
|
+
</>
|
|
2064
|
+
)}
|
|
2847
2065
|
</div>
|
|
2848
|
-
);
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
if (idleMs !== undefined) {
|
|
2852
|
-
return (
|
|
2853
2066
|
<div
|
|
2854
2067
|
style={{
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
color: "#F1F5F9",
|
|
2860
|
-
borderRadius: 10,
|
|
2861
|
-
padding: "8px 12px",
|
|
2862
|
-
fontSize: 11,
|
|
2863
|
-
pointerEvents: "none",
|
|
2864
|
-
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
2068
|
+
marginTop: 3,
|
|
2069
|
+
fontSize: 10.5,
|
|
2070
|
+
color: "var(--af-text-tertiary)",
|
|
2071
|
+
fontStyle: "italic",
|
|
2865
2072
|
}}
|
|
2866
2073
|
>
|
|
2867
|
-
|
|
2868
|
-
<div style={{ opacity: 0.78, fontFamily: "var(--font-mono)" }}>{formatGap(idleMs)}</div>
|
|
2074
|
+
{hint}
|
|
2869
2075
|
</div>
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
if (turn) {
|
|
2874
|
-
const theme = ROLE_THEMES.agent;
|
|
2875
|
-
const s = turn.summary;
|
|
2876
|
-
const dur = turn.durationMs !== undefined ? formatGap(turn.durationMs) : "";
|
|
2877
|
-
const startOff = formatOffset(turn.tOffsetMs);
|
|
2878
|
-
const endOff =
|
|
2879
|
-
turn.tOffsetMs !== undefined && turn.durationMs !== undefined
|
|
2880
|
-
? formatOffset(turn.tOffsetMs + turn.durationMs)
|
|
2881
|
-
: undefined;
|
|
2882
|
-
const hasFirstLast =
|
|
2883
|
-
s.firstAgentPreview && s.finalAgentPreview && s.firstAgentPreview !== s.finalAgentPreview;
|
|
2884
|
-
// Top tools aggregated summary line (up to 3 entries)
|
|
2885
|
-
const topTools = s.toolNames
|
|
2886
|
-
.slice(0, 3)
|
|
2887
|
-
.map((t) =>
|
|
2888
|
-
t.count > 1 ? `${shortenToolName(t.name)} ×${t.count}` : shortenToolName(t.name),
|
|
2889
|
-
)
|
|
2890
|
-
.join(" · ");
|
|
2891
|
-
|
|
2892
|
-
return (
|
|
2893
|
-
<div
|
|
2894
|
-
style={{
|
|
2895
|
-
position: "absolute",
|
|
2896
|
-
...posStyle,
|
|
2897
|
-
zIndex: 100,
|
|
2898
|
-
background: "#0F172A",
|
|
2899
|
-
color: "#F1F5F9",
|
|
2900
|
-
borderRadius: 10,
|
|
2901
|
-
padding: "12px 14px",
|
|
2902
|
-
fontSize: 11,
|
|
2903
|
-
pointerEvents: "none",
|
|
2904
|
-
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
2905
|
-
maxWidth: 440,
|
|
2906
|
-
minWidth: 280,
|
|
2907
|
-
lineHeight: 1.45,
|
|
2908
|
-
}}
|
|
2909
|
-
>
|
|
2910
|
-
{/* Header: Turn pill + start→end range */}
|
|
2911
|
-
<div
|
|
2912
|
-
style={{
|
|
2913
|
-
display: "flex",
|
|
2914
|
-
alignItems: "center",
|
|
2915
|
-
gap: 8,
|
|
2916
|
-
marginBottom: 8,
|
|
2917
|
-
}}
|
|
2918
|
-
>
|
|
2919
|
-
<span
|
|
2920
|
-
style={{
|
|
2921
|
-
fontSize: 10,
|
|
2922
|
-
fontWeight: 600,
|
|
2923
|
-
padding: "2px 8px",
|
|
2924
|
-
borderRadius: 4,
|
|
2925
|
-
background: theme.mini,
|
|
2926
|
-
color: "#fff",
|
|
2927
|
-
}}
|
|
2928
|
-
>
|
|
2929
|
-
Turn
|
|
2930
|
-
</span>
|
|
2931
|
-
<span
|
|
2932
|
-
style={{
|
|
2933
|
-
fontFamily: "var(--font-mono)",
|
|
2934
|
-
fontSize: 10,
|
|
2935
|
-
opacity: 0.7,
|
|
2936
|
-
}}
|
|
2937
|
-
>
|
|
2938
|
-
{endOff ? `${startOff} → ${endOff}` : startOff}
|
|
2939
|
-
</span>
|
|
2940
|
-
{dur && (
|
|
2941
|
-
<span
|
|
2942
|
-
style={{
|
|
2943
|
-
fontFamily: "var(--font-mono)",
|
|
2944
|
-
fontSize: 10,
|
|
2945
|
-
opacity: 0.55,
|
|
2946
|
-
}}
|
|
2947
|
-
>
|
|
2948
|
-
· {dur}
|
|
2949
|
-
</span>
|
|
2950
|
-
)}
|
|
2951
|
-
</div>
|
|
2952
|
-
|
|
2953
|
-
{/* First agent message */}
|
|
2954
|
-
{s.firstAgentPreview && (
|
|
2955
|
-
<div
|
|
2956
|
-
style={{
|
|
2957
|
-
opacity: 0.95,
|
|
2958
|
-
fontWeight: 500,
|
|
2959
|
-
display: "-webkit-box",
|
|
2960
|
-
WebkitLineClamp: 2,
|
|
2961
|
-
WebkitBoxOrient: "vertical",
|
|
2962
|
-
overflow: "hidden",
|
|
2963
|
-
marginBottom: 6,
|
|
2964
|
-
}}
|
|
2965
|
-
>
|
|
2966
|
-
{s.firstAgentPreview}
|
|
2967
|
-
</div>
|
|
2968
|
-
)}
|
|
2076
|
+
</div>
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2969
2079
|
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2080
|
+
function ColdResumeSessionStat({
|
|
2081
|
+
count,
|
|
2082
|
+
writeTokens,
|
|
2083
|
+
model,
|
|
2084
|
+
}: {
|
|
2085
|
+
count: number;
|
|
2086
|
+
writeTokens: number;
|
|
2087
|
+
model?: string;
|
|
2088
|
+
}) {
|
|
2089
|
+
const { ref, anchor, open, close } = useAnchoredTooltip();
|
|
2090
|
+
const estUsd = estimateCost(
|
|
2091
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2092
|
+
model,
|
|
2093
|
+
);
|
|
2094
|
+
return (
|
|
2095
|
+
<span
|
|
2096
|
+
ref={ref}
|
|
2097
|
+
style={{
|
|
2098
|
+
display: "inline-flex",
|
|
2099
|
+
alignItems: "center",
|
|
2100
|
+
gap: 5,
|
|
2101
|
+
fontSize: 11.5,
|
|
2102
|
+
fontWeight: 600,
|
|
2103
|
+
color: "#78350F",
|
|
2104
|
+
background: "rgba(217, 119, 6, 0.12)",
|
|
2105
|
+
padding: "2px 8px",
|
|
2106
|
+
borderRadius: 100,
|
|
2107
|
+
cursor: "default",
|
|
2108
|
+
}}
|
|
2109
|
+
onMouseEnter={open}
|
|
2110
|
+
onMouseLeave={close}
|
|
2111
|
+
>
|
|
2112
|
+
<span style={{ fontSize: 12 }}>⚡</span>
|
|
2113
|
+
{count} cache rebuild{count === 1 ? "" : "s"} · {formatTokens(writeTokens)} rewritten
|
|
2114
|
+
{anchor && (
|
|
2115
|
+
<AnchoredTooltip anchor={anchor} width={320}>
|
|
2116
|
+
<TooltipRow label="Cache rebuilds" value={count.toLocaleString()} />
|
|
2117
|
+
<TooltipRow label="Tokens rewritten" value={writeTokens.toLocaleString()} />
|
|
2118
|
+
{estUsd >= 0.005 && (
|
|
2119
|
+
<TooltipRow label="Est. rebuild cost" value={formatCost(estUsd)} />
|
|
2990
2120
|
)}
|
|
2991
|
-
</div>
|
|
2992
|
-
|
|
2993
|
-
{/* Last agent message */}
|
|
2994
|
-
{hasFirstLast && s.finalAgentPreview && (
|
|
2995
2121
|
<div
|
|
2996
2122
|
style={{
|
|
2997
|
-
|
|
2998
|
-
display: "flex",
|
|
2999
|
-
gap: 6,
|
|
3000
|
-
alignItems: "flex-start",
|
|
3001
|
-
borderTop: "1px solid rgba(241,245,249,0.08)",
|
|
2123
|
+
marginTop: 6,
|
|
3002
2124
|
paddingTop: 6,
|
|
2125
|
+
borderTop: "1px solid rgba(241,245,249,0.12)",
|
|
2126
|
+
opacity: 0.65,
|
|
2127
|
+
fontSize: 10,
|
|
2128
|
+
whiteSpace: "normal",
|
|
2129
|
+
lineHeight: 1.4,
|
|
3003
2130
|
}}
|
|
3004
2131
|
>
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
WebkitLineClamp: 2,
|
|
3010
|
-
WebkitBoxOrient: "vertical",
|
|
3011
|
-
overflow: "hidden",
|
|
3012
|
-
}}
|
|
3013
|
-
>
|
|
3014
|
-
{s.finalAgentPreview}
|
|
3015
|
-
</span>
|
|
2132
|
+
Turns where the whole prompt-cache prefix had to be rewritten.
|
|
2133
|
+
Either the cache expired during a long idle gap, or a /compact
|
|
2134
|
+
(manual or auto) summarized the conversation. Both cost tokens
|
|
2135
|
+
at 1.25× base input price.
|
|
3016
2136
|
</div>
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
if (!row) return null;
|
|
3023
|
-
|
|
3024
|
-
const theme = ROLE_THEMES[row.kind];
|
|
3025
|
-
const preview = rowPreview(row);
|
|
2137
|
+
</AnchoredTooltip>
|
|
2138
|
+
)}
|
|
2139
|
+
</span>
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
3026
2142
|
|
|
2143
|
+
function ColdResumeChip({
|
|
2144
|
+
info,
|
|
2145
|
+
model,
|
|
2146
|
+
}: {
|
|
2147
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
2148
|
+
model?: string;
|
|
2149
|
+
}) {
|
|
2150
|
+
const { writeTokens } = info;
|
|
2151
|
+
const estUsd = estimateCost(
|
|
2152
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2153
|
+
model,
|
|
2154
|
+
);
|
|
3027
2155
|
return (
|
|
3028
|
-
<
|
|
2156
|
+
<span
|
|
2157
|
+
title={`Prompt cache expired during idle; this turn rewrote ${writeTokens.toLocaleString()} tokens into cache${estUsd >= 0.005 ? ` (est. ${formatCost(estUsd)})` : ""}.`}
|
|
3029
2158
|
style={{
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
minWidth: 220,
|
|
2159
|
+
display: "inline-flex",
|
|
2160
|
+
alignItems: "center",
|
|
2161
|
+
gap: 4,
|
|
2162
|
+
fontSize: 10.5,
|
|
2163
|
+
fontWeight: 600,
|
|
2164
|
+
padding: "2px 7px",
|
|
2165
|
+
borderRadius: 3,
|
|
2166
|
+
background: "rgba(217, 119, 6, 0.14)",
|
|
2167
|
+
color: "#78350F",
|
|
2168
|
+
letterSpacing: "0.01em",
|
|
2169
|
+
cursor: "default",
|
|
3042
2170
|
}}
|
|
3043
2171
|
>
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
display: "flex",
|
|
3047
|
-
alignItems: "center",
|
|
3048
|
-
gap: 8,
|
|
3049
|
-
marginBottom: 6,
|
|
3050
|
-
}}
|
|
3051
|
-
>
|
|
3052
|
-
<span
|
|
3053
|
-
style={{
|
|
3054
|
-
fontSize: 10,
|
|
3055
|
-
fontWeight: 600,
|
|
3056
|
-
padding: "2px 8px",
|
|
3057
|
-
borderRadius: 4,
|
|
3058
|
-
background: theme.mini,
|
|
3059
|
-
color: "#fff",
|
|
3060
|
-
}}
|
|
3061
|
-
>
|
|
3062
|
-
{theme.label}
|
|
3063
|
-
</span>
|
|
3064
|
-
<span
|
|
3065
|
-
style={{
|
|
3066
|
-
fontFamily: "var(--font-mono)",
|
|
3067
|
-
fontSize: 10,
|
|
3068
|
-
opacity: 0.65,
|
|
3069
|
-
}}
|
|
3070
|
-
>
|
|
3071
|
-
{formatOffset(row.tOffsetMs)}
|
|
3072
|
-
</span>
|
|
3073
|
-
</div>
|
|
3074
|
-
<div
|
|
3075
|
-
style={{
|
|
3076
|
-
lineHeight: 1.45,
|
|
3077
|
-
opacity: 0.92,
|
|
3078
|
-
display: "-webkit-box",
|
|
3079
|
-
WebkitLineClamp: 3,
|
|
3080
|
-
WebkitBoxOrient: "vertical",
|
|
3081
|
-
overflow: "hidden",
|
|
3082
|
-
}}
|
|
3083
|
-
>
|
|
3084
|
-
{preview}
|
|
3085
|
-
</div>
|
|
3086
|
-
</div>
|
|
2172
|
+
⚡ cold resume · {formatTokens(writeTokens)} rewritten
|
|
2173
|
+
</span>
|
|
3087
2174
|
);
|
|
3088
2175
|
}
|
|
3089
2176
|
|
|
3090
2177
|
/* ------------------------------------------------------------------ */
|
|
3091
|
-
/*
|
|
2178
|
+
/* Collapsed turn row */
|
|
2179
|
+
/* */
|
|
2180
|
+
/* Three-line layout: */
|
|
2181
|
+
/* 1. First agent preview (intent / plan) */
|
|
2182
|
+
/* 2. Middle summary (counts + top tools + duration) */
|
|
2183
|
+
/* 3. Final agent preview (answer / conclusion) */
|
|
3092
2184
|
/* ------------------------------------------------------------------ */
|
|
3093
2185
|
|
|
3094
|
-
function
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
onSelect,
|
|
3099
|
-
onToggleTurn,
|
|
2186
|
+
function CollapsedTurnRow({
|
|
2187
|
+
turn,
|
|
2188
|
+
onClick,
|
|
2189
|
+
refCb,
|
|
3100
2190
|
stickyOffset,
|
|
3101
|
-
|
|
2191
|
+
inProgress,
|
|
3102
2192
|
team,
|
|
2193
|
+
coldResume,
|
|
3103
2194
|
model,
|
|
3104
|
-
rawIdleBands,
|
|
3105
|
-
sessionDays,
|
|
3106
|
-
entryByDay,
|
|
3107
|
-
dayStripRefs,
|
|
3108
|
-
onJumpAcrossDay,
|
|
3109
2195
|
}: {
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
2196
|
+
turn: TurnMegaRow;
|
|
2197
|
+
/** Fires when the user wants to fully expand this turn into the
|
|
2198
|
+
* transcript list (inner rows appear below as separate TranscriptRow
|
|
2199
|
+
* entries). Triggered by the "Show all N steps" bottom bar. */
|
|
2200
|
+
onClick: () => void;
|
|
2201
|
+
refCb: (el: HTMLDivElement | null) => void;
|
|
3115
2202
|
stickyOffset: number;
|
|
3116
|
-
|
|
2203
|
+
inProgress?: boolean;
|
|
3117
2204
|
team?: (TimelineData & { teamName: string }) | null;
|
|
2205
|
+
coldResume?: NonNullable<SessionEvent["coldResume"]>;
|
|
3118
2206
|
model?: string;
|
|
3119
|
-
/** Anchor-to-anchor idle bands. The body emits IdleDivider before any
|
|
3120
|
-
* row whose start matches a band's end, so Codex sessions get the same
|
|
3121
|
-
* "Session idle X minutes" markers Claude has always had. */
|
|
3122
|
-
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
3123
|
-
/** Day buckets (sorted by startMs) + per-day digest cards. The body drops a
|
|
3124
|
-
* card at each day's first row so per-day perception sits inline with that
|
|
3125
|
-
* day's work instead of stacked at the top. */
|
|
3126
|
-
sessionDays: { key: string; startMs: number; endMs: number; count: number }[];
|
|
3127
|
-
entryByDay: Map<string, Entry>;
|
|
3128
|
-
dayStripRefs: React.MutableRefObject<Record<string, HTMLDivElement | null>>;
|
|
3129
|
-
/** Hop to an adjacent day from an inline day-boundary button.
|
|
3130
|
-
* (selectKey, scrollKey, block, fallbackRowIndex). */
|
|
3131
|
-
onJumpAcrossDay: (
|
|
3132
|
-
selectKey: string,
|
|
3133
|
-
scrollKey: string,
|
|
3134
|
-
block: "start" | "end",
|
|
3135
|
-
fallbackIndex: number,
|
|
3136
|
-
) => void;
|
|
3137
2207
|
}) {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
if (isSessionLive) {
|
|
3142
|
-
for (let i = displayRows.length - 1; i >= 0; i--) {
|
|
3143
|
-
if (displayRows[i].kind === "turn-collapsed") {
|
|
3144
|
-
lastCollapsedTurnIdx = i;
|
|
3145
|
-
break;
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
2208
|
+
const theme = ROLE_THEMES.agent;
|
|
2209
|
+
const s = turn.summary;
|
|
2210
|
+
const hasTokens = s.totalTokens.input > 0 || s.totalTokens.output > 0;
|
|
3149
2211
|
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
if (Math.abs(b.end - tOff) < 1000) return b;
|
|
3160
|
-
}
|
|
3161
|
-
return null;
|
|
3162
|
-
};
|
|
3163
|
-
/** Pull a t-offset off whichever display-row variant we have. */
|
|
3164
|
-
const rowTOffset = (d: DisplayRow): number | undefined => {
|
|
3165
|
-
if (d.kind === "turn-collapsed" || d.kind === "turn-expanded-header") return d.turn.tOffsetMs;
|
|
3166
|
-
if (d.kind === "turn-expanded-footer") return undefined;
|
|
3167
|
-
return d.row.tOffsetMs;
|
|
3168
|
-
};
|
|
3169
|
-
// A display row → its event PRIMARY index (what jumpAcrossDay's turn/row
|
|
3170
|
-
// lookups are keyed by — NOT the displayRows loop position).
|
|
3171
|
-
const rowPrimaryIndexOf = (d: DisplayRow): number =>
|
|
3172
|
-
d.kind === "presentation" ? rowPrimaryIndex(d.row) : d.turn.firstPrimaryIndex;
|
|
3173
|
-
/** Which local day a row falls in: the last day bucket whose start is at or
|
|
3174
|
-
* before the row's offset. sessionDays is pre-sorted by startMs. */
|
|
3175
|
-
const dayKeyForOffset = (tOff: number | undefined): string | null => {
|
|
3176
|
-
if (tOff === undefined) return null;
|
|
3177
|
-
let key: string | null = null;
|
|
3178
|
-
for (const dd of sessionDays) {
|
|
3179
|
-
if (dd.startMs <= tOff) key = dd.key;
|
|
3180
|
-
else break;
|
|
3181
|
-
}
|
|
3182
|
-
return key;
|
|
3183
|
-
};
|
|
2212
|
+
// First / conclusion agent-message indices are pre-computed by
|
|
2213
|
+
// buildMegaRows — the "conclusion" uses a heuristic that skips short
|
|
2214
|
+
// codas following a task-notification (so e.g. "Ship done..." beats
|
|
2215
|
+
// a later "Acknowledged — the background task closed out").
|
|
2216
|
+
const firstAgentIdx = s.firstAgentIndex ?? -1;
|
|
2217
|
+
const finalAgentIdx = s.finalAgentIndex ?? -1;
|
|
2218
|
+
const firstAgentRow = firstAgentIdx >= 0 ? turn.rows[firstAgentIdx] : undefined;
|
|
2219
|
+
const finalAgentRow =
|
|
2220
|
+
finalAgentIdx >= 0 && finalAgentIdx !== firstAgentIdx ? turn.rows[finalAgentIdx] : undefined;
|
|
3184
2221
|
|
|
3185
|
-
|
|
3186
|
-
//
|
|
3187
|
-
//
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
const emittedBandStarts = new Set<number>();
|
|
3194
|
-
for (let i = 0; i < displayRows.length; i++) {
|
|
3195
|
-
const d = displayRows[i];
|
|
3196
|
-
// Emit a "Session idle" divider before any row that starts right after
|
|
3197
|
-
// an anchor-to-anchor idle band. Skips indented (child) rows so an
|
|
3198
|
-
// expanded turn doesn't render dividers for its inner steps.
|
|
3199
|
-
const indented = d.kind === "presentation" ? d.indented : false;
|
|
3200
|
-
if (!indented) {
|
|
3201
|
-
const band = idleBandBeforeOffset(rowTOffset(d));
|
|
3202
|
-
if (
|
|
3203
|
-
band !== null &&
|
|
3204
|
-
band.durationMs > IDLE_THRESHOLD_MS &&
|
|
3205
|
-
!emittedBandStarts.has(band.start)
|
|
3206
|
-
) {
|
|
3207
|
-
emittedBandStarts.add(band.start);
|
|
3208
|
-
out.push(<IdleDivider key={`idle-before-${i}`} gapMs={band.durationMs} />);
|
|
3209
|
-
}
|
|
3210
|
-
// Day boundary: drop this day's digest card at its first row (after any
|
|
3211
|
-
// overnight idle divider that led into the day) so per-day perception
|
|
3212
|
-
// sits inline with that day's work, not stacked at the top. The boundary
|
|
3213
|
-
// also gets inline jump controls so you can page across days while
|
|
3214
|
-
// reading — "↓ start of next day" sits at the bottom of the day that just
|
|
3215
|
-
// ended, "↑ end of previous day" at the top of the new one.
|
|
3216
|
-
const dk = dayKeyForOffset(rowTOffset(d));
|
|
3217
|
-
if (dk && dk !== currentDayKey) {
|
|
3218
|
-
const endedKey = currentDayKey; // day that just ended; null on the first day
|
|
3219
|
-
currentDayKey = dk;
|
|
3220
|
-
// Fallback scroll target for days with no digest card: the day's first
|
|
3221
|
-
// row by PRIMARY index (i is the displayRows position — wrong key space).
|
|
3222
|
-
const firstIdx = rowPrimaryIndexOf(d);
|
|
3223
|
-
if (endedKey) {
|
|
3224
|
-
out.push(
|
|
3225
|
-
<DayJumpRow
|
|
3226
|
-
key={`daydown-${dk}`}
|
|
3227
|
-
dir="down"
|
|
3228
|
-
label={formatDayKey(dk)}
|
|
3229
|
-
onClick={() => onJumpAcrossDay(dk, dk, "start", firstIdx)}
|
|
3230
|
-
/>,
|
|
3231
|
-
);
|
|
3232
|
-
}
|
|
3233
|
-
const dayEntry = entryByDay.get(dk);
|
|
3234
|
-
if (dayEntry) {
|
|
3235
|
-
out.push(
|
|
3236
|
-
<div
|
|
3237
|
-
key={`daystrip-${dk}`}
|
|
3238
|
-
ref={(el) => {
|
|
3239
|
-
dayStripRefs.current[dk] = el;
|
|
3240
|
-
}}
|
|
3241
|
-
style={{ marginBottom: 12, scrollMarginTop: stickyOffset }}
|
|
3242
|
-
>
|
|
3243
|
-
<EntryDayStrip entry={dayEntry} />
|
|
3244
|
-
</div>,
|
|
3245
|
-
);
|
|
3246
|
-
}
|
|
3247
|
-
if (endedKey) {
|
|
3248
|
-
out.push(
|
|
3249
|
-
<DayJumpRow
|
|
3250
|
-
key={`dayup-${dk}`}
|
|
3251
|
-
dir="up"
|
|
3252
|
-
label={formatDayKey(endedKey)}
|
|
3253
|
-
onClick={() => onJumpAcrossDay(endedKey, dk, "end", firstIdx)}
|
|
3254
|
-
/>,
|
|
3255
|
-
);
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
2222
|
+
// Middle list excludes the first and conclusion messages. Any coda
|
|
2223
|
+
// messages (e.g. "Acknowledged ...") remain visible here as ordinary
|
|
2224
|
+
// steps — they happened, they just aren't the semantic conclusion.
|
|
2225
|
+
const middleRows = turn.rows.filter((_, i) => {
|
|
2226
|
+
if (i === firstAgentIdx) return false;
|
|
2227
|
+
if (finalAgentRow && i === finalAgentIdx) return false;
|
|
2228
|
+
return true;
|
|
2229
|
+
});
|
|
3259
2230
|
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
out.push(
|
|
3265
|
-
<CollapsedTurnRow
|
|
3266
|
-
key={`turn-${idx}`}
|
|
3267
|
-
turn={d.turn}
|
|
3268
|
-
stickyOffset={stickyOffset}
|
|
3269
|
-
onClick={() => onToggleTurn(idx)}
|
|
3270
|
-
inProgress={i === lastCollapsedTurnIdx}
|
|
3271
|
-
team={team}
|
|
3272
|
-
coldResume={turnCold ?? undefined}
|
|
3273
|
-
model={model}
|
|
3274
|
-
refCb={(el) => {
|
|
3275
|
-
rowRefs.current[idx] = el;
|
|
3276
|
-
}}
|
|
3277
|
-
/>,
|
|
3278
|
-
);
|
|
3279
|
-
continue;
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
// Expanded turn header
|
|
3283
|
-
if (d.kind === "turn-expanded-header") {
|
|
3284
|
-
const turnCold = findColdResumeInDisplayRow(d);
|
|
3285
|
-
out.push(
|
|
3286
|
-
<ExpandedTurnHeader
|
|
3287
|
-
key={`turnhead-${d.turn.firstPrimaryIndex}`}
|
|
3288
|
-
turn={d.turn}
|
|
3289
|
-
onClick={() => onToggleTurn(d.turn.firstPrimaryIndex)}
|
|
3290
|
-
coldResume={turnCold ?? undefined}
|
|
3291
|
-
model={model}
|
|
3292
|
-
/>,
|
|
3293
|
-
);
|
|
3294
|
-
continue;
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
// Expanded turn footer — matching collapse control at the bottom of
|
|
3298
|
-
// the expanded rows so the user can collapse right where their eye
|
|
3299
|
-
// lands after reading through the content.
|
|
3300
|
-
if (d.kind === "turn-expanded-footer") {
|
|
3301
|
-
out.push(
|
|
3302
|
-
<ExpandedTurnFooter
|
|
3303
|
-
key={`turnfoot-${d.turn.firstPrimaryIndex}`}
|
|
3304
|
-
turn={d.turn}
|
|
3305
|
-
onClick={() => onToggleTurn(d.turn.firstPrimaryIndex)}
|
|
3306
|
-
/>,
|
|
3307
|
-
);
|
|
3308
|
-
continue;
|
|
3309
|
-
}
|
|
2231
|
+
// Local state: whether the first / last message is rendered in full
|
|
2232
|
+
// markdown form inline (instead of the 2-line preview).
|
|
2233
|
+
const [firstExpanded, setFirstExpanded] = useState(false);
|
|
2234
|
+
const [lastExpanded, setLastExpanded] = useState(false);
|
|
3310
2235
|
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
const idx = rowPrimaryIndex(r);
|
|
3316
|
-
out.push(
|
|
3317
|
-
<TranscriptRow
|
|
3318
|
-
key={`row-${idx}`}
|
|
3319
|
-
row={r}
|
|
3320
|
-
selected={selectedIndex === idx}
|
|
3321
|
-
onSelect={() => onSelect(idx)}
|
|
3322
|
-
stickyOffset={stickyOffset}
|
|
3323
|
-
indented={d.indented}
|
|
3324
|
-
refCb={(el) => {
|
|
3325
|
-
rowRefs.current[idx] = el;
|
|
3326
|
-
}}
|
|
3327
|
-
/>,
|
|
3328
|
-
);
|
|
3329
|
-
}
|
|
3330
|
-
return <div>{out}</div>;
|
|
3331
|
-
}
|
|
2236
|
+
// Stop click propagation on interactive children so clicks on the
|
|
2237
|
+
// expandable message areas don't bubble up and trigger onClick on the
|
|
2238
|
+
// whole row — we want explicit controls only.
|
|
2239
|
+
const stop = (e: React.MouseEvent) => e.stopPropagation();
|
|
3332
2240
|
|
|
3333
|
-
function IdleDivider({ gapMs }: { gapMs: number }) {
|
|
3334
2241
|
return (
|
|
3335
2242
|
<div
|
|
2243
|
+
ref={refCb}
|
|
2244
|
+
data-sl-row-index={turn.firstPrimaryIndex}
|
|
2245
|
+
data-sl-toffset={turn.tOffsetMs ?? 0}
|
|
3336
2246
|
style={{
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
color: "var(--af-text-tertiary)",
|
|
3346
|
-
letterSpacing: "0.02em",
|
|
2247
|
+
display: "grid",
|
|
2248
|
+
gridTemplateColumns: "20px 84px 1fr auto auto",
|
|
2249
|
+
columnGap: 12,
|
|
2250
|
+
alignItems: "start",
|
|
2251
|
+
padding: "14px 12px 0 12px",
|
|
2252
|
+
borderBottom: "1px solid var(--af-border-subtle)",
|
|
2253
|
+
transition: "background 0.08s",
|
|
2254
|
+
scrollMarginTop: stickyOffset,
|
|
3347
2255
|
}}
|
|
3348
2256
|
>
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
}
|
|
2257
|
+
{/* Col 1 — empty spacer to keep the Agent pill aligned with
|
|
2258
|
+
TranscriptRow's pill column (which also has an empty 20px slot). */}
|
|
2259
|
+
<span />
|
|
3353
2260
|
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
* "up" sits at the top of the new day and jumps to the end of the previous one.
|
|
3357
|
-
* The day always follows the scroll position (see followDayToScroll); these are
|
|
3358
|
-
* a one-click shortcut across the long idle gap between days so you don't have
|
|
3359
|
-
* to hand-scroll the whole span. "down" (start of next day) lands that day's
|
|
3360
|
-
* first row at the header line so the day-follow agrees; "up" (end of previous
|
|
3361
|
-
* day) scrolls the new day's strip to the viewport bottom, leaving the previous
|
|
3362
|
-
* day's tail above the header line. */
|
|
3363
|
-
function DayJumpRow({
|
|
3364
|
-
dir,
|
|
3365
|
-
label,
|
|
3366
|
-
onClick,
|
|
3367
|
-
}: {
|
|
3368
|
-
dir: "up" | "down";
|
|
3369
|
-
label: string;
|
|
3370
|
-
onClick: () => void;
|
|
3371
|
-
}) {
|
|
3372
|
-
return (
|
|
3373
|
-
<div style={{ display: "flex", justifyContent: "center", margin: "6px 0" }}>
|
|
3374
|
-
<button
|
|
3375
|
-
type="button"
|
|
3376
|
-
onClick={onClick}
|
|
3377
|
-
title={dir === "up" ? `Go to the end of ${label}` : `Go to the start of ${label}`}
|
|
2261
|
+
{/* Col 2 — "Agent" role pill */}
|
|
2262
|
+
<span
|
|
3378
2263
|
style={{
|
|
3379
|
-
|
|
3380
|
-
alignItems: "center",
|
|
3381
|
-
gap: 6,
|
|
2264
|
+
justifySelf: "start",
|
|
3382
2265
|
fontSize: 11,
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
cursor: "pointer",
|
|
2266
|
+
fontWeight: 600,
|
|
2267
|
+
padding: "3px 10px",
|
|
2268
|
+
borderRadius: 4,
|
|
2269
|
+
background: theme.bg,
|
|
2270
|
+
color: theme.fg,
|
|
2271
|
+
marginTop: 2,
|
|
3390
2272
|
}}
|
|
3391
2273
|
>
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
</button>
|
|
3395
|
-
</div>
|
|
3396
|
-
);
|
|
3397
|
-
}
|
|
3398
|
-
|
|
3399
|
-
function findColdResumeInDisplayRow(
|
|
3400
|
-
d: DisplayRow,
|
|
3401
|
-
): SessionEvent["coldResume"] | null {
|
|
3402
|
-
if (d.kind === "presentation") return findColdResumeInRow(d.row);
|
|
3403
|
-
if (d.kind === "turn-collapsed" || d.kind === "turn-expanded-header") {
|
|
3404
|
-
for (const r of d.turn.rows) {
|
|
3405
|
-
const hit = findColdResumeInRow(r);
|
|
3406
|
-
if (hit) return hit;
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
return null;
|
|
3410
|
-
}
|
|
3411
|
-
|
|
3412
|
-
function findColdResumeInRow(
|
|
3413
|
-
r: PresentationRow,
|
|
3414
|
-
): SessionEvent["coldResume"] | null {
|
|
3415
|
-
if (r.kind === "agent") {
|
|
3416
|
-
if (r.event.coldResume) return r.event.coldResume;
|
|
3417
|
-
for (const ge of r.groupedEvents) {
|
|
3418
|
-
if (ge.coldResume) return ge.coldResume;
|
|
3419
|
-
}
|
|
3420
|
-
} else if (r.kind === "tool-group") {
|
|
3421
|
-
for (const e of r.events) if (e.coldResume) return e.coldResume;
|
|
3422
|
-
}
|
|
3423
|
-
return null;
|
|
3424
|
-
}
|
|
2274
|
+
Agent
|
|
2275
|
+
</span>
|
|
3425
2276
|
|
|
3426
|
-
|
|
3427
|
-
info,
|
|
3428
|
-
model,
|
|
3429
|
-
}: {
|
|
3430
|
-
info: NonNullable<SessionEvent["coldResume"]>;
|
|
3431
|
-
model?: string;
|
|
3432
|
-
}) {
|
|
3433
|
-
const { trigger, gapMs, writeTokens, writeRatio, compact } = info;
|
|
3434
|
-
const estUsd = estimateCost(
|
|
3435
|
-
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
3436
|
-
model,
|
|
3437
|
-
);
|
|
3438
|
-
const isCompact = trigger === "compact";
|
|
3439
|
-
const fullyCold = writeRatio >= 0.9;
|
|
3440
|
-
let title: string;
|
|
3441
|
-
if (!isCompact) title = `Session resumed cold · idle ${formatGap(gapMs)}`;
|
|
3442
|
-
else if (compact?.trigger === "auto") title = "Auto-compact rebuilt the cache";
|
|
3443
|
-
else title = "Conversation compacted · cache rebuilt";
|
|
3444
|
-
const hint = isCompact
|
|
3445
|
-
? "Compaction summarizes the conversation, so the prefix must be rewritten into a fresh cache. Any /compact or auto-compact will cost this rewrite."
|
|
3446
|
-
: "Prompt cache expired during idle. Resuming within 5 min keeps the cache warm and avoids the rewrite tax.";
|
|
3447
|
-
return (
|
|
3448
|
-
<div
|
|
3449
|
-
style={{
|
|
3450
|
-
padding: "8px 12px",
|
|
3451
|
-
background: "rgba(217, 119, 6, 0.08)",
|
|
3452
|
-
border: "1px solid rgba(217, 119, 6, 0.35)",
|
|
3453
|
-
borderLeft: "3px solid rgba(217, 119, 6, 0.9)",
|
|
3454
|
-
borderRadius: 6,
|
|
3455
|
-
fontSize: 11.5,
|
|
3456
|
-
color: "var(--af-text-secondary)",
|
|
3457
|
-
lineHeight: 1.5,
|
|
3458
|
-
}}
|
|
3459
|
-
>
|
|
2277
|
+
{/* Col 3 — content (cold-resume notice · first · stats · steps · last · bottom bar) */}
|
|
3460
2278
|
<div
|
|
3461
2279
|
style={{
|
|
2280
|
+
minWidth: 0,
|
|
3462
2281
|
display: "flex",
|
|
3463
|
-
|
|
3464
|
-
gap:
|
|
3465
|
-
|
|
3466
|
-
color: "#78350F",
|
|
3467
|
-
letterSpacing: "0.01em",
|
|
2282
|
+
flexDirection: "column",
|
|
2283
|
+
gap: 10,
|
|
2284
|
+
paddingBottom: 10,
|
|
3468
2285
|
}}
|
|
3469
2286
|
>
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
Rewrote <b>{formatTokens(writeTokens)}</b> tokens into prompt cache
|
|
3475
|
-
{estUsd >= 0.005 && (
|
|
3476
|
-
<>
|
|
3477
|
-
{" "}
|
|
3478
|
-
· est. <b>{formatCost(estUsd)}</b>
|
|
3479
|
-
</>
|
|
3480
|
-
)}
|
|
3481
|
-
{isCompact && compact && (
|
|
3482
|
-
<>
|
|
3483
|
-
{" "}
|
|
3484
|
-
· pre-compact <b>{formatTokens(compact.preTokens)}</b>
|
|
3485
|
-
</>
|
|
3486
|
-
)}
|
|
3487
|
-
{!isCompact && (
|
|
3488
|
-
<>
|
|
3489
|
-
{" "}
|
|
3490
|
-
· {fullyCold ? "fully cold" : "partial"} ({Math.round(writeRatio * 100)}% write)
|
|
3491
|
-
</>
|
|
3492
|
-
)}
|
|
3493
|
-
</div>
|
|
3494
|
-
<div
|
|
3495
|
-
style={{
|
|
3496
|
-
marginTop: 3,
|
|
3497
|
-
fontSize: 10.5,
|
|
3498
|
-
color: "var(--af-text-tertiary)",
|
|
3499
|
-
fontStyle: "italic",
|
|
3500
|
-
}}
|
|
3501
|
-
>
|
|
3502
|
-
{hint}
|
|
3503
|
-
</div>
|
|
3504
|
-
</div>
|
|
3505
|
-
);
|
|
3506
|
-
}
|
|
3507
|
-
|
|
3508
|
-
function ColdResumeSessionStat({
|
|
3509
|
-
count,
|
|
3510
|
-
writeTokens,
|
|
3511
|
-
model,
|
|
3512
|
-
}: {
|
|
3513
|
-
count: number;
|
|
3514
|
-
writeTokens: number;
|
|
3515
|
-
model?: string;
|
|
3516
|
-
}) {
|
|
3517
|
-
const { ref, anchor, open, close } = useAnchoredTooltip();
|
|
3518
|
-
const estUsd = estimateCost(
|
|
3519
|
-
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
3520
|
-
model,
|
|
3521
|
-
);
|
|
3522
|
-
return (
|
|
3523
|
-
<span
|
|
3524
|
-
ref={ref}
|
|
3525
|
-
style={{
|
|
3526
|
-
display: "inline-flex",
|
|
3527
|
-
alignItems: "center",
|
|
3528
|
-
gap: 5,
|
|
3529
|
-
fontSize: 11.5,
|
|
3530
|
-
fontWeight: 600,
|
|
3531
|
-
color: "#78350F",
|
|
3532
|
-
background: "rgba(217, 119, 6, 0.12)",
|
|
3533
|
-
padding: "2px 8px",
|
|
3534
|
-
borderRadius: 100,
|
|
3535
|
-
cursor: "default",
|
|
3536
|
-
}}
|
|
3537
|
-
onMouseEnter={open}
|
|
3538
|
-
onMouseLeave={close}
|
|
3539
|
-
>
|
|
3540
|
-
<span style={{ fontSize: 12 }}>⚡</span>
|
|
3541
|
-
{count} cache rebuild{count === 1 ? "" : "s"} · {formatTokens(writeTokens)} rewritten
|
|
3542
|
-
{anchor && (
|
|
3543
|
-
<AnchoredTooltip anchor={anchor} width={320}>
|
|
3544
|
-
<TooltipRow label="Cache rebuilds" value={count.toLocaleString()} />
|
|
3545
|
-
<TooltipRow label="Tokens rewritten" value={writeTokens.toLocaleString()} />
|
|
3546
|
-
{estUsd >= 0.005 && (
|
|
3547
|
-
<TooltipRow label="Est. rebuild cost" value={formatCost(estUsd)} />
|
|
3548
|
-
)}
|
|
3549
|
-
<div
|
|
3550
|
-
style={{
|
|
3551
|
-
marginTop: 6,
|
|
3552
|
-
paddingTop: 6,
|
|
3553
|
-
borderTop: "1px solid rgba(241,245,249,0.12)",
|
|
3554
|
-
opacity: 0.65,
|
|
3555
|
-
fontSize: 10,
|
|
3556
|
-
whiteSpace: "normal",
|
|
3557
|
-
lineHeight: 1.4,
|
|
3558
|
-
}}
|
|
3559
|
-
>
|
|
3560
|
-
Turns where the whole prompt-cache prefix had to be rewritten.
|
|
3561
|
-
Either the cache expired during a long idle gap, or a /compact
|
|
3562
|
-
(manual or auto) summarized the conversation. Both cost tokens
|
|
3563
|
-
at 1.25× base input price.
|
|
3564
|
-
</div>
|
|
3565
|
-
</AnchoredTooltip>
|
|
3566
|
-
)}
|
|
3567
|
-
</span>
|
|
3568
|
-
);
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
function ColdResumeChip({
|
|
3572
|
-
info,
|
|
3573
|
-
model,
|
|
3574
|
-
}: {
|
|
3575
|
-
info: NonNullable<SessionEvent["coldResume"]>;
|
|
3576
|
-
model?: string;
|
|
3577
|
-
}) {
|
|
3578
|
-
const { writeTokens } = info;
|
|
3579
|
-
const estUsd = estimateCost(
|
|
3580
|
-
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
3581
|
-
model,
|
|
3582
|
-
);
|
|
3583
|
-
return (
|
|
3584
|
-
<span
|
|
3585
|
-
title={`Prompt cache expired during idle; this turn rewrote ${writeTokens.toLocaleString()} tokens into cache${estUsd >= 0.005 ? ` (est. ${formatCost(estUsd)})` : ""}.`}
|
|
3586
|
-
style={{
|
|
3587
|
-
display: "inline-flex",
|
|
3588
|
-
alignItems: "center",
|
|
3589
|
-
gap: 4,
|
|
3590
|
-
fontSize: 10.5,
|
|
3591
|
-
fontWeight: 600,
|
|
3592
|
-
padding: "2px 7px",
|
|
3593
|
-
borderRadius: 3,
|
|
3594
|
-
background: "rgba(217, 119, 6, 0.14)",
|
|
3595
|
-
color: "#78350F",
|
|
3596
|
-
letterSpacing: "0.01em",
|
|
3597
|
-
cursor: "default",
|
|
3598
|
-
}}
|
|
3599
|
-
>
|
|
3600
|
-
⚡ cold resume · {formatTokens(writeTokens)} rewritten
|
|
3601
|
-
</span>
|
|
3602
|
-
);
|
|
3603
|
-
}
|
|
3604
|
-
|
|
3605
|
-
/* ------------------------------------------------------------------ */
|
|
3606
|
-
/* Collapsed turn row */
|
|
3607
|
-
/* */
|
|
3608
|
-
/* Three-line layout: */
|
|
3609
|
-
/* 1. First agent preview (intent / plan) */
|
|
3610
|
-
/* 2. Middle summary (counts + top tools + duration) */
|
|
3611
|
-
/* 3. Final agent preview (answer / conclusion) */
|
|
3612
|
-
/* ------------------------------------------------------------------ */
|
|
3613
|
-
|
|
3614
|
-
function CollapsedTurnRow({
|
|
3615
|
-
turn,
|
|
3616
|
-
onClick,
|
|
3617
|
-
refCb,
|
|
3618
|
-
stickyOffset,
|
|
3619
|
-
inProgress,
|
|
3620
|
-
team,
|
|
3621
|
-
coldResume,
|
|
3622
|
-
model,
|
|
3623
|
-
}: {
|
|
3624
|
-
turn: TurnMegaRow;
|
|
3625
|
-
/** Fires when the user wants to fully expand this turn into the
|
|
3626
|
-
* transcript list (inner rows appear below as separate TranscriptRow
|
|
3627
|
-
* entries). Triggered by the "Show all N steps" bottom bar. */
|
|
3628
|
-
onClick: () => void;
|
|
3629
|
-
refCb: (el: HTMLDivElement | null) => void;
|
|
3630
|
-
stickyOffset: number;
|
|
3631
|
-
inProgress?: boolean;
|
|
3632
|
-
team?: (TimelineData & { teamName: string }) | null;
|
|
3633
|
-
coldResume?: NonNullable<SessionEvent["coldResume"]>;
|
|
3634
|
-
model?: string;
|
|
3635
|
-
}) {
|
|
3636
|
-
const theme = ROLE_THEMES.agent;
|
|
3637
|
-
const s = turn.summary;
|
|
3638
|
-
const hasTokens = s.totalTokens.input > 0 || s.totalTokens.output > 0;
|
|
3639
|
-
|
|
3640
|
-
// First / conclusion agent-message indices are pre-computed by
|
|
3641
|
-
// buildMegaRows — the "conclusion" uses a heuristic that skips short
|
|
3642
|
-
// codas following a task-notification (so e.g. "Ship done..." beats
|
|
3643
|
-
// a later "Acknowledged — the background task closed out").
|
|
3644
|
-
const firstAgentIdx = s.firstAgentIndex ?? -1;
|
|
3645
|
-
const finalAgentIdx = s.finalAgentIndex ?? -1;
|
|
3646
|
-
const firstAgentRow = firstAgentIdx >= 0 ? turn.rows[firstAgentIdx] : undefined;
|
|
3647
|
-
const finalAgentRow =
|
|
3648
|
-
finalAgentIdx >= 0 && finalAgentIdx !== firstAgentIdx ? turn.rows[finalAgentIdx] : undefined;
|
|
3649
|
-
|
|
3650
|
-
// Middle list excludes the first and conclusion messages. Any coda
|
|
3651
|
-
// messages (e.g. "Acknowledged ...") remain visible here as ordinary
|
|
3652
|
-
// steps — they happened, they just aren't the semantic conclusion.
|
|
3653
|
-
const middleRows = turn.rows.filter((_, i) => {
|
|
3654
|
-
if (i === firstAgentIdx) return false;
|
|
3655
|
-
if (finalAgentRow && i === finalAgentIdx) return false;
|
|
3656
|
-
return true;
|
|
3657
|
-
});
|
|
3658
|
-
|
|
3659
|
-
// Local state: whether the first / last message is rendered in full
|
|
3660
|
-
// markdown form inline (instead of the 2-line preview).
|
|
3661
|
-
const [firstExpanded, setFirstExpanded] = useState(false);
|
|
3662
|
-
const [lastExpanded, setLastExpanded] = useState(false);
|
|
3663
|
-
|
|
3664
|
-
// Stop click propagation on interactive children so clicks on the
|
|
3665
|
-
// expandable message areas don't bubble up and trigger onClick on the
|
|
3666
|
-
// whole row — we want explicit controls only.
|
|
3667
|
-
const stop = (e: React.MouseEvent) => e.stopPropagation();
|
|
3668
|
-
|
|
3669
|
-
return (
|
|
3670
|
-
<div
|
|
3671
|
-
ref={refCb}
|
|
3672
|
-
data-sl-row-index={turn.firstPrimaryIndex}
|
|
3673
|
-
data-sl-toffset={turn.tOffsetMs ?? 0}
|
|
3674
|
-
style={{
|
|
3675
|
-
display: "grid",
|
|
3676
|
-
gridTemplateColumns: "20px 84px 1fr auto auto",
|
|
3677
|
-
columnGap: 12,
|
|
3678
|
-
alignItems: "start",
|
|
3679
|
-
padding: "14px 12px 0 12px",
|
|
3680
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
3681
|
-
transition: "background 0.08s",
|
|
3682
|
-
scrollMarginTop: stickyOffset,
|
|
3683
|
-
}}
|
|
3684
|
-
>
|
|
3685
|
-
{/* Col 1 — empty spacer to keep the Agent pill aligned with
|
|
3686
|
-
TranscriptRow's pill column (which also has an empty 20px slot). */}
|
|
3687
|
-
<span />
|
|
3688
|
-
|
|
3689
|
-
{/* Col 2 — "Agent" role pill */}
|
|
3690
|
-
<span
|
|
3691
|
-
style={{
|
|
3692
|
-
justifySelf: "start",
|
|
3693
|
-
fontSize: 11,
|
|
3694
|
-
fontWeight: 600,
|
|
3695
|
-
padding: "3px 10px",
|
|
3696
|
-
borderRadius: 4,
|
|
3697
|
-
background: theme.bg,
|
|
3698
|
-
color: theme.fg,
|
|
3699
|
-
marginTop: 2,
|
|
3700
|
-
}}
|
|
3701
|
-
>
|
|
3702
|
-
Agent
|
|
3703
|
-
</span>
|
|
3704
|
-
|
|
3705
|
-
{/* Col 3 — content (cold-resume notice · first · stats · steps · last · bottom bar) */}
|
|
3706
|
-
<div
|
|
3707
|
-
style={{
|
|
3708
|
-
minWidth: 0,
|
|
3709
|
-
display: "flex",
|
|
3710
|
-
flexDirection: "column",
|
|
3711
|
-
gap: 10,
|
|
3712
|
-
paddingBottom: 10,
|
|
3713
|
-
}}
|
|
3714
|
-
>
|
|
3715
|
-
{coldResume && (
|
|
3716
|
-
<div onClick={stop}>
|
|
3717
|
-
<ColdResumeNotice info={coldResume} model={model} />
|
|
3718
|
-
</div>
|
|
2287
|
+
{coldResume && (
|
|
2288
|
+
<div onClick={stop}>
|
|
2289
|
+
<ColdResumeNotice info={coldResume} model={model} />
|
|
2290
|
+
</div>
|
|
3719
2291
|
)}
|
|
3720
2292
|
|
|
3721
2293
|
{/* 1. First agent message — clickable to expand into full markdown */}
|
|
@@ -4184,1226 +2756,340 @@ function TurnStatsLine({
|
|
|
4184
2756
|
<>
|
|
4185
2757
|
<span style={{ opacity: 0.5, marginLeft: 4 }}>·</span>
|
|
4186
2758
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10 }}>
|
|
4187
|
-
{formatGap(durationMs)}
|
|
4188
|
-
</span>
|
|
4189
|
-
</>
|
|
4190
|
-
)}
|
|
4191
|
-
</div>
|
|
4192
|
-
|
|
4193
|
-
{/* Last file path (Ghostty "└ ..." style) */}
|
|
4194
|
-
{shortPath && (
|
|
4195
|
-
<div
|
|
4196
|
-
style={{
|
|
4197
|
-
fontSize: 10,
|
|
4198
|
-
color: "var(--af-text-tertiary)",
|
|
4199
|
-
fontFamily: "var(--font-mono)",
|
|
4200
|
-
marginTop: 1,
|
|
4201
|
-
opacity: 0.75,
|
|
4202
|
-
}}
|
|
4203
|
-
title={lastFilePath}
|
|
4204
|
-
>
|
|
4205
|
-
└ {shortPath}
|
|
4206
|
-
</div>
|
|
4207
|
-
)}
|
|
4208
|
-
</div>
|
|
4209
|
-
);
|
|
4210
|
-
}
|
|
4211
|
-
|
|
4212
|
-
function ExpandedTurnHeader({
|
|
4213
|
-
turn,
|
|
4214
|
-
onClick,
|
|
4215
|
-
coldResume,
|
|
4216
|
-
model,
|
|
4217
|
-
}: {
|
|
4218
|
-
turn: TurnMegaRow;
|
|
4219
|
-
onClick: () => void;
|
|
4220
|
-
coldResume?: NonNullable<SessionEvent["coldResume"]>;
|
|
4221
|
-
model?: string;
|
|
4222
|
-
}) {
|
|
4223
|
-
const s = turn.summary;
|
|
4224
|
-
return (
|
|
4225
|
-
<div
|
|
4226
|
-
onClick={onClick}
|
|
4227
|
-
style={{
|
|
4228
|
-
display: "flex",
|
|
4229
|
-
alignItems: "center",
|
|
4230
|
-
gap: 8,
|
|
4231
|
-
padding: "6px 12px 6px 18px",
|
|
4232
|
-
marginLeft: 12,
|
|
4233
|
-
borderLeft: "2px solid var(--af-accent)",
|
|
4234
|
-
borderBottom: "1px dashed var(--af-border-subtle)",
|
|
4235
|
-
fontSize: 11,
|
|
4236
|
-
color: "var(--af-text-tertiary)",
|
|
4237
|
-
cursor: "pointer",
|
|
4238
|
-
background: "rgba(92, 132, 195, 0.04)",
|
|
4239
|
-
}}
|
|
4240
|
-
onMouseEnter={(e) => {
|
|
4241
|
-
e.currentTarget.style.background = "rgba(92, 132, 195, 0.10)";
|
|
4242
|
-
e.currentTarget.style.color = "var(--af-text-secondary)";
|
|
4243
|
-
}}
|
|
4244
|
-
onMouseLeave={(e) => {
|
|
4245
|
-
e.currentTarget.style.background = "rgba(92, 132, 195, 0.04)";
|
|
4246
|
-
e.currentTarget.style.color = "var(--af-text-tertiary)";
|
|
4247
|
-
}}
|
|
4248
|
-
>
|
|
4249
|
-
<ChevronDown size={13} />
|
|
4250
|
-
<span>
|
|
4251
|
-
Hide turn · {s.agentMessages} message{s.agentMessages === 1 ? "" : "s"} · {s.toolCalls} tool
|
|
4252
|
-
{s.toolCalls === 1 ? "" : "s"}
|
|
4253
|
-
{turn.durationMs !== undefined ? ` · ${formatGap(turn.durationMs)}` : ""}
|
|
4254
|
-
</span>
|
|
4255
|
-
{coldResume && (
|
|
4256
|
-
<span style={{ marginLeft: "auto" }}>
|
|
4257
|
-
<ColdResumeChip info={coldResume} model={model} />
|
|
4258
|
-
</span>
|
|
4259
|
-
)}
|
|
4260
|
-
</div>
|
|
4261
|
-
);
|
|
4262
|
-
}
|
|
4263
|
-
|
|
4264
|
-
/** Matching collapse control at the bottom of an expanded turn. Mirrors
|
|
4265
|
-
* the top ExpandedTurnHeader so after reading through the inner rows
|
|
4266
|
-
* the user has a collapse button right where their eye lands. */
|
|
4267
|
-
function ExpandedTurnFooter({ turn, onClick }: { turn: TurnMegaRow; onClick: () => void }) {
|
|
4268
|
-
const s = turn.summary;
|
|
4269
|
-
return (
|
|
4270
|
-
<div
|
|
4271
|
-
onClick={onClick}
|
|
4272
|
-
style={{
|
|
4273
|
-
display: "flex",
|
|
4274
|
-
alignItems: "center",
|
|
4275
|
-
gap: 8,
|
|
4276
|
-
padding: "7px 12px 7px 18px",
|
|
4277
|
-
marginLeft: 12,
|
|
4278
|
-
marginBottom: 8,
|
|
4279
|
-
borderLeft: "2px solid var(--af-accent)",
|
|
4280
|
-
borderTop: "1px dashed var(--af-border-subtle)",
|
|
4281
|
-
borderBottomLeftRadius: 6,
|
|
4282
|
-
borderBottomRightRadius: 6,
|
|
4283
|
-
fontSize: 11,
|
|
4284
|
-
color: "var(--af-text-tertiary)",
|
|
4285
|
-
cursor: "pointer",
|
|
4286
|
-
background: "rgba(92, 132, 195, 0.04)",
|
|
4287
|
-
}}
|
|
4288
|
-
onMouseEnter={(e) => {
|
|
4289
|
-
e.currentTarget.style.background = "rgba(92, 132, 195, 0.10)";
|
|
4290
|
-
e.currentTarget.style.color = "var(--af-text-secondary)";
|
|
4291
|
-
}}
|
|
4292
|
-
onMouseLeave={(e) => {
|
|
4293
|
-
e.currentTarget.style.background = "rgba(92, 132, 195, 0.04)";
|
|
4294
|
-
e.currentTarget.style.color = "var(--af-text-tertiary)";
|
|
4295
|
-
}}
|
|
4296
|
-
>
|
|
4297
|
-
<ChevronUp size={13} />
|
|
4298
|
-
<span>
|
|
4299
|
-
Collapse turn · {s.agentMessages} message{s.agentMessages === 1 ? "" : "s"} · {s.toolCalls}{" "}
|
|
4300
|
-
tool{s.toolCalls === 1 ? "" : "s"}
|
|
4301
|
-
{turn.durationMs !== undefined ? ` · ${formatGap(turn.durationMs)}` : ""}
|
|
4302
|
-
</span>
|
|
4303
|
-
</div>
|
|
4304
|
-
);
|
|
4305
|
-
}
|
|
4306
|
-
|
|
4307
|
-
function TranscriptRow({
|
|
4308
|
-
row,
|
|
4309
|
-
selected,
|
|
4310
|
-
onSelect,
|
|
4311
|
-
refCb,
|
|
4312
|
-
stickyOffset,
|
|
4313
|
-
indented,
|
|
4314
|
-
}: {
|
|
4315
|
-
row: PresentationRow;
|
|
4316
|
-
selected: boolean;
|
|
4317
|
-
onSelect: () => void;
|
|
4318
|
-
refCb: (el: HTMLDivElement | null) => void;
|
|
4319
|
-
stickyOffset: number;
|
|
4320
|
-
indented?: boolean;
|
|
4321
|
-
}) {
|
|
4322
|
-
const theme = ROLE_THEMES[row.kind];
|
|
4323
|
-
const event = row.kind === "tool-group" ? row.events[0] : row.event;
|
|
4324
|
-
const usage = event.usage;
|
|
4325
|
-
const hasUsage = row.kind === "agent" && usage && (usage.input > 0 || usage.output > 0);
|
|
4326
|
-
const isTeammateMsg = row.kind === "user" && !!row.event.teammateMessage;
|
|
4327
|
-
const roleLabel = isTeammateMsg ? "Team Lead" : theme.label;
|
|
4328
|
-
|
|
4329
|
-
const preview = rowPreview(row);
|
|
4330
|
-
|
|
4331
|
-
return (
|
|
4332
|
-
<div
|
|
4333
|
-
ref={refCb}
|
|
4334
|
-
onClick={onSelect}
|
|
4335
|
-
data-sl-row-index={row.kind === "tool-group" ? row.events[0].index : row.event.index}
|
|
4336
|
-
data-sl-toffset={row.tOffsetMs ?? 0}
|
|
4337
|
-
style={{
|
|
4338
|
-
display: "grid",
|
|
4339
|
-
// Empty 20px prefix column keeps the role pill at the same x-offset
|
|
4340
|
-
// as collapsed turn rows (which have a chevron there). Ensures the
|
|
4341
|
-
// User/Agent/Tool tags align vertically in the transcript.
|
|
4342
|
-
gridTemplateColumns: "20px 84px 1fr auto auto",
|
|
4343
|
-
gap: 14,
|
|
4344
|
-
alignItems: "center",
|
|
4345
|
-
padding: "11px 12px",
|
|
4346
|
-
paddingLeft: indented ? 28 : 12,
|
|
4347
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4348
|
-
borderLeft: indented ? "2px solid var(--af-accent)" : "2px solid transparent",
|
|
4349
|
-
marginLeft: indented ? 12 : 0,
|
|
4350
|
-
cursor: "pointer",
|
|
4351
|
-
background: selected
|
|
4352
|
-
? "var(--af-accent-subtle)"
|
|
4353
|
-
: indented
|
|
4354
|
-
? "rgba(92, 132, 195, 0.03)"
|
|
4355
|
-
: "transparent",
|
|
4356
|
-
transition: "background 0.08s",
|
|
4357
|
-
scrollMarginTop: stickyOffset,
|
|
4358
|
-
}}
|
|
4359
|
-
onMouseEnter={(e) => {
|
|
4360
|
-
if (!selected) e.currentTarget.style.background = "var(--af-surface-hover)";
|
|
4361
|
-
}}
|
|
4362
|
-
onMouseLeave={(e) => {
|
|
4363
|
-
if (!selected) e.currentTarget.style.background = "transparent";
|
|
4364
|
-
}}
|
|
4365
|
-
>
|
|
4366
|
-
{/* Empty chevron-slot column — keeps the pill column aligned with
|
|
4367
|
-
collapsed-turn rows that do render a chevron. */}
|
|
4368
|
-
<span />
|
|
4369
|
-
<span
|
|
4370
|
-
style={{
|
|
4371
|
-
justifySelf: "start",
|
|
4372
|
-
fontSize: 11,
|
|
4373
|
-
fontWeight: 600,
|
|
4374
|
-
padding: "3px 10px",
|
|
4375
|
-
borderRadius: 4,
|
|
4376
|
-
background: theme.bg,
|
|
4377
|
-
color: theme.fg,
|
|
4378
|
-
}}
|
|
4379
|
-
>
|
|
4380
|
-
{roleLabel}
|
|
4381
|
-
</span>
|
|
4382
|
-
|
|
4383
|
-
<span
|
|
4384
|
-
style={{
|
|
4385
|
-
fontSize: 13,
|
|
4386
|
-
color: row.kind === "error" ? "var(--af-danger)" : "var(--af-text)",
|
|
4387
|
-
overflow: "hidden",
|
|
4388
|
-
textOverflow: "ellipsis",
|
|
4389
|
-
whiteSpace: "nowrap",
|
|
4390
|
-
display: "flex",
|
|
4391
|
-
alignItems: "center",
|
|
4392
|
-
gap: 8,
|
|
4393
|
-
}}
|
|
4394
|
-
>
|
|
4395
|
-
{row.kind === "tool-group" && (
|
|
4396
|
-
<Wrench size={13} style={{ color: "var(--af-text-tertiary)", flexShrink: 0 }} />
|
|
4397
|
-
)}
|
|
4398
|
-
<span
|
|
4399
|
-
style={{
|
|
4400
|
-
overflow: "hidden",
|
|
4401
|
-
textOverflow: "ellipsis",
|
|
4402
|
-
whiteSpace: "nowrap",
|
|
4403
|
-
}}
|
|
4404
|
-
>
|
|
4405
|
-
{row.kind === "tool-group" ? (
|
|
4406
|
-
<ToolGroupLabel toolNames={row.toolNames} count={row.count} />
|
|
4407
|
-
) : (
|
|
4408
|
-
preview
|
|
4409
|
-
)}
|
|
4410
|
-
</span>
|
|
4411
|
-
</span>
|
|
4412
|
-
|
|
4413
|
-
{hasUsage && usage ? <TokenChip usage={usage} /> : <span />}
|
|
4414
|
-
|
|
4415
|
-
<span
|
|
4416
|
-
style={{
|
|
4417
|
-
fontFamily: "var(--font-mono)",
|
|
4418
|
-
fontSize: 11,
|
|
4419
|
-
color: "var(--af-text-tertiary)",
|
|
4420
|
-
minWidth: 64,
|
|
4421
|
-
textAlign: "right",
|
|
4422
|
-
}}
|
|
4423
|
-
>
|
|
4424
|
-
{formatOffset(row.tOffsetMs)}
|
|
4425
|
-
</span>
|
|
4426
|
-
</div>
|
|
4427
|
-
);
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
/* -------- Token chip w/ hover tooltip -------- */
|
|
4431
|
-
|
|
4432
|
-
/** Pretty inline label for a collapsed tool-group row. Shows unique tool
|
|
4433
|
-
* names with bolded names and subtle "×N" counts. */
|
|
4434
|
-
function ToolGroupLabel({
|
|
4435
|
-
toolNames,
|
|
4436
|
-
count,
|
|
4437
|
-
}: {
|
|
4438
|
-
toolNames: { name: string; count: number }[];
|
|
4439
|
-
count: number;
|
|
4440
|
-
}) {
|
|
4441
|
-
const MAX = 4;
|
|
4442
|
-
const shown = toolNames.slice(0, MAX);
|
|
4443
|
-
const overflow = toolNames.length - MAX;
|
|
4444
|
-
return (
|
|
4445
|
-
<span>
|
|
4446
|
-
{shown.map((t, i) => (
|
|
4447
|
-
<span key={t.name}>
|
|
4448
|
-
{i > 0 && <span style={{ color: "var(--af-text-tertiary)", margin: "0 6px" }}>·</span>}
|
|
4449
|
-
<b>{shortenToolName(t.name)}</b>
|
|
4450
|
-
{t.count > 1 && (
|
|
4451
|
-
<span
|
|
4452
|
-
style={{
|
|
4453
|
-
color: "var(--af-text-tertiary)",
|
|
4454
|
-
fontWeight: 400,
|
|
4455
|
-
marginLeft: 4,
|
|
4456
|
-
fontFamily: "var(--font-mono)",
|
|
4457
|
-
fontSize: 11,
|
|
4458
|
-
}}
|
|
4459
|
-
>
|
|
4460
|
-
×{t.count}
|
|
4461
|
-
</span>
|
|
4462
|
-
)}
|
|
4463
|
-
</span>
|
|
4464
|
-
))}
|
|
4465
|
-
{overflow > 0 && (
|
|
4466
|
-
<span
|
|
4467
|
-
style={{
|
|
4468
|
-
color: "var(--af-text-tertiary)",
|
|
4469
|
-
marginLeft: 8,
|
|
4470
|
-
fontSize: 11,
|
|
4471
|
-
}}
|
|
4472
|
-
>
|
|
4473
|
-
+{overflow} more
|
|
4474
|
-
</span>
|
|
4475
|
-
)}
|
|
4476
|
-
{count > 3 && (
|
|
4477
|
-
<span
|
|
4478
|
-
style={{
|
|
4479
|
-
color: "var(--af-text-tertiary)",
|
|
4480
|
-
marginLeft: 10,
|
|
4481
|
-
fontSize: 11,
|
|
4482
|
-
fontFamily: "var(--font-mono)",
|
|
4483
|
-
}}
|
|
4484
|
-
>
|
|
4485
|
-
({count} calls)
|
|
4486
|
-
</span>
|
|
4487
|
-
)}
|
|
4488
|
-
</span>
|
|
4489
|
-
);
|
|
4490
|
-
}
|
|
4491
|
-
|
|
4492
|
-
// TokenChip, TurnTokenChip moved to ./session-view/token-stats.tsx
|
|
4493
|
-
|
|
4494
|
-
/* ------------------------------------------------------------------ */
|
|
4495
|
-
/* Debug list */
|
|
4496
|
-
/* ------------------------------------------------------------------ */
|
|
4497
|
-
|
|
4498
|
-
// DebugList moved to ./session-view/debug-list.tsx
|
|
4499
|
-
|
|
4500
|
-
/* ------------------------------------------------------------------ */
|
|
4501
|
-
/* SubagentDrawer */
|
|
4502
|
-
/* */
|
|
4503
|
-
/* Opened when the user clicks a sub-agent lane bar on the mini-map. */
|
|
4504
|
-
/* Renders everything we know about the run — type pill, background */
|
|
4505
|
-
/* badge, timing range, duration, stats, the full prompt the parent */
|
|
4506
|
-
/* dispatched, the final agent text, and a "Jump to parent" action */
|
|
4507
|
-
/* that scrolls the transcript to the Agent tool_use row that kicked */
|
|
4508
|
-
/* this subagent off. */
|
|
4509
|
-
/* ------------------------------------------------------------------ */
|
|
4510
|
-
|
|
4511
|
-
// SubagentDrawer moved to ./session-view/subagent-drawer.tsx
|
|
4512
|
-
|
|
4513
|
-
/* ------------------------------------------------------------------ */
|
|
4514
|
-
/* Workflows panel */
|
|
4515
|
-
/* */
|
|
4516
|
-
/* A single `Workflow` tool call collapses a whole dynamic-workflow */
|
|
4517
|
-
/* fan-out into one transcript row. This panel surfaces what that row */
|
|
4518
|
-
/* actually did: each run's spawned-agent count, tool calls, tokens, */
|
|
4519
|
-
/* phases, and progress log — the fleet work the transcript hides. */
|
|
4520
|
-
/* ------------------------------------------------------------------ */
|
|
4521
|
-
|
|
4522
|
-
function WorkflowsPanel({
|
|
4523
|
-
workflows,
|
|
4524
|
-
spawnedAgentCount,
|
|
4525
|
-
focusRunId,
|
|
4526
|
-
selectedAgentKey,
|
|
4527
|
-
onOpenAgent,
|
|
4528
|
-
onJumpToParent,
|
|
4529
|
-
onClearFocus,
|
|
4530
|
-
}: {
|
|
4531
|
-
workflows: WorkflowRun[];
|
|
4532
|
-
spawnedAgentCount: number;
|
|
4533
|
-
focusRunId?: string | null;
|
|
4534
|
-
selectedAgentKey?: string | null;
|
|
4535
|
-
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
4536
|
-
onJumpToParent: (toolUseId?: string) => void;
|
|
4537
|
-
onClearFocus?: () => void;
|
|
4538
|
-
}) {
|
|
4539
|
-
const totalTokens = workflows.reduce((n, w) => n + w.totalTokens, 0);
|
|
4540
|
-
const totalTools = workflows.reduce((n, w) => n + w.toolCallCount, 0);
|
|
4541
|
-
return (
|
|
4542
|
-
<section
|
|
4543
|
-
style={{
|
|
4544
|
-
marginBottom: 16,
|
|
4545
|
-
border: "1px solid var(--af-border-subtle)",
|
|
4546
|
-
borderRadius: 10,
|
|
4547
|
-
background: "var(--af-surface)",
|
|
4548
|
-
overflow: "hidden",
|
|
4549
|
-
}}
|
|
4550
|
-
>
|
|
4551
|
-
<div
|
|
4552
|
-
style={{
|
|
4553
|
-
display: "flex",
|
|
4554
|
-
alignItems: "center",
|
|
4555
|
-
gap: 10,
|
|
4556
|
-
flexWrap: "wrap",
|
|
4557
|
-
padding: "10px 14px",
|
|
4558
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4559
|
-
background: "var(--af-surface-subtle, rgba(234,88,12,0.04))",
|
|
4560
|
-
}}
|
|
4561
|
-
>
|
|
4562
|
-
<span
|
|
4563
|
-
style={{
|
|
4564
|
-
display: "inline-flex",
|
|
4565
|
-
alignItems: "center",
|
|
4566
|
-
gap: 7,
|
|
4567
|
-
fontSize: 13,
|
|
4568
|
-
fontWeight: 600,
|
|
4569
|
-
color: "var(--af-text)",
|
|
4570
|
-
}}
|
|
4571
|
-
>
|
|
4572
|
-
<WorkflowIcon size={14} style={{ color: "#EA580C" }} />
|
|
4573
|
-
Workflows
|
|
4574
|
-
</span>
|
|
4575
|
-
<span
|
|
2759
|
+
{formatGap(durationMs)}
|
|
2760
|
+
</span>
|
|
2761
|
+
</>
|
|
2762
|
+
)}
|
|
2763
|
+
</div>
|
|
2764
|
+
|
|
2765
|
+
{/* Last file path (Ghostty "└ ..." style) */}
|
|
2766
|
+
{shortPath && (
|
|
2767
|
+
<div
|
|
4576
2768
|
style={{
|
|
4577
|
-
fontSize:
|
|
2769
|
+
fontSize: 10,
|
|
4578
2770
|
color: "var(--af-text-tertiary)",
|
|
4579
2771
|
fontFamily: "var(--font-mono)",
|
|
2772
|
+
marginTop: 1,
|
|
2773
|
+
opacity: 0.75,
|
|
4580
2774
|
}}
|
|
2775
|
+
title={lastFilePath}
|
|
4581
2776
|
>
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
</div>
|
|
4587
|
-
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
4588
|
-
{workflows.map((w) => (
|
|
4589
|
-
<WorkflowCard
|
|
4590
|
-
key={w.runId}
|
|
4591
|
-
w={w}
|
|
4592
|
-
focused={focusRunId === w.runId}
|
|
4593
|
-
selectedAgentKey={selectedAgentKey}
|
|
4594
|
-
onOpenAgent={onOpenAgent}
|
|
4595
|
-
onJumpToParent={onJumpToParent}
|
|
4596
|
-
onClearFocus={onClearFocus}
|
|
4597
|
-
/>
|
|
4598
|
-
))}
|
|
4599
|
-
</div>
|
|
4600
|
-
</section>
|
|
2777
|
+
└ {shortPath}
|
|
2778
|
+
</div>
|
|
2779
|
+
)}
|
|
2780
|
+
</div>
|
|
4601
2781
|
);
|
|
4602
2782
|
}
|
|
4603
2783
|
|
|
4604
|
-
function
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
onJumpToParent,
|
|
4610
|
-
onClearFocus,
|
|
2784
|
+
function ExpandedTurnHeader({
|
|
2785
|
+
turn,
|
|
2786
|
+
onClick,
|
|
2787
|
+
coldResume,
|
|
2788
|
+
model,
|
|
4611
2789
|
}: {
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
onJumpToParent: (toolUseId?: string) => void;
|
|
4617
|
-
onClearFocus?: () => void;
|
|
2790
|
+
turn: TurnMegaRow;
|
|
2791
|
+
onClick: () => void;
|
|
2792
|
+
coldResume?: NonNullable<SessionEvent["coldResume"]>;
|
|
2793
|
+
model?: string;
|
|
4618
2794
|
}) {
|
|
4619
|
-
const
|
|
4620
|
-
const cardRef = useRef<HTMLDivElement>(null);
|
|
4621
|
-
// Focused via a minimap lane click — expand and scroll this run into view.
|
|
4622
|
-
useEffect(() => {
|
|
4623
|
-
if (focused) {
|
|
4624
|
-
setOpen(true);
|
|
4625
|
-
cardRef.current?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
4626
|
-
}
|
|
4627
|
-
}, [focused]);
|
|
4628
|
-
const color = workflowColor(w.status);
|
|
4629
|
-
const dur = w.durationMs !== undefined ? formatGap(w.durationMs) : "—";
|
|
4630
|
-
const startOff = formatOffset(w.startTOffsetMs);
|
|
4631
|
-
|
|
2795
|
+
const s = turn.summary;
|
|
4632
2796
|
return (
|
|
4633
2797
|
<div
|
|
4634
|
-
|
|
2798
|
+
onClick={onClick}
|
|
4635
2799
|
style={{
|
|
4636
|
-
|
|
4637
|
-
|
|
2800
|
+
display: "flex",
|
|
2801
|
+
alignItems: "center",
|
|
2802
|
+
gap: 8,
|
|
2803
|
+
padding: "6px 12px 6px 18px",
|
|
2804
|
+
marginLeft: 12,
|
|
2805
|
+
borderLeft: "2px solid var(--af-accent)",
|
|
2806
|
+
borderBottom: "1px dashed var(--af-border-subtle)",
|
|
2807
|
+
fontSize: 11,
|
|
2808
|
+
color: "var(--af-text-tertiary)",
|
|
2809
|
+
cursor: "pointer",
|
|
2810
|
+
background: "rgba(92, 132, 195, 0.04)",
|
|
2811
|
+
}}
|
|
2812
|
+
onMouseEnter={(e) => {
|
|
2813
|
+
e.currentTarget.style.background = "rgba(92, 132, 195, 0.10)";
|
|
2814
|
+
e.currentTarget.style.color = "var(--af-text-secondary)";
|
|
2815
|
+
}}
|
|
2816
|
+
onMouseLeave={(e) => {
|
|
2817
|
+
e.currentTarget.style.background = "rgba(92, 132, 195, 0.04)";
|
|
2818
|
+
e.currentTarget.style.color = "var(--af-text-tertiary)";
|
|
4638
2819
|
}}
|
|
4639
2820
|
>
|
|
4640
|
-
{
|
|
4641
|
-
<
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
}}
|
|
4648
|
-
|
|
4649
|
-
width: "100%",
|
|
4650
|
-
display: "flex",
|
|
4651
|
-
alignItems: "center",
|
|
4652
|
-
gap: 10,
|
|
4653
|
-
padding: "10px 14px",
|
|
4654
|
-
background: "transparent",
|
|
4655
|
-
border: "none",
|
|
4656
|
-
cursor: "pointer",
|
|
4657
|
-
textAlign: "left",
|
|
4658
|
-
color: "inherit",
|
|
4659
|
-
flexWrap: "wrap",
|
|
4660
|
-
}}
|
|
4661
|
-
>
|
|
4662
|
-
<span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>
|
|
4663
|
-
{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
4664
|
-
</span>
|
|
4665
|
-
<span
|
|
4666
|
-
style={{
|
|
4667
|
-
fontFamily: "var(--font-mono)",
|
|
4668
|
-
fontSize: 13,
|
|
4669
|
-
fontWeight: 600,
|
|
4670
|
-
color: focused ? "#EA580C" : "var(--af-text)",
|
|
4671
|
-
background: focused ? "rgba(234,88,12,0.16)" : "transparent",
|
|
4672
|
-
padding: "2px 8px",
|
|
4673
|
-
borderRadius: 6,
|
|
4674
|
-
transition: "background 0.4s ease, color 0.4s ease",
|
|
4675
|
-
minWidth: 0,
|
|
4676
|
-
overflow: "hidden",
|
|
4677
|
-
textOverflow: "ellipsis",
|
|
4678
|
-
whiteSpace: "nowrap",
|
|
4679
|
-
}}
|
|
4680
|
-
title={w.name}
|
|
4681
|
-
>
|
|
4682
|
-
{w.name}
|
|
4683
|
-
</span>
|
|
4684
|
-
<span
|
|
4685
|
-
style={{
|
|
4686
|
-
fontSize: 9.5,
|
|
4687
|
-
fontWeight: 600,
|
|
4688
|
-
padding: "2px 7px",
|
|
4689
|
-
borderRadius: 100,
|
|
4690
|
-
background: color,
|
|
4691
|
-
color: "#fff",
|
|
4692
|
-
textTransform: "uppercase",
|
|
4693
|
-
letterSpacing: "0.04em",
|
|
4694
|
-
}}
|
|
4695
|
-
>
|
|
4696
|
-
{w.status}
|
|
4697
|
-
</span>
|
|
4698
|
-
<span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 12 }}>
|
|
4699
|
-
<span
|
|
4700
|
-
style={{
|
|
4701
|
-
fontSize: 12,
|
|
4702
|
-
fontWeight: 700,
|
|
4703
|
-
padding: "2px 10px",
|
|
4704
|
-
borderRadius: 100,
|
|
4705
|
-
background: "rgba(234,88,12,0.14)",
|
|
4706
|
-
color: "#EA580C",
|
|
4707
|
-
fontFamily: "var(--font-mono)",
|
|
4708
|
-
whiteSpace: "nowrap",
|
|
4709
|
-
}}
|
|
4710
|
-
>
|
|
4711
|
-
{w.agentCount} agents
|
|
4712
|
-
</span>
|
|
4713
|
-
<WfMiniStat icon={<Wrench size={11} />} value={`${w.toolCallCount.toLocaleString()}`} />
|
|
4714
|
-
<WfMiniStat value={`${formatTokens(w.totalTokens)} tok`} />
|
|
4715
|
-
<WfMiniStat icon={<Clock size={11} />} value={dur} />
|
|
2821
|
+
<ChevronDown size={13} />
|
|
2822
|
+
<span>
|
|
2823
|
+
Hide turn · {s.agentMessages} message{s.agentMessages === 1 ? "" : "s"} · {s.toolCalls} tool
|
|
2824
|
+
{s.toolCalls === 1 ? "" : "s"}
|
|
2825
|
+
{turn.durationMs !== undefined ? ` · ${formatGap(turn.durationMs)}` : ""}
|
|
2826
|
+
</span>
|
|
2827
|
+
{coldResume && (
|
|
2828
|
+
<span style={{ marginLeft: "auto" }}>
|
|
2829
|
+
<ColdResumeChip info={coldResume} model={model} />
|
|
4716
2830
|
</span>
|
|
4717
|
-
</button>
|
|
4718
|
-
|
|
4719
|
-
{/* Expanded detail */}
|
|
4720
|
-
{open && (
|
|
4721
|
-
<div style={{ padding: "0 14px 14px 38px", display: "flex", flexDirection: "column", gap: 12 }}>
|
|
4722
|
-
{w.description && (
|
|
4723
|
-
<div style={{ fontSize: 12.5, color: "var(--af-text-secondary)", lineHeight: 1.5 }}>
|
|
4724
|
-
{w.description}
|
|
4725
|
-
</div>
|
|
4726
|
-
)}
|
|
4727
|
-
|
|
4728
|
-
<div
|
|
4729
|
-
style={{
|
|
4730
|
-
display: "flex",
|
|
4731
|
-
gap: 14,
|
|
4732
|
-
flexWrap: "wrap",
|
|
4733
|
-
fontSize: 11,
|
|
4734
|
-
fontFamily: "var(--font-mono)",
|
|
4735
|
-
color: "var(--af-text-tertiary)",
|
|
4736
|
-
}}
|
|
4737
|
-
>
|
|
4738
|
-
<span>starts at {startOff}</span>
|
|
4739
|
-
{w.model && <span>· {w.model}</span>}
|
|
4740
|
-
<span>· runId {w.runId}</span>
|
|
4741
|
-
{w.parentToolUseId && (
|
|
4742
|
-
<button
|
|
4743
|
-
type="button"
|
|
4744
|
-
onClick={() => onJumpToParent(w.parentToolUseId)}
|
|
4745
|
-
style={{
|
|
4746
|
-
marginLeft: "auto",
|
|
4747
|
-
fontSize: 11,
|
|
4748
|
-
color: "var(--af-accent)",
|
|
4749
|
-
background: "transparent",
|
|
4750
|
-
border: "none",
|
|
4751
|
-
cursor: "pointer",
|
|
4752
|
-
padding: 0,
|
|
4753
|
-
fontFamily: "var(--font-mono)",
|
|
4754
|
-
}}
|
|
4755
|
-
>
|
|
4756
|
-
Jump to timeline →
|
|
4757
|
-
</button>
|
|
4758
|
-
)}
|
|
4759
|
-
</div>
|
|
4760
|
-
|
|
4761
|
-
{w.agents.length > 0 ? (
|
|
4762
|
-
<WorkflowPhaseTabs
|
|
4763
|
-
w={w}
|
|
4764
|
-
selectedAgentKey={selectedAgentKey}
|
|
4765
|
-
onOpenAgent={onOpenAgent}
|
|
4766
|
-
/>
|
|
4767
|
-
) : (
|
|
4768
|
-
w.phases.length > 0 && (
|
|
4769
|
-
<div>
|
|
4770
|
-
<SectionLabel>Phases</SectionLabel>
|
|
4771
|
-
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
|
4772
|
-
{w.phases.map((p, i) => (
|
|
4773
|
-
<span
|
|
4774
|
-
key={`${p.title}-${i}`}
|
|
4775
|
-
title={p.detail}
|
|
4776
|
-
style={{
|
|
4777
|
-
display: "inline-flex",
|
|
4778
|
-
alignItems: "center",
|
|
4779
|
-
gap: 5,
|
|
4780
|
-
fontSize: 11,
|
|
4781
|
-
padding: "3px 9px",
|
|
4782
|
-
borderRadius: 4,
|
|
4783
|
-
background: "var(--af-border-subtle)",
|
|
4784
|
-
color: "var(--af-text)",
|
|
4785
|
-
}}
|
|
4786
|
-
>
|
|
4787
|
-
<b style={{ fontWeight: 600, fontFamily: "var(--font-mono)" }}>{p.title}</b>
|
|
4788
|
-
{p.detail && (
|
|
4789
|
-
<span style={{ color: "var(--af-text-tertiary)", maxWidth: 360, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
4790
|
-
{p.detail}
|
|
4791
|
-
</span>
|
|
4792
|
-
)}
|
|
4793
|
-
</span>
|
|
4794
|
-
))}
|
|
4795
|
-
</div>
|
|
4796
|
-
</div>
|
|
4797
|
-
)
|
|
4798
|
-
)}
|
|
4799
|
-
|
|
4800
|
-
{w.logs.length > 0 && <WorkflowRunLog logs={w.logs} />}
|
|
4801
|
-
</div>
|
|
4802
2831
|
)}
|
|
4803
2832
|
</div>
|
|
4804
2833
|
);
|
|
4805
2834
|
}
|
|
4806
2835
|
|
|
4807
|
-
/**
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
.replace(/-\d{6,}$/, "")
|
|
4813
|
-
.trim();
|
|
4814
|
-
}
|
|
4815
|
-
|
|
4816
|
-
/** Status dot color for a single workflow-spawned agent. */
|
|
4817
|
-
function agentStateColor(state?: string): string {
|
|
4818
|
-
const s = (state ?? "").toLowerCase();
|
|
4819
|
-
if (s === "done" || s === "completed" || s === "success") return "#10B981";
|
|
4820
|
-
if (s === "error" || s === "failed") return "#EF4444";
|
|
4821
|
-
if (s === "running" || s === "active" || s === "in_progress") return "#F59E0B";
|
|
4822
|
-
return "#8A8580";
|
|
4823
|
-
}
|
|
4824
|
-
|
|
4825
|
-
/** Per-phase tabs for a workflow run. Each tab lists the agents (actions)
|
|
4826
|
-
* that ran in that phase so you can review what actually happened. */
|
|
4827
|
-
function WorkflowPhaseTabs({
|
|
4828
|
-
w,
|
|
4829
|
-
selectedAgentKey,
|
|
4830
|
-
onOpenAgent,
|
|
4831
|
-
}: {
|
|
4832
|
-
w: WorkflowRun;
|
|
4833
|
-
selectedAgentKey?: string | null;
|
|
4834
|
-
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
4835
|
-
}) {
|
|
4836
|
-
// Build the phase list from meta.phases (ordered), bucketing agents by
|
|
4837
|
-
// phaseIndex. Agents whose phaseIndex isn't in meta.phases fall into an
|
|
4838
|
-
// appended bucket keyed by their phaseTitle so nothing is dropped.
|
|
4839
|
-
const groups = useMemo(() => {
|
|
4840
|
-
const byIndex = new Map<number, WorkflowRun["agents"]>();
|
|
4841
|
-
const extras = new Map<string, WorkflowRun["agents"]>();
|
|
4842
|
-
for (const a of w.agents) {
|
|
4843
|
-
if (a.phaseIndex && a.phaseIndex >= 1 && a.phaseIndex <= w.phases.length) {
|
|
4844
|
-
const arr = byIndex.get(a.phaseIndex) ?? [];
|
|
4845
|
-
arr.push(a);
|
|
4846
|
-
byIndex.set(a.phaseIndex, arr);
|
|
4847
|
-
} else {
|
|
4848
|
-
const key = a.phaseTitle ?? "Other";
|
|
4849
|
-
const arr = extras.get(key) ?? [];
|
|
4850
|
-
arr.push(a);
|
|
4851
|
-
extras.set(key, arr);
|
|
4852
|
-
}
|
|
4853
|
-
}
|
|
4854
|
-
const out = w.phases.map((p, i) => ({
|
|
4855
|
-
key: `p${i + 1}`,
|
|
4856
|
-
title: p.title,
|
|
4857
|
-
detail: p.detail,
|
|
4858
|
-
agents: byIndex.get(i + 1) ?? [],
|
|
4859
|
-
}));
|
|
4860
|
-
for (const [title, agents] of extras) {
|
|
4861
|
-
out.push({ key: `x-${title}`, title, detail: undefined, agents });
|
|
4862
|
-
}
|
|
4863
|
-
return out;
|
|
4864
|
-
}, [w]);
|
|
4865
|
-
|
|
4866
|
-
// Default to the first phase that actually has agents.
|
|
4867
|
-
const firstWithAgents = groups.find((g) => g.agents.length > 0)?.key ?? groups[0]?.key ?? "";
|
|
4868
|
-
const [active, setActive] = useState(firstWithAgents);
|
|
4869
|
-
const activeGroup = groups.find((g) => g.key === active) ?? groups[0];
|
|
4870
|
-
|
|
2836
|
+
/** Matching collapse control at the bottom of an expanded turn. Mirrors
|
|
2837
|
+
* the top ExpandedTurnHeader so after reading through the inner rows
|
|
2838
|
+
* the user has a collapse button right where their eye lands. */
|
|
2839
|
+
function ExpandedTurnFooter({ turn, onClick }: { turn: TurnMegaRow; onClick: () => void }) {
|
|
2840
|
+
const s = turn.summary;
|
|
4871
2841
|
return (
|
|
4872
|
-
<div
|
|
4873
|
-
|
|
4874
|
-
{
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
background: isActive ? "#EA580C" : "var(--af-border-subtle)",
|
|
4906
|
-
color: isActive ? "#fff" : "var(--af-text-tertiary)",
|
|
4907
|
-
}}
|
|
4908
|
-
>
|
|
4909
|
-
{g.agents.length}
|
|
4910
|
-
</span>
|
|
4911
|
-
</button>
|
|
4912
|
-
);
|
|
4913
|
-
})}
|
|
4914
|
-
</div>
|
|
4915
|
-
|
|
4916
|
-
{/* Active phase body */}
|
|
4917
|
-
{activeGroup && (
|
|
4918
|
-
<div>
|
|
4919
|
-
{activeGroup.detail && (
|
|
4920
|
-
<div
|
|
4921
|
-
style={{
|
|
4922
|
-
fontSize: 11.5,
|
|
4923
|
-
color: "var(--af-text-tertiary)",
|
|
4924
|
-
marginBottom: 8,
|
|
4925
|
-
fontStyle: "italic",
|
|
4926
|
-
}}
|
|
4927
|
-
>
|
|
4928
|
-
{activeGroup.detail}
|
|
4929
|
-
</div>
|
|
4930
|
-
)}
|
|
4931
|
-
{activeGroup.agents.length === 0 ? (
|
|
4932
|
-
<div style={{ fontSize: 12, color: "var(--af-text-tertiary)", padding: "8px 0" }}>
|
|
4933
|
-
No agents recorded for this phase.
|
|
4934
|
-
</div>
|
|
4935
|
-
) : (
|
|
4936
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
4937
|
-
{activeGroup.agents.map((a) => (
|
|
4938
|
-
<WorkflowAgentRow
|
|
4939
|
-
key={`${a.index}-${a.agentId ?? a.label}`}
|
|
4940
|
-
a={a}
|
|
4941
|
-
selected={selectedAgentKey === `${w.runId}:${a.agentId ?? a.index}`}
|
|
4942
|
-
onOpen={() => onOpenAgent(w.runId, a)}
|
|
4943
|
-
/>
|
|
4944
|
-
))}
|
|
4945
|
-
</div>
|
|
4946
|
-
)}
|
|
4947
|
-
</div>
|
|
4948
|
-
)}
|
|
2842
|
+
<div
|
|
2843
|
+
onClick={onClick}
|
|
2844
|
+
style={{
|
|
2845
|
+
display: "flex",
|
|
2846
|
+
alignItems: "center",
|
|
2847
|
+
gap: 8,
|
|
2848
|
+
padding: "7px 12px 7px 18px",
|
|
2849
|
+
marginLeft: 12,
|
|
2850
|
+
marginBottom: 8,
|
|
2851
|
+
borderLeft: "2px solid var(--af-accent)",
|
|
2852
|
+
borderTop: "1px dashed var(--af-border-subtle)",
|
|
2853
|
+
borderBottomLeftRadius: 6,
|
|
2854
|
+
borderBottomRightRadius: 6,
|
|
2855
|
+
fontSize: 11,
|
|
2856
|
+
color: "var(--af-text-tertiary)",
|
|
2857
|
+
cursor: "pointer",
|
|
2858
|
+
background: "rgba(92, 132, 195, 0.04)",
|
|
2859
|
+
}}
|
|
2860
|
+
onMouseEnter={(e) => {
|
|
2861
|
+
e.currentTarget.style.background = "rgba(92, 132, 195, 0.10)";
|
|
2862
|
+
e.currentTarget.style.color = "var(--af-text-secondary)";
|
|
2863
|
+
}}
|
|
2864
|
+
onMouseLeave={(e) => {
|
|
2865
|
+
e.currentTarget.style.background = "rgba(92, 132, 195, 0.04)";
|
|
2866
|
+
e.currentTarget.style.color = "var(--af-text-tertiary)";
|
|
2867
|
+
}}
|
|
2868
|
+
>
|
|
2869
|
+
<ChevronUp size={13} />
|
|
2870
|
+
<span>
|
|
2871
|
+
Collapse turn · {s.agentMessages} message{s.agentMessages === 1 ? "" : "s"} · {s.toolCalls}{" "}
|
|
2872
|
+
tool{s.toolCalls === 1 ? "" : "s"}
|
|
2873
|
+
{turn.durationMs !== undefined ? ` · ${formatGap(turn.durationMs)}` : ""}
|
|
2874
|
+
</span>
|
|
4949
2875
|
</div>
|
|
4950
2876
|
);
|
|
4951
2877
|
}
|
|
4952
2878
|
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
/** Full transcript detail for one workflow agent — fetched on demand from
|
|
4956
|
-
* /api/workflow-agent. Mirrors the parser's WorkflowAgentDetail. */
|
|
4957
|
-
type WfAgentDetail = {
|
|
4958
|
-
prompt?: string;
|
|
4959
|
-
finalText?: string;
|
|
4960
|
-
steps: { index: number; tool: string; preview: string; full?: string; isError?: boolean }[];
|
|
4961
|
-
toolCalls: { name: string; count: number }[];
|
|
4962
|
-
toolCallCount: number;
|
|
4963
|
-
assistantMessageCount: number;
|
|
4964
|
-
eventCount: number;
|
|
4965
|
-
totalUsage: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
4966
|
-
model?: string;
|
|
4967
|
-
durationMs?: number;
|
|
4968
|
-
};
|
|
4969
|
-
|
|
4970
|
-
function WorkflowAgentRow({
|
|
4971
|
-
a,
|
|
2879
|
+
function TranscriptRow({
|
|
2880
|
+
row,
|
|
4972
2881
|
selected,
|
|
4973
|
-
|
|
2882
|
+
onSelect,
|
|
2883
|
+
refCb,
|
|
2884
|
+
stickyOffset,
|
|
2885
|
+
indented,
|
|
4974
2886
|
}: {
|
|
4975
|
-
|
|
2887
|
+
row: PresentationRow;
|
|
4976
2888
|
selected: boolean;
|
|
4977
|
-
|
|
2889
|
+
onSelect: () => void;
|
|
2890
|
+
refCb: (el: HTMLDivElement | null) => void;
|
|
2891
|
+
stickyOffset: number;
|
|
2892
|
+
indented?: boolean;
|
|
4978
2893
|
}) {
|
|
4979
|
-
const
|
|
2894
|
+
const theme = ROLE_THEMES[row.kind];
|
|
2895
|
+
const event = row.kind === "tool-group" ? row.events[0] : row.event;
|
|
2896
|
+
const usage = event.usage;
|
|
2897
|
+
const hasUsage = row.kind === "agent" && usage && (usage.input > 0 || usage.output > 0);
|
|
2898
|
+
const isTeammateMsg = row.kind === "user" && !!row.event.teammateMessage;
|
|
2899
|
+
const roleLabel = isTeammateMsg ? "Team Lead" : theme.label;
|
|
2900
|
+
|
|
2901
|
+
const preview = rowPreview(row);
|
|
2902
|
+
|
|
4980
2903
|
return (
|
|
4981
|
-
<
|
|
4982
|
-
|
|
2904
|
+
<div
|
|
2905
|
+
ref={refCb}
|
|
2906
|
+
onClick={onSelect}
|
|
2907
|
+
data-sl-row-index={row.kind === "tool-group" ? row.events[0].index : row.event.index}
|
|
2908
|
+
data-sl-toffset={row.tOffsetMs ?? 0}
|
|
4983
2909
|
style={{
|
|
4984
|
-
|
|
4985
|
-
|
|
2910
|
+
display: "grid",
|
|
2911
|
+
// Empty 20px prefix column keeps the role pill at the same x-offset
|
|
2912
|
+
// as collapsed turn rows (which have a chevron there). Ensures the
|
|
2913
|
+
// User/Agent/Tool tags align vertically in the transcript.
|
|
2914
|
+
gridTemplateColumns: "20px 84px 1fr auto auto",
|
|
2915
|
+
gap: 14,
|
|
4986
2916
|
alignItems: "center",
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
2917
|
+
padding: "11px 12px",
|
|
2918
|
+
paddingLeft: indented ? 28 : 12,
|
|
2919
|
+
borderBottom: "1px solid var(--af-border-subtle)",
|
|
2920
|
+
borderLeft: indented ? "2px solid var(--af-accent)" : "2px solid transparent",
|
|
2921
|
+
marginLeft: indented ? 12 : 0,
|
|
4990
2922
|
cursor: "pointer",
|
|
4991
|
-
border: "1px solid",
|
|
4992
|
-
borderColor: selected ? "#EA580C" : "var(--af-border-subtle)",
|
|
4993
|
-
borderRadius: 6,
|
|
4994
2923
|
background: selected
|
|
4995
|
-
? "
|
|
4996
|
-
:
|
|
4997
|
-
|
|
4998
|
-
|
|
2924
|
+
? "var(--af-accent-subtle)"
|
|
2925
|
+
: indented
|
|
2926
|
+
? "rgba(92, 132, 195, 0.03)"
|
|
2927
|
+
: "transparent",
|
|
2928
|
+
transition: "background 0.08s",
|
|
2929
|
+
scrollMarginTop: stickyOffset,
|
|
2930
|
+
}}
|
|
2931
|
+
onMouseEnter={(e) => {
|
|
2932
|
+
if (!selected) e.currentTarget.style.background = "var(--af-surface-hover)";
|
|
2933
|
+
}}
|
|
2934
|
+
onMouseLeave={(e) => {
|
|
2935
|
+
if (!selected) e.currentTarget.style.background = "transparent";
|
|
4999
2936
|
}}
|
|
5000
2937
|
>
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
/>
|
|
2938
|
+
{/* Empty chevron-slot column — keeps the pill column aligned with
|
|
2939
|
+
collapsed-turn rows that do render a chevron. */}
|
|
2940
|
+
<span />
|
|
5005
2941
|
<span
|
|
5006
2942
|
style={{
|
|
5007
|
-
|
|
5008
|
-
fontSize:
|
|
2943
|
+
justifySelf: "start",
|
|
2944
|
+
fontSize: 11,
|
|
5009
2945
|
fontWeight: 600,
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
whiteSpace: "nowrap",
|
|
5015
|
-
maxWidth: 280,
|
|
2946
|
+
padding: "3px 10px",
|
|
2947
|
+
borderRadius: 4,
|
|
2948
|
+
background: theme.bg,
|
|
2949
|
+
color: theme.fg,
|
|
5016
2950
|
}}
|
|
5017
|
-
title={a.label}
|
|
5018
2951
|
>
|
|
5019
|
-
{
|
|
5020
|
-
</span>
|
|
5021
|
-
{a.model && (
|
|
5022
|
-
<span style={{ fontSize: 10, color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)" }}>
|
|
5023
|
-
{shortenModel(a.model)}
|
|
5024
|
-
</span>
|
|
5025
|
-
)}
|
|
5026
|
-
{a.lastToolSummary && (
|
|
5027
|
-
<span
|
|
5028
|
-
style={{
|
|
5029
|
-
fontSize: 11,
|
|
5030
|
-
color: "var(--af-text-tertiary)",
|
|
5031
|
-
overflow: "hidden",
|
|
5032
|
-
textOverflow: "ellipsis",
|
|
5033
|
-
whiteSpace: "nowrap",
|
|
5034
|
-
maxWidth: 240,
|
|
5035
|
-
}}
|
|
5036
|
-
title={a.lastToolSummary}
|
|
5037
|
-
>
|
|
5038
|
-
→ {a.lastToolSummary}
|
|
5039
|
-
</span>
|
|
5040
|
-
)}
|
|
5041
|
-
<span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
|
|
5042
|
-
{dur && <WfMiniStat icon={<Clock size={10} />} value={dur} />}
|
|
5043
|
-
{a.tokens !== undefined && <WfMiniStat value={`${formatTokens(a.tokens)} tok`} />}
|
|
5044
|
-
{a.toolCalls !== undefined && <WfMiniStat icon={<Wrench size={10} />} value={`${a.toolCalls}`} />}
|
|
5045
|
-
<span style={{ color: selected ? "#EA580C" : "var(--af-text-tertiary)", display: "inline-flex" }}>
|
|
5046
|
-
<ChevronRight size={14} />
|
|
5047
|
-
</span>
|
|
2952
|
+
{roleLabel}
|
|
5048
2953
|
</span>
|
|
5049
|
-
</button>
|
|
5050
|
-
);
|
|
5051
|
-
}
|
|
5052
2954
|
|
|
5053
|
-
|
|
5054
|
-
* ordered Steps, and Result — fetched on demand. Kept out of the card so the
|
|
5055
|
-
* Workflows tab stays short even for 100+ step agents. */
|
|
5056
|
-
function WorkflowAgentDrawer({
|
|
5057
|
-
sessionId,
|
|
5058
|
-
runId,
|
|
5059
|
-
agent,
|
|
5060
|
-
onClose,
|
|
5061
|
-
}: {
|
|
5062
|
-
sessionId: string;
|
|
5063
|
-
runId: string;
|
|
5064
|
-
agent: WorkflowRun["agents"][number];
|
|
5065
|
-
onClose: () => void;
|
|
5066
|
-
}) {
|
|
5067
|
-
const [detail, setDetail] = useState<WfAgentDetail | null>(null);
|
|
5068
|
-
const [loading, setLoading] = useState(false);
|
|
5069
|
-
const [failed, setFailed] = useState(false);
|
|
5070
|
-
|
|
5071
|
-
useEffect(() => {
|
|
5072
|
-
setDetail(null);
|
|
5073
|
-
setFailed(false);
|
|
5074
|
-
if (!agent.agentId) return;
|
|
5075
|
-
setLoading(true);
|
|
5076
|
-
const ctrl = new AbortController();
|
|
5077
|
-
const url = `/api/workflow-agent?session=${encodeURIComponent(sessionId)}&run=${encodeURIComponent(runId)}&agent=${encodeURIComponent(agent.agentId)}`;
|
|
5078
|
-
fetch(url, { signal: ctrl.signal })
|
|
5079
|
-
.then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
|
|
5080
|
-
.then((d: WfAgentDetail) => setDetail(d))
|
|
5081
|
-
.catch((e) => {
|
|
5082
|
-
if (e?.name !== "AbortError") setFailed(true);
|
|
5083
|
-
})
|
|
5084
|
-
.finally(() => setLoading(false));
|
|
5085
|
-
return () => ctrl.abort();
|
|
5086
|
-
}, [sessionId, runId, agent.agentId]);
|
|
5087
|
-
|
|
5088
|
-
const prompt = detail?.prompt ?? agent.promptPreview;
|
|
5089
|
-
const finalText = detail?.finalText ?? agent.resultPreview;
|
|
5090
|
-
const dur = detail?.durationMs ?? agent.durationMs;
|
|
5091
|
-
const model = detail?.model ?? agent.model;
|
|
5092
|
-
const toolCount = detail?.toolCallCount ?? agent.toolCalls;
|
|
5093
|
-
const tok = detail ? detail.totalUsage.input + detail.totalUsage.output : agent.tokens;
|
|
5094
|
-
|
|
5095
|
-
return (
|
|
5096
|
-
<div>
|
|
5097
|
-
{/* Sticky title bar */}
|
|
5098
|
-
<div
|
|
2955
|
+
<span
|
|
5099
2956
|
style={{
|
|
5100
|
-
|
|
5101
|
-
|
|
2957
|
+
fontSize: 13,
|
|
2958
|
+
color: row.kind === "error" ? "var(--af-danger)" : "var(--af-text)",
|
|
2959
|
+
overflow: "hidden",
|
|
2960
|
+
textOverflow: "ellipsis",
|
|
2961
|
+
whiteSpace: "nowrap",
|
|
5102
2962
|
display: "flex",
|
|
5103
2963
|
alignItems: "center",
|
|
5104
|
-
gap:
|
|
5105
|
-
background: "var(--af-surface)",
|
|
5106
|
-
position: "sticky",
|
|
5107
|
-
top: 0,
|
|
5108
|
-
zIndex: 1,
|
|
2964
|
+
gap: 8,
|
|
5109
2965
|
}}
|
|
5110
2966
|
>
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
/>
|
|
2967
|
+
{row.kind === "tool-group" && (
|
|
2968
|
+
<Wrench size={13} style={{ color: "var(--af-text-tertiary)", flexShrink: 0 }} />
|
|
2969
|
+
)}
|
|
5115
2970
|
<span
|
|
5116
2971
|
style={{
|
|
5117
|
-
fontSize: 13.5,
|
|
5118
|
-
fontWeight: 600,
|
|
5119
|
-
fontFamily: "var(--font-mono)",
|
|
5120
|
-
color: "var(--af-text)",
|
|
5121
|
-
flex: 1,
|
|
5122
|
-
minWidth: 0,
|
|
5123
2972
|
overflow: "hidden",
|
|
5124
2973
|
textOverflow: "ellipsis",
|
|
5125
2974
|
whiteSpace: "nowrap",
|
|
5126
2975
|
}}
|
|
5127
|
-
title={agent.label}
|
|
5128
2976
|
>
|
|
5129
|
-
{
|
|
2977
|
+
{row.kind === "tool-group" ? (
|
|
2978
|
+
<ToolGroupLabel toolNames={row.toolNames} count={row.count} />
|
|
2979
|
+
) : (
|
|
2980
|
+
preview
|
|
2981
|
+
)}
|
|
5130
2982
|
</span>
|
|
5131
|
-
|
|
5132
|
-
onClick={onClose}
|
|
5133
|
-
style={{ background: "transparent", border: "none", cursor: "pointer", color: "var(--af-text-tertiary)", padding: 4 }}
|
|
5134
|
-
aria-label="Close"
|
|
5135
|
-
>
|
|
5136
|
-
<X size={16} />
|
|
5137
|
-
</button>
|
|
5138
|
-
</div>
|
|
2983
|
+
</span>
|
|
5139
2984
|
|
|
5140
|
-
{
|
|
5141
|
-
|
|
2985
|
+
{hasUsage && usage ? <TokenChip usage={usage} /> : <span />}
|
|
2986
|
+
|
|
2987
|
+
<span
|
|
5142
2988
|
style={{
|
|
5143
|
-
padding: "9px 18px",
|
|
5144
2989
|
fontFamily: "var(--font-mono)",
|
|
5145
2990
|
fontSize: 11,
|
|
5146
2991
|
color: "var(--af-text-tertiary)",
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
gap: 12,
|
|
5150
|
-
flexWrap: "wrap",
|
|
2992
|
+
minWidth: 64,
|
|
2993
|
+
textAlign: "right",
|
|
5151
2994
|
}}
|
|
5152
2995
|
>
|
|
5153
|
-
{
|
|
5154
|
-
|
|
5155
|
-
{model && <span>· {shortenModel(model)}</span>}
|
|
5156
|
-
{dur !== undefined && <span>· {formatGap(dur)}</span>}
|
|
5157
|
-
{toolCount !== undefined && <span>· {toolCount} tools</span>}
|
|
5158
|
-
{tok !== undefined && <span>· {formatTokens(tok)} tok</span>}
|
|
5159
|
-
</div>
|
|
5160
|
-
|
|
5161
|
-
{/* Body — each section collapsed by default; click to expand. */}
|
|
5162
|
-
<div style={{ padding: "12px 18px 18px", display: "flex", flexDirection: "column", gap: 8 }}>
|
|
5163
|
-
{loading && (
|
|
5164
|
-
<div style={{ fontSize: 11.5, color: "var(--af-text-tertiary)", fontStyle: "italic" }}>
|
|
5165
|
-
Loading full transcript…
|
|
5166
|
-
</div>
|
|
5167
|
-
)}
|
|
5168
|
-
{failed && (
|
|
5169
|
-
<div style={{ fontSize: 11.5, color: "var(--af-text-tertiary)", fontStyle: "italic" }}>
|
|
5170
|
-
Full transcript unavailable — showing the journal summary.
|
|
5171
|
-
</div>
|
|
5172
|
-
)}
|
|
5173
|
-
|
|
5174
|
-
{prompt && (
|
|
5175
|
-
<DrawerCollapsible title="Task" hint={detail ? undefined : "preview"}>
|
|
5176
|
-
<div style={{ fontSize: 12, color: "var(--af-text-secondary)", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>
|
|
5177
|
-
{prompt}
|
|
5178
|
-
</div>
|
|
5179
|
-
</DrawerCollapsible>
|
|
5180
|
-
)}
|
|
5181
|
-
|
|
5182
|
-
{detail && detail.steps.length > 0 && (
|
|
5183
|
-
<DrawerCollapsible
|
|
5184
|
-
title="Steps"
|
|
5185
|
-
count={detail.steps.length}
|
|
5186
|
-
hint={detail.toolCalls.slice(0, 6).map((t) => `${shortenToolName(t.name)} ×${t.count}`).join(" · ")}
|
|
5187
|
-
>
|
|
5188
|
-
<div style={{ border: "1px solid var(--af-border-subtle)", borderRadius: 6, background: "var(--af-surface)" }}>
|
|
5189
|
-
{detail.steps.map((s) => (
|
|
5190
|
-
<WorkflowStepRow key={s.index} s={s} />
|
|
5191
|
-
))}
|
|
5192
|
-
</div>
|
|
5193
|
-
</DrawerCollapsible>
|
|
5194
|
-
)}
|
|
5195
|
-
|
|
5196
|
-
{finalText && (
|
|
5197
|
-
<DrawerCollapsible title="Result" hint={detail ? undefined : "preview"}>
|
|
5198
|
-
<div
|
|
5199
|
-
style={{
|
|
5200
|
-
fontFamily: "var(--font-mono)",
|
|
5201
|
-
fontSize: 11,
|
|
5202
|
-
color: "var(--af-text-secondary)",
|
|
5203
|
-
whiteSpace: "pre-wrap",
|
|
5204
|
-
wordBreak: "break-word",
|
|
5205
|
-
lineHeight: 1.5,
|
|
5206
|
-
background: "var(--af-surface)",
|
|
5207
|
-
border: "1px solid var(--af-border-subtle)",
|
|
5208
|
-
borderRadius: 4,
|
|
5209
|
-
padding: "8px 10px",
|
|
5210
|
-
}}
|
|
5211
|
-
>
|
|
5212
|
-
{finalText}
|
|
5213
|
-
</div>
|
|
5214
|
-
</DrawerCollapsible>
|
|
5215
|
-
)}
|
|
5216
|
-
</div>
|
|
2996
|
+
{formatOffset(row.tOffsetMs)}
|
|
2997
|
+
</span>
|
|
5217
2998
|
</div>
|
|
5218
2999
|
);
|
|
5219
3000
|
}
|
|
5220
3001
|
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
3002
|
+
/* -------- Token chip w/ hover tooltip -------- */
|
|
3003
|
+
|
|
3004
|
+
/** Pretty inline label for a collapsed tool-group row. Shows unique tool
|
|
3005
|
+
* names with bolded names and subtle "×N" counts. */
|
|
3006
|
+
function ToolGroupLabel({
|
|
3007
|
+
toolNames,
|
|
5224
3008
|
count,
|
|
5225
|
-
hint,
|
|
5226
|
-
defaultOpen = false,
|
|
5227
|
-
children,
|
|
5228
3009
|
}: {
|
|
5229
|
-
|
|
5230
|
-
count
|
|
5231
|
-
hint?: string;
|
|
5232
|
-
defaultOpen?: boolean;
|
|
5233
|
-
children: React.ReactNode;
|
|
3010
|
+
toolNames: { name: string; count: number }[];
|
|
3011
|
+
count: number;
|
|
5234
3012
|
}) {
|
|
5235
|
-
const
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
<button
|
|
5239
|
-
onClick={() => setOpen((v) => !v)}
|
|
5240
|
-
style={{
|
|
5241
|
-
width: "100%",
|
|
5242
|
-
display: "flex",
|
|
5243
|
-
alignItems: "center",
|
|
5244
|
-
gap: 8,
|
|
5245
|
-
padding: "9px 12px",
|
|
5246
|
-
background: open ? "var(--af-surface-subtle, rgba(120,115,108,0.05))" : "transparent",
|
|
5247
|
-
border: "none",
|
|
5248
|
-
cursor: "pointer",
|
|
5249
|
-
textAlign: "left",
|
|
5250
|
-
color: "inherit",
|
|
5251
|
-
}}
|
|
5252
|
-
>
|
|
5253
|
-
<span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>
|
|
5254
|
-
{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
5255
|
-
</span>
|
|
5256
|
-
<span style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--af-text-secondary)" }}>
|
|
5257
|
-
{title}
|
|
5258
|
-
</span>
|
|
5259
|
-
{count !== undefined && (
|
|
5260
|
-
<span
|
|
5261
|
-
style={{
|
|
5262
|
-
fontSize: 10,
|
|
5263
|
-
fontWeight: 700,
|
|
5264
|
-
padding: "1px 7px",
|
|
5265
|
-
borderRadius: 100,
|
|
5266
|
-
background: "rgba(234,88,12,0.14)",
|
|
5267
|
-
color: "#EA580C",
|
|
5268
|
-
fontFamily: "var(--font-mono)",
|
|
5269
|
-
}}
|
|
5270
|
-
>
|
|
5271
|
-
{count}
|
|
5272
|
-
</span>
|
|
5273
|
-
)}
|
|
5274
|
-
{hint && (
|
|
5275
|
-
<span
|
|
5276
|
-
style={{
|
|
5277
|
-
marginLeft: "auto",
|
|
5278
|
-
fontSize: 10.5,
|
|
5279
|
-
color: "var(--af-text-tertiary)",
|
|
5280
|
-
fontFamily: "var(--font-mono)",
|
|
5281
|
-
overflow: "hidden",
|
|
5282
|
-
textOverflow: "ellipsis",
|
|
5283
|
-
whiteSpace: "nowrap",
|
|
5284
|
-
maxWidth: 280,
|
|
5285
|
-
}}
|
|
5286
|
-
>
|
|
5287
|
-
{hint}
|
|
5288
|
-
</span>
|
|
5289
|
-
)}
|
|
5290
|
-
</button>
|
|
5291
|
-
{open && <div style={{ padding: "0 12px 12px" }}>{children}</div>}
|
|
5292
|
-
</div>
|
|
5293
|
-
);
|
|
5294
|
-
}
|
|
5295
|
-
|
|
5296
|
-
/** One step row — click to reveal the full tool input (e.g. the complete
|
|
5297
|
-
* multi-line bash command) when it's longer than the one-line preview. */
|
|
5298
|
-
function WorkflowStepRow({ s }: { s: WfAgentDetail["steps"][number] }) {
|
|
5299
|
-
const [open, setOpen] = useState(false);
|
|
5300
|
-
const expandable = !!s.full;
|
|
3013
|
+
const MAX = 4;
|
|
3014
|
+
const shown = toolNames.slice(0, MAX);
|
|
3015
|
+
const overflow = toolNames.length - MAX;
|
|
5301
3016
|
return (
|
|
5302
|
-
<
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
{shortenToolName(s.tool)}
|
|
3017
|
+
<span>
|
|
3018
|
+
{shown.map((t, i) => (
|
|
3019
|
+
<span key={t.name}>
|
|
3020
|
+
{i > 0 && <span style={{ color: "var(--af-text-tertiary)", margin: "0 6px" }}>·</span>}
|
|
3021
|
+
<b>{shortenToolName(t.name)}</b>
|
|
3022
|
+
{t.count > 1 && (
|
|
3023
|
+
<span
|
|
3024
|
+
style={{
|
|
3025
|
+
color: "var(--af-text-tertiary)",
|
|
3026
|
+
fontWeight: 400,
|
|
3027
|
+
marginLeft: 4,
|
|
3028
|
+
fontFamily: "var(--font-mono)",
|
|
3029
|
+
fontSize: 11,
|
|
3030
|
+
}}
|
|
3031
|
+
>
|
|
3032
|
+
×{t.count}
|
|
3033
|
+
</span>
|
|
3034
|
+
)}
|
|
5321
3035
|
</span>
|
|
3036
|
+
))}
|
|
3037
|
+
{overflow > 0 && (
|
|
5322
3038
|
<span
|
|
5323
|
-
style={{ flex: 1, color: "var(--af-text-tertiary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
|
5324
|
-
title={s.preview}
|
|
5325
|
-
>
|
|
5326
|
-
{s.preview}
|
|
5327
|
-
</span>
|
|
5328
|
-
{expandable && (
|
|
5329
|
-
<span style={{ color: "var(--af-text-tertiary)", display: "inline-flex", flexShrink: 0 }}>
|
|
5330
|
-
{open ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
|
|
5331
|
-
</span>
|
|
5332
|
-
)}
|
|
5333
|
-
</div>
|
|
5334
|
-
{open && s.full && (
|
|
5335
|
-
<pre
|
|
5336
3039
|
style={{
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
fontFamily: "var(--font-mono)",
|
|
3040
|
+
color: "var(--af-text-tertiary)",
|
|
3041
|
+
marginLeft: 8,
|
|
5340
3042
|
fontSize: 11,
|
|
5341
|
-
whiteSpace: "pre-wrap",
|
|
5342
|
-
wordBreak: "break-word",
|
|
5343
|
-
color: "var(--af-text-secondary)",
|
|
5344
|
-
background: "var(--af-surface-subtle, rgba(120,115,108,0.05))",
|
|
5345
|
-
lineHeight: 1.5,
|
|
5346
3043
|
}}
|
|
5347
3044
|
>
|
|
5348
|
-
{
|
|
5349
|
-
</
|
|
3045
|
+
+{overflow} more
|
|
3046
|
+
</span>
|
|
5350
3047
|
)}
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
}
|
|
5354
|
-
|
|
5355
|
-
/** Collapsible coarse run log (▶ / ✓ task markers) — secondary to the
|
|
5356
|
-
* per-phase agent view. */
|
|
5357
|
-
function WorkflowRunLog({ logs }: { logs: string[] }) {
|
|
5358
|
-
const [open, setOpen] = useState(false);
|
|
5359
|
-
return (
|
|
5360
|
-
<div>
|
|
5361
|
-
<button
|
|
5362
|
-
onClick={() => setOpen((v) => !v)}
|
|
5363
|
-
style={{
|
|
5364
|
-
display: "inline-flex",
|
|
5365
|
-
alignItems: "center",
|
|
5366
|
-
gap: 5,
|
|
5367
|
-
fontSize: 10,
|
|
5368
|
-
fontWeight: 600,
|
|
5369
|
-
textTransform: "uppercase",
|
|
5370
|
-
letterSpacing: "0.04em",
|
|
5371
|
-
color: "var(--af-text-tertiary)",
|
|
5372
|
-
background: "transparent",
|
|
5373
|
-
border: "none",
|
|
5374
|
-
cursor: "pointer",
|
|
5375
|
-
padding: 0,
|
|
5376
|
-
}}
|
|
5377
|
-
>
|
|
5378
|
-
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
5379
|
-
Run log ({logs.length})
|
|
5380
|
-
</button>
|
|
5381
|
-
{open && (
|
|
5382
|
-
<div
|
|
3048
|
+
{count > 3 && (
|
|
3049
|
+
<span
|
|
5383
3050
|
style={{
|
|
5384
|
-
|
|
3051
|
+
color: "var(--af-text-tertiary)",
|
|
3052
|
+
marginLeft: 10,
|
|
3053
|
+
fontSize: 11,
|
|
5385
3054
|
fontFamily: "var(--font-mono)",
|
|
5386
|
-
fontSize: 11.5,
|
|
5387
|
-
lineHeight: 1.7,
|
|
5388
|
-
color: "var(--af-text-secondary)",
|
|
5389
|
-
background: "var(--af-surface-subtle, rgba(120,115,108,0.05))",
|
|
5390
|
-
border: "1px solid var(--af-border-subtle)",
|
|
5391
|
-
borderRadius: 6,
|
|
5392
|
-
padding: "8px 12px",
|
|
5393
|
-
maxHeight: 240,
|
|
5394
|
-
overflowY: "auto",
|
|
5395
|
-
whiteSpace: "pre-wrap",
|
|
5396
3055
|
}}
|
|
5397
3056
|
>
|
|
5398
|
-
{
|
|
5399
|
-
|
|
5400
|
-
))}
|
|
5401
|
-
</div>
|
|
3057
|
+
({count} calls)
|
|
3058
|
+
</span>
|
|
5402
3059
|
)}
|
|
5403
|
-
</
|
|
3060
|
+
</span>
|
|
5404
3061
|
);
|
|
5405
3062
|
}
|
|
5406
3063
|
|
|
3064
|
+
// TokenChip, TurnTokenChip moved to ./session-view/token-stats.tsx
|
|
3065
|
+
|
|
3066
|
+
/* ------------------------------------------------------------------ */
|
|
3067
|
+
/* Debug list */
|
|
3068
|
+
/* ------------------------------------------------------------------ */
|
|
3069
|
+
|
|
3070
|
+
// DebugList moved to ./session-view/debug-list.tsx
|
|
3071
|
+
|
|
3072
|
+
/* ------------------------------------------------------------------ */
|
|
3073
|
+
/* SubagentDrawer */
|
|
3074
|
+
/* */
|
|
3075
|
+
/* Opened when the user clicks a sub-agent lane bar on the mini-map. */
|
|
3076
|
+
/* Renders everything we know about the run — type pill, background */
|
|
3077
|
+
/* badge, timing range, duration, stats, the full prompt the parent */
|
|
3078
|
+
/* dispatched, the final agent text, and a "Jump to parent" action */
|
|
3079
|
+
/* that scrolls the transcript to the Agent tool_use row that kicked */
|
|
3080
|
+
/* this subagent off. */
|
|
3081
|
+
/* ------------------------------------------------------------------ */
|
|
3082
|
+
|
|
3083
|
+
// SubagentDrawer moved to ./session-view/subagent-drawer.tsx
|
|
3084
|
+
|
|
3085
|
+
/* ------------------------------------------------------------------ */
|
|
3086
|
+
/* Workflows panel */
|
|
3087
|
+
/* ------------------------------------------------------------------ */
|
|
3088
|
+
|
|
3089
|
+
// WorkflowsPanel, WorkflowCard, WorkflowPhaseTabs, WorkflowAgentRow,
|
|
3090
|
+
// WorkflowAgentDrawer, DrawerCollapsible, WorkflowStepRow, WorkflowRunLog,
|
|
3091
|
+
// shortenModel, agentStateColor, WfAgentDetail moved to ./session-view/workflows-panel.tsx
|
|
3092
|
+
|
|
5407
3093
|
// WfMiniStat, SectionLabel, StatCell, TokenLine moved to ./session-view/workflows-shared.tsx
|
|
5408
3094
|
|
|
5409
3095
|
/* ------------------------------------------------------------------ */
|