fleetlens 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/build-manifest.json +8 -8
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +7 -7
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/changelog/page.js +1 -1
- package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/day/page.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/digest/page.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/insights/page.js +3 -2
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/page.js +1 -1
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/projects/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/runs/page.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/sessions/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/settings/page.js +1 -1
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/team/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/team/page.js +1 -1
- package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page/build-manifest.json +5 -5
- package/app/apps/web/.next/server/app/usage/page.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gril0u._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0n79hpm._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~utv~v._.js +9 -0
- package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0-1_njm._.js → [root-of-the-server]__0k1x-c9._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
- package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
- package/app/apps/web/.next/server/middleware-manifest.json +5 -5
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/{0pzt9vemtzct8.js → 02bv_ejzhhzvp.js} +3 -3
- package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
- package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
- package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
- package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
- package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
- package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
- package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
- package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
- package/app/apps/web/.next/static/chunks/0q8dg4txlzno0.js +1 -0
- package/app/apps/web/.next/static/chunks/{0gblp_v.kiu-..js → 0qv-gw888_5nu.js} +1 -1
- package/app/apps/web/.next/static/chunks/{14bjditmmrn7l.js → 0tirv_vo6q8uc.js} +1 -1
- package/app/apps/web/.next/static/chunks/{0qmh5da6gfm56.js → 0vz4~a~~hq399.js} +1 -1
- package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
- package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
- package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
- package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
- package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
- package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
- package/app/apps/web/app/insights/page.tsx +20 -5
- package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
- package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
- package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
- package/app/apps/web/app/parallelism/gantt-chart.tsx +16 -902
- package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
- package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
- package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
- package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
- package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
- package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
- package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
- package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
- package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
- package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
- package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
- package/app/apps/web/app/sessions/[id]/session-view.tsx +448 -1639
- package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
- package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
- package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
- package/app/apps/web/app/sessions/sessions-grid.tsx +10 -10
- package/app/apps/web/components/digest-actions.tsx +103 -0
- package/app/apps/web/components/job-queue-widget.tsx +1 -1
- package/app/apps/web/components/month-digest-view.tsx +19 -64
- package/app/apps/web/components/searchable-select.tsx +215 -0
- package/app/apps/web/components/tail-mode.tsx +39 -8
- package/app/apps/web/components/week-digest-view.tsx +16 -71
- package/app/apps/web/lib/changelog-data.generated.ts +1 -1
- package/app/apps/web/lib/entries-index.ts +7 -8
- package/app/apps/web/lib/entries.ts +0 -9
- package/app/apps/web/lib/format.test.ts +221 -0
- package/app/apps/web/lib/utilization-tone.test.ts +56 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +35 -43
- package/dist/index.js +37 -49
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
- package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
- package/app/apps/web/.next/static/chunks/05k84gif3ucxm.js +0 -6
- package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
- package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
- package/app/apps/web/.next/static/chunks/0rnay-a.-jr8-.js +0 -1
- package/app/apps/web/.next/static/chunks/0snv3-2ux2xvv.js +0 -6
- /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_ssgManifest.js +0 -0
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import ReactMarkdown from "react-markdown";
|
|
21
21
|
import remarkGfm from "remark-gfm";
|
|
22
22
|
import type {
|
|
23
|
-
ContentBlock,
|
|
24
23
|
SessionDetail,
|
|
25
24
|
SessionEvent,
|
|
26
25
|
SubagentRun,
|
|
@@ -33,13 +32,44 @@ import {
|
|
|
33
32
|
buildPresentation,
|
|
34
33
|
buildMegaRows,
|
|
35
34
|
detectPrMarkers,
|
|
36
|
-
rowPrimaryIndex as rowPrimaryIndexFromLib,
|
|
37
35
|
type MegaRow,
|
|
38
36
|
type PresentationRow,
|
|
39
37
|
type PresentationRowKind,
|
|
40
38
|
type TurnMegaRow,
|
|
41
39
|
type TurnSummary,
|
|
42
40
|
} from "@claude-lens/parser";
|
|
41
|
+
import {
|
|
42
|
+
rowPrimaryIndex,
|
|
43
|
+
type DisplayRow,
|
|
44
|
+
type MinimapSeg,
|
|
45
|
+
} from "./session-view/types";
|
|
46
|
+
import { subagentColor, workflowColor } from "./session-view/colors";
|
|
47
|
+
import {
|
|
48
|
+
formatDayKey,
|
|
49
|
+
flattenMegaRows,
|
|
50
|
+
allRowsAsRawRows,
|
|
51
|
+
formatDurationHeader,
|
|
52
|
+
} from "./session-view/helpers";
|
|
53
|
+
import { EntryDayStrip } from "./session-view/entry-day-strip";
|
|
54
|
+
import { DebugList } from "./session-view/debug-list";
|
|
55
|
+
import { Tooltip, TooltipRow } from "./session-view/tooltip";
|
|
56
|
+
import { TokenChip, TurnTokenChip } from "./session-view/token-stats";
|
|
57
|
+
import {
|
|
58
|
+
InlineStat,
|
|
59
|
+
InlineStatDivider,
|
|
60
|
+
EntrypointBadge,
|
|
61
|
+
InlineTokenStat,
|
|
62
|
+
useAnchoredTooltip,
|
|
63
|
+
AnchoredTooltip,
|
|
64
|
+
} from "./session-view/header-stats";
|
|
65
|
+
import { Drawer } from "./session-view/drawer";
|
|
66
|
+
import {
|
|
67
|
+
WfMiniStat,
|
|
68
|
+
SectionLabel,
|
|
69
|
+
StatCell,
|
|
70
|
+
TokenLine,
|
|
71
|
+
} from "./session-view/workflows-shared";
|
|
72
|
+
import { SubagentDrawer } from "./session-view/subagent-drawer";
|
|
43
73
|
import { estimateCost, formatCost, formatGap, formatOffset, formatRelative, formatTokens, shortId } from "@/lib/format";
|
|
44
74
|
import { LiveBadge } from "@/components/live-badge";
|
|
45
75
|
import { AskButton, AskDrawer } from "@/components/ask";
|
|
@@ -90,16 +120,6 @@ const FILTER_MODES: { value: FilterMode; label: string }[] = [
|
|
|
90
120
|
{ value: "error", label: "Errors only" },
|
|
91
121
|
];
|
|
92
122
|
|
|
93
|
-
/** Render a `YYYY-MM-DD` day key as a short, locale-free label (e.g.
|
|
94
|
-
* "Jun 16"). Formatting straight from the key — not a Date — keeps SSR and
|
|
95
|
-
* client output identical regardless of timezone. */
|
|
96
|
-
const DAY_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
97
|
-
function formatDayKey(key: string): string {
|
|
98
|
-
const [, m, d] = key.split("-").map(Number);
|
|
99
|
-
if (!m || !d) return key;
|
|
100
|
-
return `${DAY_MONTHS[m - 1]} ${d}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
123
|
/* ------------------------------------------------------------------ */
|
|
104
124
|
/* Main component */
|
|
105
125
|
/* ------------------------------------------------------------------ */
|
|
@@ -182,9 +202,14 @@ export function SessionView({
|
|
|
182
202
|
const [scrollIntent, setScrollIntent] = useState(0);
|
|
183
203
|
/** Scroll-only target (no selection) — used by day navigation so jumping to
|
|
184
204
|
* a day scrolls the transcript there WITHOUT popping the event drawer the
|
|
185
|
-
* way a row/minimap selection does. `
|
|
186
|
-
*
|
|
187
|
-
|
|
205
|
+
* way a row/minimap selection does. `block` aligns the strip to the top
|
|
206
|
+
* ("start", normal day jump) or the bottom ("end", used by "go to end of the
|
|
207
|
+
* previous day" — scrolling the current day's strip to the viewport bottom
|
|
208
|
+
* reveals the previous day's tail above it). `n` forces the effect to re-run
|
|
209
|
+
* even when the same target is requested twice. */
|
|
210
|
+
const [dayScrollTarget, setDayScrollTarget] = useState<
|
|
211
|
+
{ index: number; key: string; block: "start" | "end"; n: number } | null
|
|
212
|
+
>(null);
|
|
188
213
|
const rowRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
|
189
214
|
// Per-day digest cards (EntryDayStrip) are interleaved into the transcript at
|
|
190
215
|
// each day's first row, so day-nav scrolls to the card itself when present.
|
|
@@ -211,6 +236,12 @@ export function SessionView({
|
|
|
211
236
|
* at mount + on resize via ResizeObserver — no magic constants. */
|
|
212
237
|
const headerRef = useRef<HTMLDivElement>(null);
|
|
213
238
|
const [headerH, setHeaderH] = useState(STICKY_HEADER_HEIGHT);
|
|
239
|
+
// Live mirror so the day-scroll alignment can read the current header height
|
|
240
|
+
// WITHOUT taking headerH as an effect dep — collapse flips headerH on every
|
|
241
|
+
// scroll direction change, and a dep there would re-fire the day scroll mid-
|
|
242
|
+
// free-scroll and yank the view back to the last nav target.
|
|
243
|
+
const headerHRef = useRef(headerH);
|
|
244
|
+
headerHRef.current = headerH;
|
|
214
245
|
useEffect(() => {
|
|
215
246
|
const el = headerRef.current;
|
|
216
247
|
if (!el) return;
|
|
@@ -231,6 +262,10 @@ export function SessionView({
|
|
|
231
262
|
// When the user explicitly clicks the toggle, suppress auto-collapse
|
|
232
263
|
// for a short period so the scroll listener doesn't immediately undo it.
|
|
233
264
|
const manualPinRef = useRef(0);
|
|
265
|
+
// Stamped on every explicit day-jump so TailMode's live-follow observer
|
|
266
|
+
// bails for a beat — otherwise a streaming live event yanks the view back
|
|
267
|
+
// to the bottom mid-navigation. See gotoDay / jumpAcrossDay.
|
|
268
|
+
const tailNavLockRef = useRef(0);
|
|
234
269
|
const toggleCollapsed = () => {
|
|
235
270
|
setCollapsed((v) => !v);
|
|
236
271
|
manualPinRef.current = Date.now();
|
|
@@ -312,11 +347,45 @@ export function SessionView({
|
|
|
312
347
|
// selectedIndex (so no drawer opens). Same scrollIntoView contract as above.
|
|
313
348
|
useEffect(() => {
|
|
314
349
|
if (!dayScrollTarget) return;
|
|
350
|
+
const main = headerRef.current?.closest("main") as HTMLElement | null;
|
|
315
351
|
// Prefer the day's digest card (it leads the day); fall back to the first
|
|
316
352
|
// row when that day has no entry.
|
|
317
353
|
const el =
|
|
318
354
|
dayStripRefs.current[dayScrollTarget.key] ?? rowRefs.current[dayScrollTarget.index];
|
|
319
|
-
if (el
|
|
355
|
+
if (!el || !main) return;
|
|
356
|
+
el.scrollIntoView({ block: dayScrollTarget.block, behavior: "auto" });
|
|
357
|
+
|
|
358
|
+
// scrollIntoView aims using the content-visibility ESTIMATED heights of the
|
|
359
|
+
// rows ABOVE the target, so on a long transcript (worst at mount, when none
|
|
360
|
+
// have been laid out yet) it undershoots by 100–200px — leaving the PREVIOUS
|
|
361
|
+
// day's tall trailing turn-block still straddling the header line. The
|
|
362
|
+
// playhead observer then reads THAT block's offset and followDayToScroll
|
|
363
|
+
// reverts the day-jump. For a top-aligned jump, re-measure on the next few
|
|
364
|
+
// frames (real heights now settle progressively) and nudge the target to sit
|
|
365
|
+
// exactly at the header line so the prev block clears it. block:"end" (go to
|
|
366
|
+
// end of prev day) intentionally leaves the prev-day tail on the line, so it
|
|
367
|
+
// needs no correction.
|
|
368
|
+
if (dayScrollTarget.block !== "start") return;
|
|
369
|
+
let frames = 0;
|
|
370
|
+
let settled = 0;
|
|
371
|
+
let raf = 0;
|
|
372
|
+
const align = () => {
|
|
373
|
+
const delta = Math.round(
|
|
374
|
+
el.getBoundingClientRect().top - main.getBoundingClientRect().top - headerHRef.current,
|
|
375
|
+
);
|
|
376
|
+
if (Math.abs(delta) > 1) {
|
|
377
|
+
main.scrollTop += delta;
|
|
378
|
+
settled = 0;
|
|
379
|
+
} else {
|
|
380
|
+
settled++;
|
|
381
|
+
}
|
|
382
|
+
// Keep correcting until it holds for two consecutive frames (rows above
|
|
383
|
+
// the target settle their real content-visibility heights progressively,
|
|
384
|
+
// and on a long transcript that can take many frames). Cap as a backstop.
|
|
385
|
+
if (settled < 2 && ++frames < 20) raf = requestAnimationFrame(align);
|
|
386
|
+
};
|
|
387
|
+
raf = requestAnimationFrame(align);
|
|
388
|
+
return () => cancelAnimationFrame(raf);
|
|
320
389
|
}, [dayScrollTarget]);
|
|
321
390
|
|
|
322
391
|
const { events, durationMs, totalUsage, model, eventCount, projectName } = session;
|
|
@@ -561,65 +630,88 @@ export function SessionView({
|
|
|
561
630
|
return m;
|
|
562
631
|
}, [entries]);
|
|
563
632
|
|
|
564
|
-
// Selected timeline day. Defaults to the
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
633
|
+
// Selected timeline day. Defaults to the LAST (most recent) active day so a
|
|
634
|
+
// long, multi-day transcript opens where the action is rather than at the
|
|
635
|
+
// top of an old day; the initial-jump effect below scrolls there instantly,
|
|
636
|
+
// and the day then follows the scroll position as the user reads in either
|
|
637
|
+
// direction (see followDayToScroll). The lazy initializer runs identically on
|
|
638
|
+
// server + client (sessionDays is deterministic), so there's no hydration
|
|
639
|
+
// mismatch; the effect re-pins it when the component is reused across
|
|
640
|
+
// /sessions/[id] navigations. "all" shows the full session.
|
|
641
|
+
const didInitialDayJump = useRef(false);
|
|
570
642
|
const [selectedDayKey, setSelectedDayKey] = useState<string>(
|
|
571
|
-
() => (sessionDays.length ? sessionDays[
|
|
643
|
+
() => (sessionDays.length ? sessionDays[sessionDays.length - 1]!.key : "all"),
|
|
572
644
|
);
|
|
573
645
|
useEffect(() => {
|
|
574
|
-
setSelectedDayKey(sessionDays.length ? sessionDays[
|
|
646
|
+
setSelectedDayKey(sessionDays.length ? sessionDays[sessionDays.length - 1]!.key : "all");
|
|
647
|
+
didInitialDayJump.current = false;
|
|
575
648
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
576
649
|
}, [session.sessionId]);
|
|
577
650
|
|
|
578
651
|
const dayIndex = sessionDays.findIndex((d) => d.key === selectedDayKey);
|
|
579
|
-
// Latest
|
|
580
|
-
//
|
|
581
|
-
//
|
|
652
|
+
// Latest sessionDays, read by the initial-jump effect so it doesn't re-run on
|
|
653
|
+
// every render. On LIVE sessions SSE router.refresh() rebuilds sessionDays each
|
|
654
|
+
// tick; reading it via ref keeps the once-per-session jump from re-firing.
|
|
655
|
+
const sessionDaysRef = useRef(sessionDays);
|
|
656
|
+
sessionDaysRef.current = sessionDays;
|
|
657
|
+
// Read by followDayToScroll (the scroll→day follower) without stale closure.
|
|
582
658
|
const selectedDayKeyRef = useRef(selectedDayKey);
|
|
583
659
|
selectedDayKeyRef.current = selectedDayKey;
|
|
584
660
|
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
661
|
+
// Initial jump: on first paint of a multi-day session, snap straight to the
|
|
662
|
+
// most recent day (the default selectedDayKey is already the last day, so the
|
|
663
|
+
// minimap matches). Routed through gotoDay rather than scrolling to the day's
|
|
664
|
+
// strip directly: in "turns" mode the most recent day can be absorbed into a
|
|
665
|
+
// turn anchored on an earlier day and so have NO inline strip — gotoDay finds
|
|
666
|
+
// the day's first event, expands its turn, and falls back to the row ref, so
|
|
667
|
+
// it lands reliably either way. Single-day sessions stay at the top. Runs once
|
|
668
|
+
// per session; the re-pin effect above clears didInitialDayJump on nav.
|
|
669
|
+
//
|
|
670
|
+
// SKIPPED for live sessions: their destination is the live tail, which
|
|
671
|
+
// TailMode scrolls to on mount. Jumping to the most-recent-day's TOP would
|
|
672
|
+
// scroll the view UP, and that programmatic scroll trips TailMode's scroll-up
|
|
673
|
+
// detector (check()) into turning live-follow off — and it would race
|
|
674
|
+
// TailMode's own mount scroll. followDayToScroll still keeps the minimap on
|
|
675
|
+
// the last day, since the tail sits there.
|
|
590
676
|
useEffect(() => {
|
|
591
|
-
if (tab !== "transcript" ||
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
let currentKey: string | null = null;
|
|
601
|
-
for (const d of sessionDays) {
|
|
602
|
-
const stripEl = dayStripRefs.current[d.key];
|
|
603
|
-
if (!stripEl) continue;
|
|
604
|
-
if (stripEl.getBoundingClientRect().top <= line) currentKey = d.key;
|
|
605
|
-
else break;
|
|
606
|
-
}
|
|
607
|
-
// Above every card (top of transcript) → we're still reading the first day.
|
|
608
|
-
if (!currentKey) currentKey = sessionDays.find((d) => dayStripRefs.current[d.key])?.key ?? null;
|
|
609
|
-
if (currentKey) setSelectedDayKey((prev) => (prev !== currentKey ? currentKey! : prev));
|
|
610
|
-
};
|
|
611
|
-
const onScroll = () => {
|
|
612
|
-
if (raf) return;
|
|
613
|
-
raf = requestAnimationFrame(update);
|
|
614
|
-
};
|
|
615
|
-
mainEl.addEventListener("scroll", onScroll, { passive: true });
|
|
616
|
-
update(); // sync the minimap to the initial reading position (day 1 at top)
|
|
617
|
-
return () => {
|
|
618
|
-
mainEl.removeEventListener("scroll", onScroll);
|
|
619
|
-
if (raf) cancelAnimationFrame(raf);
|
|
620
|
-
};
|
|
677
|
+
if (tab !== "transcript" || didInitialDayJump.current) return;
|
|
678
|
+
const days = sessionDaysRef.current;
|
|
679
|
+
if (days.length <= 1 || isSessionLive) {
|
|
680
|
+
didInitialDayJump.current = true;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
didInitialDayJump.current = true;
|
|
684
|
+
gotoDay(days[days.length - 1]!.key);
|
|
685
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
621
686
|
}, [tab, sessionDays]);
|
|
622
687
|
|
|
688
|
+
// The displayed day is a single function of scroll position. The Minimap
|
|
689
|
+
// reports the offset (ms) of the topmost on-screen row — the SAME signal it
|
|
690
|
+
// draws the playhead from — and we select that row's day. Display only: this
|
|
691
|
+
// never scrolls the transcript. Navigation (gotoDay / the inline day-boundary
|
|
692
|
+
// buttons) moves the scroll, and the day then follows this one signal, so
|
|
693
|
+
// there's no second writer that could revert a day-jump. (The old scroll-spy
|
|
694
|
+
// used a separate pixel heuristic that disagreed with where nav scrolled to,
|
|
695
|
+
// which is what made "next day" snap back.) The tailNavLock guard lets an
|
|
696
|
+
// explicit jump's scroll settle first, and the key-unchanged no-op keeps a
|
|
697
|
+
// LIVE session's SSE refreshes from churning the selection.
|
|
698
|
+
function followDayToScroll(ms: number | null) {
|
|
699
|
+
if (ms === null) return;
|
|
700
|
+
if (Date.now() - tailNavLockRef.current < 1500) return;
|
|
701
|
+
const days = sessionDaysRef.current;
|
|
702
|
+
if (days.length <= 1) return;
|
|
703
|
+
const cur = selectedDayKeyRef.current;
|
|
704
|
+
if (cur === "all") return; // respect an explicit "show all days" choice
|
|
705
|
+
// Days are sorted by startMs; the last one starting at/below ms is the
|
|
706
|
+
// containing day (and tolerates the overnight gap between days).
|
|
707
|
+
let hit = days[0]!;
|
|
708
|
+
for (const d of days) {
|
|
709
|
+
if (ms >= d.startMs) hit = d;
|
|
710
|
+
else break;
|
|
711
|
+
}
|
|
712
|
+
if (hit.key !== cur) setSelectedDayKey(hit.key);
|
|
713
|
+
}
|
|
714
|
+
|
|
623
715
|
/** Offset window for the selected day, or null when there's a single day
|
|
624
716
|
* or "all" is chosen — null tells the minimap to render the full session
|
|
625
717
|
* exactly as before (no regression for single-day sessions). */
|
|
@@ -709,12 +801,13 @@ export function SessionView({
|
|
|
709
801
|
setScrollIntent((n) => n + 1);
|
|
710
802
|
}
|
|
711
803
|
|
|
712
|
-
/** Switch the minimap to a given day (or "all").
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
804
|
+
/** Switch the minimap to a given day (or "all"). Also scrolls the transcript
|
|
805
|
+
* to that day's first row so "jump to a day" navigates the body too. Called
|
|
806
|
+
* on user day-picks and once on mount by the initial-jump effect (for non-live
|
|
807
|
+
* multi-day sessions, to open on the most recent day). */
|
|
716
808
|
function gotoDay(key: string) {
|
|
717
809
|
setSelectedDayKey(key);
|
|
810
|
+
tailNavLockRef.current = Date.now();
|
|
718
811
|
// Paging days scrolls the transcript; that programmatic scroll (often
|
|
719
812
|
// upward to an earlier day's first row) would otherwise read as a
|
|
720
813
|
// user scroll-up and pop the sticky header open. Force it collapsed and
|
|
@@ -735,7 +828,38 @@ export function SessionView({
|
|
|
735
828
|
setExpandedTurns((prev) => new Set(prev).add(turnIdx));
|
|
736
829
|
}
|
|
737
830
|
}
|
|
738
|
-
setDayScrollTarget((prev) => ({ index: ev.index, key, n: (prev?.n ?? 0) + 1 }));
|
|
831
|
+
setDayScrollTarget((prev) => ({ index: ev.index, key, block: "start", n: (prev?.n ?? 0) + 1 }));
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Cross-day hop used by the inline day-boundary buttons. Selecting a day can
|
|
835
|
+
* differ from where we scroll: "go to end of previous day" selects the
|
|
836
|
+
* previous day but scrolls the CURRENT day's strip to the viewport bottom
|
|
837
|
+
* (block:"end"), revealing the previous day's tail above it. "go to start of
|
|
838
|
+
* next day" selects + scrolls to that day's strip (block:"start"). */
|
|
839
|
+
function jumpAcrossDay(
|
|
840
|
+
selectKey: string,
|
|
841
|
+
scrollKey: string,
|
|
842
|
+
block: "start" | "end",
|
|
843
|
+
fallbackIndex: number,
|
|
844
|
+
) {
|
|
845
|
+
setSelectedDayKey(selectKey);
|
|
846
|
+
setCollapsed(true);
|
|
847
|
+
manualPinRef.current = Date.now();
|
|
848
|
+
tailNavLockRef.current = Date.now();
|
|
849
|
+
// The fallback row may live inside a collapsed turn — expand it so the
|
|
850
|
+
// element exists if the strip ref is unavailable.
|
|
851
|
+
if (filter === "turns") {
|
|
852
|
+
const turnIdx = turnByRowIndex.get(fallbackIndex);
|
|
853
|
+
if (turnIdx !== undefined && !expandedTurns.has(turnIdx)) {
|
|
854
|
+
setExpandedTurns((prev) => new Set(prev).add(turnIdx));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
setDayScrollTarget((prev) => ({
|
|
858
|
+
index: fallbackIndex,
|
|
859
|
+
key: scrollKey,
|
|
860
|
+
block,
|
|
861
|
+
n: (prev?.n ?? 0) + 1,
|
|
862
|
+
}));
|
|
739
863
|
}
|
|
740
864
|
|
|
741
865
|
/** Scroll the transcript to the row whose assistant message issued a given
|
|
@@ -1186,6 +1310,7 @@ export function SessionView({
|
|
|
1186
1310
|
// for the same right-side real estate.
|
|
1187
1311
|
if (id) setSelectedIndex(null);
|
|
1188
1312
|
}}
|
|
1313
|
+
onPlayheadChange={followDayToScroll}
|
|
1189
1314
|
/>
|
|
1190
1315
|
)}
|
|
1191
1316
|
</div>
|
|
@@ -1230,6 +1355,7 @@ export function SessionView({
|
|
|
1230
1355
|
)
|
|
1231
1356
|
}
|
|
1232
1357
|
onJumpToParent={jumpToToolUse}
|
|
1358
|
+
onClearFocus={() => setSelectedWorkflowId(null)}
|
|
1233
1359
|
/>
|
|
1234
1360
|
</div>
|
|
1235
1361
|
) : tab === "debug" ? (
|
|
@@ -1281,6 +1407,7 @@ export function SessionView({
|
|
|
1281
1407
|
sessionDays={sessionDays}
|
|
1282
1408
|
entryByDay={entryByDay}
|
|
1283
1409
|
dayStripRefs={dayStripRefs}
|
|
1410
|
+
onJumpAcrossDay={jumpAcrossDay}
|
|
1284
1411
|
/>
|
|
1285
1412
|
</>
|
|
1286
1413
|
)}
|
|
@@ -1411,7 +1538,7 @@ export function SessionView({
|
|
|
1411
1538
|
)}
|
|
1412
1539
|
|
|
1413
1540
|
{/* Tail mode FAB — auto-scroll to follow live events */}
|
|
1414
|
-
<TailMode isLive={isSessionLive} />
|
|
1541
|
+
<TailMode isLive={isSessionLive} navLockRef={tailNavLockRef} />
|
|
1415
1542
|
</div>
|
|
1416
1543
|
);
|
|
1417
1544
|
}
|
|
@@ -1420,353 +1547,19 @@ export function SessionView({
|
|
|
1420
1547
|
/* Row helpers */
|
|
1421
1548
|
/* ------------------------------------------------------------------ */
|
|
1422
1549
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
/* ------------------------------------------------------------------ */
|
|
1426
|
-
/* DisplayRow: one line in the transcript list */
|
|
1427
|
-
/* */
|
|
1428
|
-
/* Flat list item type — either a presentation row (user, agent, */
|
|
1429
|
-
/* tool-group, error, ...), a collapsed turn summary, or an */
|
|
1430
|
-
/* expanded-turn header with its inner rows following. */
|
|
1431
|
-
/* ------------------------------------------------------------------ */
|
|
1432
|
-
|
|
1433
|
-
type DisplayRow =
|
|
1434
|
-
| { kind: "presentation"; row: PresentationRow; indented?: boolean }
|
|
1435
|
-
| { kind: "turn-collapsed"; turn: TurnMegaRow }
|
|
1436
|
-
| { kind: "turn-expanded-header"; turn: TurnMegaRow }
|
|
1437
|
-
| { kind: "turn-expanded-footer"; turn: TurnMegaRow };
|
|
1438
|
-
|
|
1439
|
-
function flattenMegaRows(megaRows: MegaRow[], expanded: Set<number>): DisplayRow[] {
|
|
1440
|
-
const out: DisplayRow[] = [];
|
|
1441
|
-
for (const m of megaRows) {
|
|
1442
|
-
if (m.kind === "turn") {
|
|
1443
|
-
if (expanded.has(m.firstPrimaryIndex)) {
|
|
1444
|
-
out.push({ kind: "turn-expanded-header", turn: m });
|
|
1445
|
-
for (const r of m.rows) {
|
|
1446
|
-
out.push({ kind: "presentation", row: r, indented: true });
|
|
1447
|
-
}
|
|
1448
|
-
out.push({ kind: "turn-expanded-footer", turn: m });
|
|
1449
|
-
} else {
|
|
1450
|
-
out.push({ kind: "turn-collapsed", turn: m });
|
|
1451
|
-
}
|
|
1452
|
-
} else {
|
|
1453
|
-
out.push({ kind: "presentation", row: m });
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
return out;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
/** Fallback for "All events" filter mode — wraps each raw event in a
|
|
1460
|
-
* synthetic PresentationRow so the transcript component can render
|
|
1461
|
-
* everything (attachments, meta, thinking, tool_result) without
|
|
1462
|
-
* going through the meaningful-transformation logic. */
|
|
1463
|
-
function allRowsAsRawRows(events: SessionEvent[]): PresentationRow[] {
|
|
1464
|
-
const out: PresentationRow[] = [];
|
|
1465
|
-
for (const e of events) {
|
|
1466
|
-
// Coerce every raw event to the closest presentation kind.
|
|
1467
|
-
let kind: PresentationRowKind;
|
|
1468
|
-
switch (e.role) {
|
|
1469
|
-
case "user":
|
|
1470
|
-
kind = "user";
|
|
1471
|
-
break;
|
|
1472
|
-
case "agent":
|
|
1473
|
-
case "agent-thinking":
|
|
1474
|
-
kind = "agent";
|
|
1475
|
-
break;
|
|
1476
|
-
case "tool-call":
|
|
1477
|
-
case "tool-result":
|
|
1478
|
-
kind = "tool-group";
|
|
1479
|
-
break;
|
|
1480
|
-
case "system":
|
|
1481
|
-
case "meta":
|
|
1482
|
-
kind = "model";
|
|
1483
|
-
break;
|
|
1484
|
-
}
|
|
1485
|
-
if (kind === "tool-group") {
|
|
1486
|
-
out.push({
|
|
1487
|
-
kind: "tool-group",
|
|
1488
|
-
toolNames: [{ name: e.toolName ?? e.rawType, count: 1 }],
|
|
1489
|
-
count: 1,
|
|
1490
|
-
events: [e],
|
|
1491
|
-
tOffsetMs: e.tOffsetMs,
|
|
1492
|
-
gapMs: e.gapMs,
|
|
1493
|
-
});
|
|
1494
|
-
} else if (kind === "agent") {
|
|
1495
|
-
out.push({
|
|
1496
|
-
kind: "agent",
|
|
1497
|
-
event: e,
|
|
1498
|
-
groupedEvents: [e],
|
|
1499
|
-
tOffsetMs: e.tOffsetMs,
|
|
1500
|
-
gapMs: e.gapMs,
|
|
1501
|
-
});
|
|
1502
|
-
} else if (kind === "user") {
|
|
1503
|
-
out.push({
|
|
1504
|
-
kind: "user",
|
|
1505
|
-
event: e,
|
|
1506
|
-
tOffsetMs: e.tOffsetMs,
|
|
1507
|
-
gapMs: e.gapMs,
|
|
1508
|
-
});
|
|
1509
|
-
} else {
|
|
1510
|
-
out.push({
|
|
1511
|
-
kind: "model",
|
|
1512
|
-
event: e,
|
|
1513
|
-
tOffsetMs: e.tOffsetMs,
|
|
1514
|
-
gapMs: e.gapMs,
|
|
1515
|
-
});
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
return out;
|
|
1519
|
-
}
|
|
1550
|
+
// rowPrimaryIndex, DisplayRow, flattenMegaRows, allRowsAsRawRows moved to ./session-view/{types,helpers}.ts
|
|
1520
1551
|
|
|
1521
1552
|
/* ------------------------------------------------------------------ */
|
|
1522
1553
|
/* Header stats + token stat w/ tooltip */
|
|
1523
1554
|
/* ------------------------------------------------------------------ */
|
|
1524
1555
|
|
|
1525
|
-
|
|
1526
|
-
* Used in the single-line session header, modeled on Claude's Sessions page. */
|
|
1527
|
-
function InlineStat({
|
|
1528
|
-
icon,
|
|
1529
|
-
value,
|
|
1530
|
-
mono,
|
|
1531
|
-
truncate,
|
|
1532
|
-
}: {
|
|
1533
|
-
icon?: React.ReactNode;
|
|
1534
|
-
value: string;
|
|
1535
|
-
mono?: boolean;
|
|
1536
|
-
truncate?: boolean;
|
|
1537
|
-
}) {
|
|
1538
|
-
return (
|
|
1539
|
-
<span
|
|
1540
|
-
style={{
|
|
1541
|
-
display: "inline-flex",
|
|
1542
|
-
alignItems: "center",
|
|
1543
|
-
gap: 5,
|
|
1544
|
-
fontSize: 12,
|
|
1545
|
-
color: "var(--af-text-secondary)",
|
|
1546
|
-
maxWidth: truncate ? 260 : undefined,
|
|
1547
|
-
overflow: "hidden",
|
|
1548
|
-
textOverflow: "ellipsis",
|
|
1549
|
-
whiteSpace: "nowrap",
|
|
1550
|
-
}}
|
|
1551
|
-
>
|
|
1552
|
-
{icon && (
|
|
1553
|
-
<span
|
|
1554
|
-
style={{
|
|
1555
|
-
display: "inline-flex",
|
|
1556
|
-
color: "var(--af-text-tertiary)",
|
|
1557
|
-
}}
|
|
1558
|
-
>
|
|
1559
|
-
{icon}
|
|
1560
|
-
</span>
|
|
1561
|
-
)}
|
|
1562
|
-
<span
|
|
1563
|
-
style={{
|
|
1564
|
-
fontFamily: mono ? "var(--font-mono)" : "inherit",
|
|
1565
|
-
overflow: "hidden",
|
|
1566
|
-
textOverflow: "ellipsis",
|
|
1567
|
-
whiteSpace: "nowrap",
|
|
1568
|
-
}}
|
|
1569
|
-
>
|
|
1570
|
-
{value}
|
|
1571
|
-
</span>
|
|
1572
|
-
</span>
|
|
1573
|
-
);
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
function InlineStatDivider() {
|
|
1577
|
-
return (
|
|
1578
|
-
<span
|
|
1579
|
-
style={{
|
|
1580
|
-
color: "var(--af-text-tertiary)",
|
|
1581
|
-
fontSize: 11,
|
|
1582
|
-
opacity: 0.6,
|
|
1583
|
-
}}
|
|
1584
|
-
>
|
|
1585
|
-
·
|
|
1586
|
-
</span>
|
|
1587
|
-
);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
function EntrypointBadge({ entrypoint }: { entrypoint: string }) {
|
|
1591
|
-
// cli + claude-desktop are human-driven; sdk-* are programmatic.
|
|
1592
|
-
const isSdk = entrypoint.startsWith("sdk-");
|
|
1593
|
-
const tone = isSdk
|
|
1594
|
-
? { bg: "rgba(245, 158, 11, 0.16)", fg: "#b45309" }
|
|
1595
|
-
: { bg: "rgba(16, 185, 129, 0.16)", fg: "#047857" };
|
|
1596
|
-
return (
|
|
1597
|
-
<span
|
|
1598
|
-
title={`entrypoint: ${entrypoint}`}
|
|
1599
|
-
style={{
|
|
1600
|
-
fontSize: 10.5,
|
|
1601
|
-
padding: "2px 7px",
|
|
1602
|
-
borderRadius: 4,
|
|
1603
|
-
background: tone.bg,
|
|
1604
|
-
color: tone.fg,
|
|
1605
|
-
fontWeight: 600,
|
|
1606
|
-
fontFamily: "var(--font-mono)",
|
|
1607
|
-
letterSpacing: 0.2,
|
|
1608
|
-
}}
|
|
1609
|
-
>
|
|
1610
|
-
{entrypoint}
|
|
1611
|
-
</span>
|
|
1612
|
-
);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
|
|
1616
|
-
const { ref, anchor, open, close } = useAnchoredTooltip();
|
|
1617
|
-
const u = usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1618
|
-
const totalIn = u.input + u.cacheRead + u.cacheWrite;
|
|
1619
|
-
const pctRead = totalIn > 0 ? Math.round((u.cacheRead / totalIn) * 100) : 0;
|
|
1620
|
-
const cached = u.cacheRead + u.cacheWrite;
|
|
1621
|
-
|
|
1622
|
-
return (
|
|
1623
|
-
<span
|
|
1624
|
-
ref={ref}
|
|
1625
|
-
style={{
|
|
1626
|
-
display: "inline-flex",
|
|
1627
|
-
alignItems: "center",
|
|
1628
|
-
gap: 6,
|
|
1629
|
-
fontSize: 12,
|
|
1630
|
-
color: "var(--af-text-secondary)",
|
|
1631
|
-
cursor: "default",
|
|
1632
|
-
fontFamily: "var(--font-mono)",
|
|
1633
|
-
}}
|
|
1634
|
-
onMouseEnter={open}
|
|
1635
|
-
onMouseLeave={close}
|
|
1636
|
-
>
|
|
1637
|
-
<span>
|
|
1638
|
-
{formatTokens(u.input)}
|
|
1639
|
-
<span style={{ color: "var(--af-text-tertiary)", margin: "0 3px" }}>in</span>
|
|
1640
|
-
{formatTokens(u.output)}
|
|
1641
|
-
<span style={{ color: "var(--af-text-tertiary)", marginLeft: 3 }}>out</span>
|
|
1642
|
-
</span>
|
|
1643
|
-
{cached > 0 && (
|
|
1644
|
-
<span
|
|
1645
|
-
style={{
|
|
1646
|
-
fontSize: 10.5,
|
|
1647
|
-
color: "var(--af-text-tertiary)",
|
|
1648
|
-
paddingLeft: 6,
|
|
1649
|
-
borderLeft: "1px solid var(--af-border-subtle)",
|
|
1650
|
-
}}
|
|
1651
|
-
>
|
|
1652
|
-
+{formatTokens(cached)} cached
|
|
1653
|
-
</span>
|
|
1654
|
-
)}
|
|
1655
|
-
{anchor && (
|
|
1656
|
-
<AnchoredTooltip anchor={anchor} width={280}>
|
|
1657
|
-
<TooltipRow label="Input (fresh)" value={u.input.toLocaleString()} />
|
|
1658
|
-
<TooltipRow label="Output" value={u.output.toLocaleString()} />
|
|
1659
|
-
<TooltipRow label="Cache read" value={`${u.cacheRead.toLocaleString()} (${pctRead}%)`} />
|
|
1660
|
-
<TooltipRow label="Cache write" value={u.cacheWrite.toLocaleString()} />
|
|
1661
|
-
<div
|
|
1662
|
-
style={{
|
|
1663
|
-
marginTop: 6,
|
|
1664
|
-
paddingTop: 6,
|
|
1665
|
-
borderTop: "1px solid rgba(241,245,249,0.12)",
|
|
1666
|
-
opacity: 0.65,
|
|
1667
|
-
fontSize: 10,
|
|
1668
|
-
whiteSpace: "normal",
|
|
1669
|
-
lineHeight: 1.4,
|
|
1670
|
-
}}
|
|
1671
|
-
>
|
|
1672
|
-
Cache reads are cumulative across all API requests and billed at ~10% of regular input.
|
|
1673
|
-
</div>
|
|
1674
|
-
</AnchoredTooltip>
|
|
1675
|
-
)}
|
|
1676
|
-
</span>
|
|
1677
|
-
);
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
// Used for tooltips that live inside an overflow:hidden ancestor (like the
|
|
1681
|
-
// session-header row). getBoundingClientRect + position:fixed lets the
|
|
1682
|
-
// tooltip escape the clipping context instead of being cropped.
|
|
1683
|
-
function useAnchoredTooltip() {
|
|
1684
|
-
const ref = useRef<HTMLSpanElement>(null);
|
|
1685
|
-
const [anchor, setAnchor] = useState<{ top: number; left: number } | null>(null);
|
|
1686
|
-
const open = () => {
|
|
1687
|
-
if (!ref.current) return;
|
|
1688
|
-
const r = ref.current.getBoundingClientRect();
|
|
1689
|
-
setAnchor({ top: r.bottom + 6, left: r.left });
|
|
1690
|
-
};
|
|
1691
|
-
const close = () => setAnchor(null);
|
|
1692
|
-
return { ref, anchor, open, close };
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
function AnchoredTooltip({
|
|
1696
|
-
anchor,
|
|
1697
|
-
width,
|
|
1698
|
-
children,
|
|
1699
|
-
}: {
|
|
1700
|
-
anchor: { top: number; left: number };
|
|
1701
|
-
width: number;
|
|
1702
|
-
children: React.ReactNode;
|
|
1703
|
-
}) {
|
|
1704
|
-
return (
|
|
1705
|
-
<div
|
|
1706
|
-
style={{
|
|
1707
|
-
position: "fixed",
|
|
1708
|
-
top: anchor.top,
|
|
1709
|
-
left: anchor.left,
|
|
1710
|
-
zIndex: 1000,
|
|
1711
|
-
background: "#1A1A1A",
|
|
1712
|
-
color: "#F5F1EC",
|
|
1713
|
-
padding: "8px 12px",
|
|
1714
|
-
borderRadius: 6,
|
|
1715
|
-
fontSize: 11,
|
|
1716
|
-
fontFamily: "var(--font-mono)",
|
|
1717
|
-
lineHeight: 1.5,
|
|
1718
|
-
pointerEvents: "none",
|
|
1719
|
-
boxShadow: "0 4px 16px rgba(15,23,42,0.24)",
|
|
1720
|
-
width,
|
|
1721
|
-
}}
|
|
1722
|
-
>
|
|
1723
|
-
{children}
|
|
1724
|
-
</div>
|
|
1725
|
-
);
|
|
1726
|
-
}
|
|
1556
|
+
// InlineStat, InlineStatDivider, EntrypointBadge, InlineTokenStat, useAnchoredTooltip, AnchoredTooltip moved to ./session-view/header-stats.tsx
|
|
1727
1557
|
|
|
1728
1558
|
/* ------------------------------------------------------------------ */
|
|
1729
1559
|
/* Generic Tooltip */
|
|
1730
1560
|
/* ------------------------------------------------------------------ */
|
|
1731
1561
|
|
|
1732
|
-
|
|
1733
|
-
return (
|
|
1734
|
-
<div
|
|
1735
|
-
style={{
|
|
1736
|
-
position: "absolute",
|
|
1737
|
-
zIndex: 100,
|
|
1738
|
-
background: "#1A1A1A",
|
|
1739
|
-
color: "#F5F1EC",
|
|
1740
|
-
borderRadius: 8,
|
|
1741
|
-
padding: "10px 12px",
|
|
1742
|
-
fontSize: 11,
|
|
1743
|
-
fontFamily: "var(--font-mono)",
|
|
1744
|
-
lineHeight: 1.5,
|
|
1745
|
-
boxShadow: "0 4px 20px rgba(0,0,0,0.18)",
|
|
1746
|
-
pointerEvents: "none",
|
|
1747
|
-
whiteSpace: "nowrap",
|
|
1748
|
-
...style,
|
|
1749
|
-
}}
|
|
1750
|
-
>
|
|
1751
|
-
{children}
|
|
1752
|
-
</div>
|
|
1753
|
-
);
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
function TooltipRow({ label, value }: { label: string; value: string }) {
|
|
1757
|
-
return (
|
|
1758
|
-
<div
|
|
1759
|
-
style={{
|
|
1760
|
-
display: "flex",
|
|
1761
|
-
justifyContent: "space-between",
|
|
1762
|
-
gap: 12,
|
|
1763
|
-
}}
|
|
1764
|
-
>
|
|
1765
|
-
<span style={{ opacity: 0.7 }}>{label}</span>
|
|
1766
|
-
<span>{value}</span>
|
|
1767
|
-
</div>
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1562
|
+
// Tooltip, TooltipRow moved to ./session-view/tooltip.tsx
|
|
1770
1563
|
|
|
1771
1564
|
/* ------------------------------------------------------------------ */
|
|
1772
1565
|
/* Mini-map */
|
|
@@ -1787,22 +1580,7 @@ function TooltipRow({ label, value }: { label: string; value: string }) {
|
|
|
1787
1580
|
/* - Rich hover card (role pill + preview) instead of bare SVG title. */
|
|
1788
1581
|
/* ------------------------------------------------------------------ */
|
|
1789
1582
|
|
|
1790
|
-
|
|
1791
|
-
| {
|
|
1792
|
-
kind: "row";
|
|
1793
|
-
row: PresentationRow;
|
|
1794
|
-
primaryIndex: number;
|
|
1795
|
-
start: number;
|
|
1796
|
-
end: number;
|
|
1797
|
-
}
|
|
1798
|
-
| {
|
|
1799
|
-
kind: "turn";
|
|
1800
|
-
turn: TurnMegaRow;
|
|
1801
|
-
primaryIndex: number;
|
|
1802
|
-
start: number;
|
|
1803
|
-
end: number;
|
|
1804
|
-
}
|
|
1805
|
-
| { kind: "idle"; start: number; end: number; durationMs: number };
|
|
1583
|
+
// MinimapSeg moved to ./session-view/types.ts
|
|
1806
1584
|
|
|
1807
1585
|
/** All rows render at full bar height — distinction comes from color/pattern
|
|
1808
1586
|
* alone, matching Claude Managed Agents' unified-height timeline. */
|
|
@@ -1833,6 +1611,7 @@ function Minimap({
|
|
|
1833
1611
|
coldResumeMarkers,
|
|
1834
1612
|
rawIdleBands,
|
|
1835
1613
|
model,
|
|
1614
|
+
onPlayheadChange,
|
|
1836
1615
|
}: {
|
|
1837
1616
|
displayRows: DisplayRow[];
|
|
1838
1617
|
durationMs: number;
|
|
@@ -1859,6 +1638,10 @@ function Minimap({
|
|
|
1859
1638
|
* surface their dead-air on the minimap. */
|
|
1860
1639
|
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
1861
1640
|
model?: string;
|
|
1641
|
+
/** Fires with the offset (ms) of the topmost on-screen transcript row — the
|
|
1642
|
+
* same scroll signal that draws the playhead. SessionView uses it to make
|
|
1643
|
+
* the selected day follow scroll. */
|
|
1644
|
+
onPlayheadChange?: (ms: number | null) => void;
|
|
1862
1645
|
}) {
|
|
1863
1646
|
const WIDTH = 1400;
|
|
1864
1647
|
/** Main timeline height. Sub-agent lanes stack below this. */
|
|
@@ -1897,6 +1680,10 @@ function Minimap({
|
|
|
1897
1680
|
};
|
|
1898
1681
|
} | null>(null);
|
|
1899
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;
|
|
1900
1687
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
1901
1688
|
|
|
1902
1689
|
/* Playhead — track which transcript row is at the top of the viewport.
|
|
@@ -1905,60 +1692,54 @@ function Minimap({
|
|
|
1905
1692
|
* on every scroll event. For a session with ~2000 rows that's thousands
|
|
1906
1693
|
* of DOM queries per second during scroll — enough to pin a CPU core
|
|
1907
1694
|
* and make Chrome ask to kill the tab. IntersectionObserver is event-
|
|
1908
|
-
* driven: the browser only notifies us when rows
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
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.
|
|
1911
1698
|
*/
|
|
1912
1699
|
useEffect(() => {
|
|
1913
1700
|
const main = containerRef.current?.closest("main") as HTMLElement | null;
|
|
1914
1701
|
if (!main) return;
|
|
1915
1702
|
|
|
1916
|
-
//
|
|
1917
|
-
//
|
|
1918
|
-
//
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
// the playhead. Using a Set keeps updates O(1).
|
|
1929
|
-
const inBand = new Set<HTMLElement>();
|
|
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>();
|
|
1930
1715
|
|
|
1931
1716
|
const publishTopmost = () => {
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
if (
|
|
1944
|
-
|
|
1945
|
-
|
|
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;
|
|
1946
1731
|
}
|
|
1947
1732
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
const tOff = Number(topmost.getAttribute("data-sl-toffset"));
|
|
1953
|
-
setPlayheadMs(Number.isNaN(tOff) ? null : tOff);
|
|
1733
|
+
setPlayheadMs(value);
|
|
1734
|
+
onPlayheadChangeRef.current?.(value);
|
|
1954
1735
|
};
|
|
1955
1736
|
|
|
1956
1737
|
const observer = new IntersectionObserver(
|
|
1957
1738
|
(entries) => {
|
|
1958
1739
|
for (const e of entries) {
|
|
1959
1740
|
const el = e.target as HTMLElement;
|
|
1960
|
-
if (e.isIntersecting)
|
|
1961
|
-
else
|
|
1741
|
+
if (e.isIntersecting) visibleRows.add(el);
|
|
1742
|
+
else visibleRows.delete(el);
|
|
1962
1743
|
}
|
|
1963
1744
|
publishTopmost();
|
|
1964
1745
|
},
|
|
@@ -2607,12 +2388,17 @@ function Minimap({
|
|
|
2607
2388
|
strokeWidth="1.5"
|
|
2608
2389
|
strokeDasharray="4 3"
|
|
2609
2390
|
opacity="0.75"
|
|
2391
|
+
pointerEvents="none"
|
|
2610
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. */}
|
|
2611
2397
|
<rect
|
|
2612
|
-
x={x -
|
|
2613
|
-
y={
|
|
2614
|
-
width={
|
|
2615
|
-
height={
|
|
2398
|
+
x={x - 6}
|
|
2399
|
+
y={MAIN_H / 2 - 8}
|
|
2400
|
+
width={12}
|
|
2401
|
+
height={16}
|
|
2616
2402
|
fill="transparent"
|
|
2617
2403
|
/>
|
|
2618
2404
|
<polygon
|
|
@@ -2620,6 +2406,7 @@ function Minimap({
|
|
|
2620
2406
|
fill="#D97706"
|
|
2621
2407
|
stroke="var(--af-surface)"
|
|
2622
2408
|
strokeWidth="1.5"
|
|
2409
|
+
pointerEvents="none"
|
|
2623
2410
|
/>
|
|
2624
2411
|
</g>
|
|
2625
2412
|
);
|
|
@@ -2717,33 +2504,7 @@ function Minimap({
|
|
|
2717
2504
|
);
|
|
2718
2505
|
}
|
|
2719
2506
|
|
|
2720
|
-
|
|
2721
|
-
* variant of their color so true parallelism is visually distinct from
|
|
2722
|
-
* blocking subagent calls (where the parent waits and there's no
|
|
2723
|
-
* meaningful overlap with main session work). */
|
|
2724
|
-
function subagentColor(agentType: string, background?: boolean): string {
|
|
2725
|
-
const palette: Record<string, [string, string]> = {
|
|
2726
|
-
"general-purpose": ["#5C84C3", "#7BA3DC"],
|
|
2727
|
-
Explore: ["#A855F7", "#C57BFF"],
|
|
2728
|
-
Plan: ["#F59E0B", "#FFBD3D"],
|
|
2729
|
-
"code-reviewer": ["#34D399", "#5EE5B0"],
|
|
2730
|
-
"playwright-qa-verifier": ["#22D3EE", "#67E8F9"],
|
|
2731
|
-
"claude-code-guide": ["#EC4899", "#F472B6"],
|
|
2732
|
-
};
|
|
2733
|
-
const [base, bright] = palette[agentType] ?? ["#8A8580", "#A8A19A"];
|
|
2734
|
-
return background ? bright : base;
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
/** Workflow signature color, modulated by run status. Deliberately a warm
|
|
2738
|
-
* orange — distinct from the blue/purple subagent palette so workflow lanes
|
|
2739
|
-
* read as a different tier on the minimap. Failed runs go red, in-flight
|
|
2740
|
-
* runs go green. */
|
|
2741
|
-
function workflowColor(status: string): string {
|
|
2742
|
-
const s = status.toLowerCase();
|
|
2743
|
-
if (s === "failed" || s === "aborted" || s === "error") return "#EF4444";
|
|
2744
|
-
if (s === "running" || s === "in_progress" || s === "active") return "#10B981";
|
|
2745
|
-
return "#EA580C";
|
|
2746
|
-
}
|
|
2507
|
+
// subagentColor, workflowColor moved to ./session-view/colors.ts
|
|
2747
2508
|
|
|
2748
2509
|
function MinimapHoverCard({
|
|
2749
2510
|
containerRef,
|
|
@@ -3344,6 +3105,7 @@ function TranscriptList({
|
|
|
3344
3105
|
sessionDays,
|
|
3345
3106
|
entryByDay,
|
|
3346
3107
|
dayStripRefs,
|
|
3108
|
+
onJumpAcrossDay,
|
|
3347
3109
|
}: {
|
|
3348
3110
|
displayRows: DisplayRow[];
|
|
3349
3111
|
rowRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
|
|
@@ -3364,6 +3126,14 @@ function TranscriptList({
|
|
|
3364
3126
|
sessionDays: { key: string; startMs: number; endMs: number; count: number }[];
|
|
3365
3127
|
entryByDay: Map<string, Entry>;
|
|
3366
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;
|
|
3367
3137
|
}) {
|
|
3368
3138
|
// Find the last collapsed turn index so we can mark it as in-progress
|
|
3369
3139
|
// when the session is live.
|
|
@@ -3396,6 +3166,10 @@ function TranscriptList({
|
|
|
3396
3166
|
if (d.kind === "turn-expanded-footer") return undefined;
|
|
3397
3167
|
return d.row.tOffsetMs;
|
|
3398
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;
|
|
3399
3173
|
/** Which local day a row falls in: the last day bucket whose start is at or
|
|
3400
3174
|
* before the row's offset. sessionDays is pre-sorted by startMs. */
|
|
3401
3175
|
const dayKeyForOffset = (tOff: number | undefined): string | null => {
|
|
@@ -3435,10 +3209,27 @@ function TranscriptList({
|
|
|
3435
3209
|
}
|
|
3436
3210
|
// Day boundary: drop this day's digest card at its first row (after any
|
|
3437
3211
|
// overnight idle divider that led into the day) so per-day perception
|
|
3438
|
-
// sits inline with that day's work, not stacked at the top.
|
|
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.
|
|
3439
3216
|
const dk = dayKeyForOffset(rowTOffset(d));
|
|
3440
3217
|
if (dk && dk !== currentDayKey) {
|
|
3218
|
+
const endedKey = currentDayKey; // day that just ended; null on the first day
|
|
3441
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
|
+
}
|
|
3442
3233
|
const dayEntry = entryByDay.get(dk);
|
|
3443
3234
|
if (dayEntry) {
|
|
3444
3235
|
out.push(
|
|
@@ -3453,6 +3244,16 @@ function TranscriptList({
|
|
|
3453
3244
|
</div>,
|
|
3454
3245
|
);
|
|
3455
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
|
+
}
|
|
3456
3257
|
}
|
|
3457
3258
|
}
|
|
3458
3259
|
|
|
@@ -3550,30 +3351,75 @@ function IdleDivider({ gapMs }: { gapMs: number }) {
|
|
|
3550
3351
|
);
|
|
3551
3352
|
}
|
|
3552
3353
|
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3354
|
+
/** Inline cross-day jump control rendered at each day boundary. "down" sits at
|
|
3355
|
+
* the bottom of the day that just ended and jumps to the start of the next day;
|
|
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}`}
|
|
3378
|
+
style={{
|
|
3379
|
+
display: "inline-flex",
|
|
3380
|
+
alignItems: "center",
|
|
3381
|
+
gap: 6,
|
|
3382
|
+
fontSize: 11,
|
|
3383
|
+
fontFamily: "var(--font-mono)",
|
|
3384
|
+
color: "var(--af-text-secondary)",
|
|
3385
|
+
background: "var(--af-surface)",
|
|
3386
|
+
border: "1px solid var(--af-border-subtle)",
|
|
3387
|
+
borderRadius: 100,
|
|
3388
|
+
padding: "3px 12px",
|
|
3389
|
+
cursor: "pointer",
|
|
3390
|
+
}}
|
|
3391
|
+
>
|
|
3392
|
+
{dir === "up" ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
|
3393
|
+
{dir === "up" ? `End of ${label}` : `Start of ${label}`}
|
|
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
|
+
}
|
|
3577
3423
|
return null;
|
|
3578
3424
|
}
|
|
3579
3425
|
|
|
@@ -4643,170 +4489,13 @@ function ToolGroupLabel({
|
|
|
4643
4489
|
);
|
|
4644
4490
|
}
|
|
4645
4491
|
|
|
4646
|
-
|
|
4647
|
-
const [hover, setHover] = useState(false);
|
|
4648
|
-
const totalIn = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
4649
|
-
const pctRead = totalIn > 0 ? Math.round((usage.cacheRead / totalIn) * 100) : 0;
|
|
4650
|
-
return (
|
|
4651
|
-
<span
|
|
4652
|
-
style={{
|
|
4653
|
-
position: "relative",
|
|
4654
|
-
fontFamily: "var(--font-mono)",
|
|
4655
|
-
fontSize: 11,
|
|
4656
|
-
color: "var(--af-text-secondary)",
|
|
4657
|
-
display: "inline-flex",
|
|
4658
|
-
alignItems: "center",
|
|
4659
|
-
gap: 4,
|
|
4660
|
-
whiteSpace: "nowrap",
|
|
4661
|
-
}}
|
|
4662
|
-
onMouseEnter={() => setHover(true)}
|
|
4663
|
-
onMouseLeave={() => setHover(false)}
|
|
4664
|
-
>
|
|
4665
|
-
<span style={{ opacity: 0.6 }}>▤</span>
|
|
4666
|
-
{formatTokens(totalIn)} / {formatTokens(usage.output)}
|
|
4667
|
-
{hover && (
|
|
4668
|
-
<Tooltip style={{ right: 0, top: "calc(100% + 6px)", minWidth: 180 }}>
|
|
4669
|
-
<TooltipRow label="Input" value={usage.input.toLocaleString()} />
|
|
4670
|
-
<TooltipRow
|
|
4671
|
-
label="Cache read"
|
|
4672
|
-
value={`${usage.cacheRead.toLocaleString()} (${pctRead}%)`}
|
|
4673
|
-
/>
|
|
4674
|
-
<TooltipRow label="Cache write" value={usage.cacheWrite.toLocaleString()} />
|
|
4675
|
-
<TooltipRow label="Output" value={usage.output.toLocaleString()} />
|
|
4676
|
-
</Tooltip>
|
|
4677
|
-
)}
|
|
4678
|
-
</span>
|
|
4679
|
-
);
|
|
4680
|
-
}
|
|
4681
|
-
|
|
4682
|
-
/** Like TokenChip but for per-turn aggregates. Primary display is
|
|
4683
|
-
* fresh input / output (not the cache-inflated sum), consistent with
|
|
4684
|
-
* the session header. Tooltip shows the full breakdown plus a footnote
|
|
4685
|
-
* reminding that cache reads are cumulative. */
|
|
4686
|
-
function TurnTokenChip({
|
|
4687
|
-
usage,
|
|
4688
|
-
}: {
|
|
4689
|
-
usage: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
4690
|
-
}) {
|
|
4691
|
-
const [hover, setHover] = useState(false);
|
|
4692
|
-
const totalIn = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
4693
|
-
const pctRead = totalIn > 0 ? Math.round((usage.cacheRead / totalIn) * 100) : 0;
|
|
4694
|
-
return (
|
|
4695
|
-
<span
|
|
4696
|
-
style={{
|
|
4697
|
-
position: "relative",
|
|
4698
|
-
fontFamily: "var(--font-mono)",
|
|
4699
|
-
fontSize: 11,
|
|
4700
|
-
color: "var(--af-text-tertiary)",
|
|
4701
|
-
display: "inline-flex",
|
|
4702
|
-
alignItems: "center",
|
|
4703
|
-
gap: 4,
|
|
4704
|
-
whiteSpace: "nowrap",
|
|
4705
|
-
cursor: "default",
|
|
4706
|
-
}}
|
|
4707
|
-
onMouseEnter={() => setHover(true)}
|
|
4708
|
-
onMouseLeave={() => setHover(false)}
|
|
4709
|
-
>
|
|
4710
|
-
<span style={{ opacity: 0.6 }}>▤</span>
|
|
4711
|
-
{formatTokens(usage.input)} / {formatTokens(usage.output)}
|
|
4712
|
-
{hover && (
|
|
4713
|
-
<Tooltip style={{ right: 0, top: "calc(100% + 6px)", minWidth: 220 }}>
|
|
4714
|
-
<TooltipRow label="Input (fresh)" value={usage.input.toLocaleString()} />
|
|
4715
|
-
<TooltipRow label="Output" value={usage.output.toLocaleString()} />
|
|
4716
|
-
<TooltipRow
|
|
4717
|
-
label="Cache read"
|
|
4718
|
-
value={`${usage.cacheRead.toLocaleString()} (${pctRead}%)`}
|
|
4719
|
-
/>
|
|
4720
|
-
<TooltipRow label="Cache write" value={usage.cacheWrite.toLocaleString()} />
|
|
4721
|
-
<div
|
|
4722
|
-
style={{
|
|
4723
|
-
marginTop: 6,
|
|
4724
|
-
paddingTop: 6,
|
|
4725
|
-
borderTop: "1px solid rgba(241,245,249,0.12)",
|
|
4726
|
-
opacity: 0.65,
|
|
4727
|
-
fontSize: 10,
|
|
4728
|
-
whiteSpace: "normal",
|
|
4729
|
-
lineHeight: 1.4,
|
|
4730
|
-
}}
|
|
4731
|
-
>
|
|
4732
|
-
Cumulative across all requests in this turn.
|
|
4733
|
-
</div>
|
|
4734
|
-
</Tooltip>
|
|
4735
|
-
)}
|
|
4736
|
-
</span>
|
|
4737
|
-
);
|
|
4738
|
-
}
|
|
4492
|
+
// TokenChip, TurnTokenChip moved to ./session-view/token-stats.tsx
|
|
4739
4493
|
|
|
4740
4494
|
/* ------------------------------------------------------------------ */
|
|
4741
4495
|
/* Debug list */
|
|
4742
4496
|
/* ------------------------------------------------------------------ */
|
|
4743
4497
|
|
|
4744
|
-
|
|
4745
|
-
// Rebuild a JSON view from the structured fields on the event. The
|
|
4746
|
-
// original `raw` field is stripped server-side before serialization
|
|
4747
|
-
// (see app/sessions/[id]/page.tsx) because including the full JSONL
|
|
4748
|
-
// line per event doubles the RSC payload on large sessions. All the
|
|
4749
|
-
// useful data is already on the structured event; this view surfaces
|
|
4750
|
-
// it in the same shape you'd get from the raw JSONL.
|
|
4751
|
-
const shapeForDebug = (e: SessionEvent) => ({
|
|
4752
|
-
type: e.rawType,
|
|
4753
|
-
index: e.index,
|
|
4754
|
-
uuid: e.uuid,
|
|
4755
|
-
parentUuid: e.parentUuid,
|
|
4756
|
-
timestamp: e.timestamp,
|
|
4757
|
-
role: e.role,
|
|
4758
|
-
messageId: e.messageId,
|
|
4759
|
-
stopReason: e.stopReason,
|
|
4760
|
-
model: e.model,
|
|
4761
|
-
requestId: e.requestId,
|
|
4762
|
-
toolName: e.toolName,
|
|
4763
|
-
toolUseId: e.toolUseId,
|
|
4764
|
-
attachmentType: e.attachmentType,
|
|
4765
|
-
usage: e.usage,
|
|
4766
|
-
blocks: e.blocks,
|
|
4767
|
-
});
|
|
4768
|
-
|
|
4769
|
-
return (
|
|
4770
|
-
<div style={{ padding: "8px 0" }}>
|
|
4771
|
-
{events.map((e) => (
|
|
4772
|
-
<details
|
|
4773
|
-
key={e.index}
|
|
4774
|
-
style={{
|
|
4775
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4776
|
-
padding: "8px 12px",
|
|
4777
|
-
}}
|
|
4778
|
-
>
|
|
4779
|
-
<summary
|
|
4780
|
-
style={{
|
|
4781
|
-
cursor: "pointer",
|
|
4782
|
-
fontFamily: "var(--font-mono)",
|
|
4783
|
-
fontSize: 12,
|
|
4784
|
-
color: "var(--af-text-secondary)",
|
|
4785
|
-
}}
|
|
4786
|
-
>
|
|
4787
|
-
#{e.index} · {e.rawType}
|
|
4788
|
-
{e.attachmentType ? `/${e.attachmentType}` : ""} · {e.timestamp ?? "(no ts)"}
|
|
4789
|
-
</summary>
|
|
4790
|
-
<pre
|
|
4791
|
-
style={{
|
|
4792
|
-
marginTop: 8,
|
|
4793
|
-
padding: 12,
|
|
4794
|
-
background: "var(--background)",
|
|
4795
|
-
border: "1px solid var(--af-border-subtle)",
|
|
4796
|
-
borderRadius: 6,
|
|
4797
|
-
fontSize: 11,
|
|
4798
|
-
overflow: "auto",
|
|
4799
|
-
maxHeight: 400,
|
|
4800
|
-
color: "var(--af-text-secondary)",
|
|
4801
|
-
}}
|
|
4802
|
-
>
|
|
4803
|
-
{JSON.stringify(shapeForDebug(e), null, 2)}
|
|
4804
|
-
</pre>
|
|
4805
|
-
</details>
|
|
4806
|
-
))}
|
|
4807
|
-
</div>
|
|
4808
|
-
);
|
|
4809
|
-
}
|
|
4498
|
+
// DebugList moved to ./session-view/debug-list.tsx
|
|
4810
4499
|
|
|
4811
4500
|
/* ------------------------------------------------------------------ */
|
|
4812
4501
|
/* SubagentDrawer */
|
|
@@ -4819,412 +4508,98 @@ function DebugList({ events }: { events: SessionEvent[] }) {
|
|
|
4819
4508
|
/* this subagent off. */
|
|
4820
4509
|
/* ------------------------------------------------------------------ */
|
|
4821
4510
|
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
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,
|
|
4825
4528
|
onJumpToParent,
|
|
4529
|
+
onClearFocus,
|
|
4826
4530
|
}: {
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
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;
|
|
4830
4538
|
}) {
|
|
4831
|
-
const
|
|
4832
|
-
const
|
|
4833
|
-
const startOff = formatOffset(s.startTOffsetMs);
|
|
4834
|
-
const endOff = formatOffset(s.endTOffsetMs);
|
|
4835
|
-
const dur = s.durationMs !== undefined ? formatGap(s.durationMs) : "—";
|
|
4836
|
-
const totalIn = s.totalUsage.input + s.totalUsage.cacheRead + s.totalUsage.cacheWrite;
|
|
4837
|
-
const pctRead = totalIn > 0 ? Math.round((s.totalUsage.cacheRead / totalIn) * 100) : 0;
|
|
4838
|
-
|
|
4539
|
+
const totalTokens = workflows.reduce((n, w) => n + w.totalTokens, 0);
|
|
4540
|
+
const totalTools = workflows.reduce((n, w) => n + w.toolCallCount, 0);
|
|
4839
4541
|
return (
|
|
4840
|
-
<
|
|
4841
|
-
{
|
|
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
|
+
>
|
|
4842
4551
|
<div
|
|
4843
4552
|
style={{
|
|
4844
|
-
padding: "14px 20px",
|
|
4845
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4846
4553
|
display: "flex",
|
|
4847
4554
|
alignItems: "center",
|
|
4848
4555
|
gap: 10,
|
|
4849
|
-
background: "var(--af-surface)",
|
|
4850
|
-
position: "sticky",
|
|
4851
|
-
top: 0,
|
|
4852
|
-
zIndex: 1,
|
|
4853
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))",
|
|
4854
4560
|
}}
|
|
4855
4561
|
>
|
|
4856
4562
|
<span
|
|
4857
4563
|
style={{
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
background: fill,
|
|
4863
|
-
color: "#fff",
|
|
4864
|
-
}}
|
|
4865
|
-
>
|
|
4866
|
-
{s.agentType}
|
|
4867
|
-
</span>
|
|
4868
|
-
{s.runInBackground && (
|
|
4869
|
-
<span
|
|
4870
|
-
style={{
|
|
4871
|
-
fontSize: 9,
|
|
4872
|
-
fontWeight: 600,
|
|
4873
|
-
padding: "2px 8px",
|
|
4874
|
-
borderRadius: 4,
|
|
4875
|
-
background: "var(--af-warning-subtle)",
|
|
4876
|
-
color: "var(--af-warning)",
|
|
4877
|
-
textTransform: "uppercase",
|
|
4878
|
-
letterSpacing: "0.04em",
|
|
4879
|
-
}}
|
|
4880
|
-
>
|
|
4881
|
-
background
|
|
4882
|
-
</span>
|
|
4883
|
-
)}
|
|
4884
|
-
<span
|
|
4885
|
-
style={{
|
|
4886
|
-
fontSize: 14,
|
|
4564
|
+
display: "inline-flex",
|
|
4565
|
+
alignItems: "center",
|
|
4566
|
+
gap: 7,
|
|
4567
|
+
fontSize: 13,
|
|
4887
4568
|
fontWeight: 600,
|
|
4888
4569
|
color: "var(--af-text)",
|
|
4889
|
-
flex: 1,
|
|
4890
|
-
minWidth: 0,
|
|
4891
|
-
overflow: "hidden",
|
|
4892
|
-
textOverflow: "ellipsis",
|
|
4893
|
-
whiteSpace: "nowrap",
|
|
4894
4570
|
}}
|
|
4895
|
-
title={s.description}
|
|
4896
4571
|
>
|
|
4897
|
-
{
|
|
4572
|
+
<WorkflowIcon size={14} style={{ color: "#EA580C" }} />
|
|
4573
|
+
Workflows
|
|
4898
4574
|
</span>
|
|
4899
|
-
<
|
|
4900
|
-
onClick={onClose}
|
|
4575
|
+
<span
|
|
4901
4576
|
style={{
|
|
4902
|
-
|
|
4903
|
-
border: "none",
|
|
4904
|
-
cursor: "pointer",
|
|
4577
|
+
fontSize: 11.5,
|
|
4905
4578
|
color: "var(--af-text-tertiary)",
|
|
4906
|
-
|
|
4907
|
-
borderRadius: 4,
|
|
4579
|
+
fontFamily: "var(--font-mono)",
|
|
4908
4580
|
}}
|
|
4909
|
-
aria-label="Close"
|
|
4910
4581
|
>
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
{/* Meta strip */}
|
|
4916
|
-
<div
|
|
4917
|
-
style={{
|
|
4918
|
-
padding: "10px 20px",
|
|
4919
|
-
fontFamily: "var(--font-mono)",
|
|
4920
|
-
fontSize: 11,
|
|
4921
|
-
color: "var(--af-text-tertiary)",
|
|
4922
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4923
|
-
display: "grid",
|
|
4924
|
-
gridTemplateColumns: "auto 1fr",
|
|
4925
|
-
columnGap: 10,
|
|
4926
|
-
rowGap: 3,
|
|
4927
|
-
}}
|
|
4928
|
-
>
|
|
4929
|
-
<span style={{ opacity: 0.7 }}>range</span>
|
|
4930
|
-
<span style={{ color: "var(--af-text-secondary)" }}>
|
|
4931
|
-
{startOff} → {endOff}
|
|
4582
|
+
{workflows.length} run{workflows.length === 1 ? "" : "s"} ·{" "}
|
|
4583
|
+
{spawnedAgentCount.toLocaleString()} agents · {totalTools.toLocaleString()} tool calls ·{" "}
|
|
4584
|
+
{formatTokens(totalTokens)} tok orchestrated
|
|
4932
4585
|
</span>
|
|
4933
|
-
<span style={{ opacity: 0.7 }}>duration</span>
|
|
4934
|
-
<span style={{ color: "var(--af-text-secondary)" }}>{dur}</span>
|
|
4935
|
-
{s.model && (
|
|
4936
|
-
<>
|
|
4937
|
-
<span style={{ opacity: 0.7 }}>model</span>
|
|
4938
|
-
<span style={{ color: "var(--af-text-secondary)" }}>{s.model}</span>
|
|
4939
|
-
</>
|
|
4940
|
-
)}
|
|
4941
|
-
{s.parentToolUseId && (
|
|
4942
|
-
<>
|
|
4943
|
-
<span style={{ opacity: 0.7 }}>parent</span>
|
|
4944
|
-
<span style={{ color: "var(--af-text-secondary)" }}>
|
|
4945
|
-
{s.parentToolUseId.slice(0, 24)}…
|
|
4946
|
-
</span>
|
|
4947
|
-
</>
|
|
4948
|
-
)}
|
|
4949
4586
|
</div>
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
<StatCell label="Messages" value={String(s.assistantMessageCount ?? 0)} />
|
|
4963
|
-
<StatCell label="Tool calls" value={String(s.toolCallCount ?? 0)} />
|
|
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
|
+
))}
|
|
4964
4599
|
</div>
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
style={{
|
|
4969
|
-
padding: "12px 20px",
|
|
4970
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4971
|
-
fontSize: 11,
|
|
4972
|
-
fontFamily: "var(--font-mono)",
|
|
4973
|
-
color: "var(--af-text-secondary)",
|
|
4974
|
-
display: "flex",
|
|
4975
|
-
flexDirection: "column",
|
|
4976
|
-
gap: 3,
|
|
4977
|
-
}}
|
|
4978
|
-
>
|
|
4979
|
-
<TokenLine label="Input (fresh)" value={s.totalUsage.input} />
|
|
4980
|
-
<TokenLine label="Output" value={s.totalUsage.output} />
|
|
4981
|
-
<TokenLine
|
|
4982
|
-
label="Cache read"
|
|
4983
|
-
value={s.totalUsage.cacheRead}
|
|
4984
|
-
suffix={` (${pctRead}%)`}
|
|
4985
|
-
/>
|
|
4986
|
-
<TokenLine label="Cache write" value={s.totalUsage.cacheWrite} />
|
|
4987
|
-
</div>
|
|
4988
|
-
|
|
4989
|
-
{/* Tool breakdown */}
|
|
4990
|
-
{s.toolCalls && s.toolCalls.length > 0 && (
|
|
4991
|
-
<div
|
|
4992
|
-
style={{
|
|
4993
|
-
padding: "14px 20px",
|
|
4994
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4995
|
-
}}
|
|
4996
|
-
>
|
|
4997
|
-
<div
|
|
4998
|
-
style={{
|
|
4999
|
-
fontSize: 10,
|
|
5000
|
-
fontWeight: 600,
|
|
5001
|
-
textTransform: "uppercase",
|
|
5002
|
-
letterSpacing: "0.04em",
|
|
5003
|
-
color: "var(--af-text-tertiary)",
|
|
5004
|
-
marginBottom: 8,
|
|
5005
|
-
}}
|
|
5006
|
-
>
|
|
5007
|
-
Tools used
|
|
5008
|
-
</div>
|
|
5009
|
-
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
|
5010
|
-
{s.toolCalls.map((t) => (
|
|
5011
|
-
<span
|
|
5012
|
-
key={t.name}
|
|
5013
|
-
style={{
|
|
5014
|
-
display: "inline-flex",
|
|
5015
|
-
alignItems: "center",
|
|
5016
|
-
gap: 4,
|
|
5017
|
-
fontSize: 11,
|
|
5018
|
-
padding: "2px 8px",
|
|
5019
|
-
borderRadius: 4,
|
|
5020
|
-
background: "var(--af-border-subtle)",
|
|
5021
|
-
color: "var(--af-text)",
|
|
5022
|
-
fontFamily: "var(--font-mono)",
|
|
5023
|
-
}}
|
|
5024
|
-
>
|
|
5025
|
-
<b style={{ fontWeight: 600 }}>{shortenToolName(t.name)}</b>
|
|
5026
|
-
<span style={{ color: "var(--af-text-tertiary)" }}>×{t.count}</span>
|
|
5027
|
-
</span>
|
|
5028
|
-
))}
|
|
5029
|
-
</div>
|
|
5030
|
-
</div>
|
|
5031
|
-
)}
|
|
5032
|
-
|
|
5033
|
-
{/* Parent prompt (what the parent asked the subagent to do) */}
|
|
5034
|
-
{s.prompt && (
|
|
5035
|
-
<div
|
|
5036
|
-
style={{
|
|
5037
|
-
padding: "14px 20px",
|
|
5038
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
5039
|
-
}}
|
|
5040
|
-
>
|
|
5041
|
-
<div
|
|
5042
|
-
style={{
|
|
5043
|
-
display: "flex",
|
|
5044
|
-
alignItems: "center",
|
|
5045
|
-
justifyContent: "space-between",
|
|
5046
|
-
marginBottom: 8,
|
|
5047
|
-
}}
|
|
5048
|
-
>
|
|
5049
|
-
<div
|
|
5050
|
-
style={{
|
|
5051
|
-
fontSize: 10,
|
|
5052
|
-
fontWeight: 600,
|
|
5053
|
-
textTransform: "uppercase",
|
|
5054
|
-
letterSpacing: "0.04em",
|
|
5055
|
-
color: "var(--af-text-tertiary)",
|
|
5056
|
-
}}
|
|
5057
|
-
>
|
|
5058
|
-
Prompt
|
|
5059
|
-
</div>
|
|
5060
|
-
{s.parentToolUseId && (
|
|
5061
|
-
<button
|
|
5062
|
-
type="button"
|
|
5063
|
-
onClick={onJumpToParent}
|
|
5064
|
-
style={{
|
|
5065
|
-
fontSize: 10,
|
|
5066
|
-
color: "var(--af-accent)",
|
|
5067
|
-
background: "transparent",
|
|
5068
|
-
border: "none",
|
|
5069
|
-
cursor: "pointer",
|
|
5070
|
-
padding: 0,
|
|
5071
|
-
}}
|
|
5072
|
-
>
|
|
5073
|
-
Jump to parent →
|
|
5074
|
-
</button>
|
|
5075
|
-
)}
|
|
5076
|
-
</div>
|
|
5077
|
-
<div className="sl-prose" style={{ fontSize: 12.5 }}>
|
|
5078
|
-
<ReactMarkdown
|
|
5079
|
-
remarkPlugins={[remarkGfm]}
|
|
5080
|
-
components={{
|
|
5081
|
-
a: (props) => (
|
|
5082
|
-
<a
|
|
5083
|
-
{...props}
|
|
5084
|
-
target="_blank"
|
|
5085
|
-
rel="noopener noreferrer"
|
|
5086
|
-
style={{
|
|
5087
|
-
color: "var(--af-accent)",
|
|
5088
|
-
textDecoration: "underline",
|
|
5089
|
-
}}
|
|
5090
|
-
/>
|
|
5091
|
-
),
|
|
5092
|
-
}}
|
|
5093
|
-
>
|
|
5094
|
-
{s.prompt}
|
|
5095
|
-
</ReactMarkdown>
|
|
5096
|
-
</div>
|
|
5097
|
-
</div>
|
|
5098
|
-
)}
|
|
5099
|
-
|
|
5100
|
-
{/* Final text */}
|
|
5101
|
-
{s.finalText && (
|
|
5102
|
-
<div style={{ padding: "14px 20px" }}>
|
|
5103
|
-
<div
|
|
5104
|
-
style={{
|
|
5105
|
-
fontSize: 10,
|
|
5106
|
-
fontWeight: 600,
|
|
5107
|
-
textTransform: "uppercase",
|
|
5108
|
-
letterSpacing: "0.04em",
|
|
5109
|
-
color: "var(--af-text-tertiary)",
|
|
5110
|
-
marginBottom: 8,
|
|
5111
|
-
}}
|
|
5112
|
-
>
|
|
5113
|
-
Final result
|
|
5114
|
-
</div>
|
|
5115
|
-
<div className="sl-prose" style={{ fontSize: 13 }}>
|
|
5116
|
-
<ReactMarkdown
|
|
5117
|
-
remarkPlugins={[remarkGfm]}
|
|
5118
|
-
components={{
|
|
5119
|
-
a: (props) => (
|
|
5120
|
-
<a
|
|
5121
|
-
{...props}
|
|
5122
|
-
target="_blank"
|
|
5123
|
-
rel="noopener noreferrer"
|
|
5124
|
-
style={{
|
|
5125
|
-
color: "var(--af-accent)",
|
|
5126
|
-
textDecoration: "underline",
|
|
5127
|
-
}}
|
|
5128
|
-
/>
|
|
5129
|
-
),
|
|
5130
|
-
}}
|
|
5131
|
-
>
|
|
5132
|
-
{s.finalText}
|
|
5133
|
-
</ReactMarkdown>
|
|
5134
|
-
</div>
|
|
5135
|
-
</div>
|
|
5136
|
-
)}
|
|
5137
|
-
</div>
|
|
5138
|
-
);
|
|
5139
|
-
}
|
|
5140
|
-
|
|
5141
|
-
/* ------------------------------------------------------------------ */
|
|
5142
|
-
/* Workflows panel */
|
|
5143
|
-
/* */
|
|
5144
|
-
/* A single `Workflow` tool call collapses a whole dynamic-workflow */
|
|
5145
|
-
/* fan-out into one transcript row. This panel surfaces what that row */
|
|
5146
|
-
/* actually did: each run's spawned-agent count, tool calls, tokens, */
|
|
5147
|
-
/* phases, and progress log — the fleet work the transcript hides. */
|
|
5148
|
-
/* ------------------------------------------------------------------ */
|
|
5149
|
-
|
|
5150
|
-
function WorkflowsPanel({
|
|
5151
|
-
workflows,
|
|
5152
|
-
spawnedAgentCount,
|
|
5153
|
-
focusRunId,
|
|
5154
|
-
selectedAgentKey,
|
|
5155
|
-
onOpenAgent,
|
|
5156
|
-
onJumpToParent,
|
|
5157
|
-
}: {
|
|
5158
|
-
workflows: WorkflowRun[];
|
|
5159
|
-
spawnedAgentCount: number;
|
|
5160
|
-
focusRunId?: string | null;
|
|
5161
|
-
selectedAgentKey?: string | null;
|
|
5162
|
-
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
5163
|
-
onJumpToParent: (toolUseId?: string) => void;
|
|
5164
|
-
}) {
|
|
5165
|
-
const totalTokens = workflows.reduce((n, w) => n + w.totalTokens, 0);
|
|
5166
|
-
const totalTools = workflows.reduce((n, w) => n + w.toolCallCount, 0);
|
|
5167
|
-
return (
|
|
5168
|
-
<section
|
|
5169
|
-
style={{
|
|
5170
|
-
marginBottom: 16,
|
|
5171
|
-
border: "1px solid var(--af-border-subtle)",
|
|
5172
|
-
borderRadius: 10,
|
|
5173
|
-
background: "var(--af-surface)",
|
|
5174
|
-
overflow: "hidden",
|
|
5175
|
-
}}
|
|
5176
|
-
>
|
|
5177
|
-
<div
|
|
5178
|
-
style={{
|
|
5179
|
-
display: "flex",
|
|
5180
|
-
alignItems: "center",
|
|
5181
|
-
gap: 10,
|
|
5182
|
-
flexWrap: "wrap",
|
|
5183
|
-
padding: "10px 14px",
|
|
5184
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
5185
|
-
background: "var(--af-surface-subtle, rgba(234,88,12,0.04))",
|
|
5186
|
-
}}
|
|
5187
|
-
>
|
|
5188
|
-
<span
|
|
5189
|
-
style={{
|
|
5190
|
-
display: "inline-flex",
|
|
5191
|
-
alignItems: "center",
|
|
5192
|
-
gap: 7,
|
|
5193
|
-
fontSize: 13,
|
|
5194
|
-
fontWeight: 600,
|
|
5195
|
-
color: "var(--af-text)",
|
|
5196
|
-
}}
|
|
5197
|
-
>
|
|
5198
|
-
<WorkflowIcon size={14} style={{ color: "#EA580C" }} />
|
|
5199
|
-
Workflows
|
|
5200
|
-
</span>
|
|
5201
|
-
<span
|
|
5202
|
-
style={{
|
|
5203
|
-
fontSize: 11.5,
|
|
5204
|
-
color: "var(--af-text-tertiary)",
|
|
5205
|
-
fontFamily: "var(--font-mono)",
|
|
5206
|
-
}}
|
|
5207
|
-
>
|
|
5208
|
-
{workflows.length} run{workflows.length === 1 ? "" : "s"} ·{" "}
|
|
5209
|
-
{spawnedAgentCount.toLocaleString()} agents · {totalTools.toLocaleString()} tool calls ·{" "}
|
|
5210
|
-
{formatTokens(totalTokens)} tok orchestrated
|
|
5211
|
-
</span>
|
|
5212
|
-
</div>
|
|
5213
|
-
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
5214
|
-
{workflows.map((w) => (
|
|
5215
|
-
<WorkflowCard
|
|
5216
|
-
key={w.runId}
|
|
5217
|
-
w={w}
|
|
5218
|
-
focused={focusRunId === w.runId}
|
|
5219
|
-
selectedAgentKey={selectedAgentKey}
|
|
5220
|
-
onOpenAgent={onOpenAgent}
|
|
5221
|
-
onJumpToParent={onJumpToParent}
|
|
5222
|
-
/>
|
|
5223
|
-
))}
|
|
5224
|
-
</div>
|
|
5225
|
-
</section>
|
|
5226
|
-
);
|
|
5227
|
-
}
|
|
4600
|
+
</section>
|
|
4601
|
+
);
|
|
4602
|
+
}
|
|
5228
4603
|
|
|
5229
4604
|
function WorkflowCard({
|
|
5230
4605
|
w,
|
|
@@ -5232,12 +4607,14 @@ function WorkflowCard({
|
|
|
5232
4607
|
selectedAgentKey,
|
|
5233
4608
|
onOpenAgent,
|
|
5234
4609
|
onJumpToParent,
|
|
4610
|
+
onClearFocus,
|
|
5235
4611
|
}: {
|
|
5236
4612
|
w: WorkflowRun;
|
|
5237
4613
|
focused?: boolean;
|
|
5238
4614
|
selectedAgentKey?: string | null;
|
|
5239
4615
|
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
5240
4616
|
onJumpToParent: (toolUseId?: string) => void;
|
|
4617
|
+
onClearFocus?: () => void;
|
|
5241
4618
|
}) {
|
|
5242
4619
|
const [open, setOpen] = useState(focused);
|
|
5243
4620
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
@@ -5257,14 +4634,17 @@ function WorkflowCard({
|
|
|
5257
4634
|
ref={cardRef}
|
|
5258
4635
|
style={{
|
|
5259
4636
|
borderTop: "1px solid var(--af-border-subtle)",
|
|
5260
|
-
background: focused ? "rgba(234,88,12,0.07)" : "transparent",
|
|
5261
|
-
transition: "background 0.5s ease",
|
|
5262
4637
|
scrollMarginTop: 120,
|
|
5263
4638
|
}}
|
|
5264
4639
|
>
|
|
5265
4640
|
{/* Collapsed header — click to expand */}
|
|
5266
4641
|
<button
|
|
5267
|
-
onClick={() =>
|
|
4642
|
+
onClick={() => {
|
|
4643
|
+
setOpen((v) => !v);
|
|
4644
|
+
// Any toggle is the user taking over from the timeline jump — drop the
|
|
4645
|
+
// focus highlight so it doesn't linger on a card they've moved past.
|
|
4646
|
+
onClearFocus?.();
|
|
4647
|
+
}}
|
|
5268
4648
|
style={{
|
|
5269
4649
|
width: "100%",
|
|
5270
4650
|
display: "flex",
|
|
@@ -5287,7 +4667,11 @@ function WorkflowCard({
|
|
|
5287
4667
|
fontFamily: "var(--font-mono)",
|
|
5288
4668
|
fontSize: 13,
|
|
5289
4669
|
fontWeight: 600,
|
|
5290
|
-
color: "var(--af-text)",
|
|
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",
|
|
5291
4675
|
minWidth: 0,
|
|
5292
4676
|
overflow: "hidden",
|
|
5293
4677
|
textOverflow: "ellipsis",
|
|
@@ -5369,7 +4753,7 @@ function WorkflowCard({
|
|
|
5369
4753
|
fontFamily: "var(--font-mono)",
|
|
5370
4754
|
}}
|
|
5371
4755
|
>
|
|
5372
|
-
Jump to
|
|
4756
|
+
Jump to timeline →
|
|
5373
4757
|
</button>
|
|
5374
4758
|
)}
|
|
5375
4759
|
</div>
|
|
@@ -6020,593 +5404,18 @@ function WorkflowRunLog({ logs }: { logs: string[] }) {
|
|
|
6020
5404
|
);
|
|
6021
5405
|
}
|
|
6022
5406
|
|
|
6023
|
-
|
|
6024
|
-
return (
|
|
6025
|
-
<span
|
|
6026
|
-
style={{
|
|
6027
|
-
display: "inline-flex",
|
|
6028
|
-
alignItems: "center",
|
|
6029
|
-
gap: 4,
|
|
6030
|
-
fontSize: 11.5,
|
|
6031
|
-
color: "var(--af-text-secondary)",
|
|
6032
|
-
fontFamily: "var(--font-mono)",
|
|
6033
|
-
whiteSpace: "nowrap",
|
|
6034
|
-
}}
|
|
6035
|
-
>
|
|
6036
|
-
{icon && <span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>{icon}</span>}
|
|
6037
|
-
{value}
|
|
6038
|
-
</span>
|
|
6039
|
-
);
|
|
6040
|
-
}
|
|
6041
|
-
|
|
6042
|
-
function SectionLabel({ children }: { children: React.ReactNode }) {
|
|
6043
|
-
return (
|
|
6044
|
-
<div
|
|
6045
|
-
style={{
|
|
6046
|
-
fontSize: 10,
|
|
6047
|
-
fontWeight: 600,
|
|
6048
|
-
textTransform: "uppercase",
|
|
6049
|
-
letterSpacing: "0.04em",
|
|
6050
|
-
color: "var(--af-text-tertiary)",
|
|
6051
|
-
marginBottom: 8,
|
|
6052
|
-
}}
|
|
6053
|
-
>
|
|
6054
|
-
{children}
|
|
6055
|
-
</div>
|
|
6056
|
-
);
|
|
6057
|
-
}
|
|
6058
|
-
|
|
6059
|
-
function StatCell({ label, value }: { label: string; value: string }) {
|
|
6060
|
-
return (
|
|
6061
|
-
<div>
|
|
6062
|
-
<div
|
|
6063
|
-
style={{
|
|
6064
|
-
fontSize: 9,
|
|
6065
|
-
color: "var(--af-text-tertiary)",
|
|
6066
|
-
textTransform: "uppercase",
|
|
6067
|
-
letterSpacing: "0.04em",
|
|
6068
|
-
fontWeight: 600,
|
|
6069
|
-
}}
|
|
6070
|
-
>
|
|
6071
|
-
{label}
|
|
6072
|
-
</div>
|
|
6073
|
-
<div
|
|
6074
|
-
style={{
|
|
6075
|
-
fontSize: 18,
|
|
6076
|
-
fontWeight: 700,
|
|
6077
|
-
fontFamily: "var(--font-mono)",
|
|
6078
|
-
color: "var(--af-text)",
|
|
6079
|
-
marginTop: 2,
|
|
6080
|
-
}}
|
|
6081
|
-
>
|
|
6082
|
-
{value}
|
|
6083
|
-
</div>
|
|
6084
|
-
</div>
|
|
6085
|
-
);
|
|
6086
|
-
}
|
|
6087
|
-
|
|
6088
|
-
function TokenLine({
|
|
6089
|
-
label,
|
|
6090
|
-
value,
|
|
6091
|
-
suffix,
|
|
6092
|
-
}: {
|
|
6093
|
-
label: string;
|
|
6094
|
-
value: number;
|
|
6095
|
-
suffix?: string;
|
|
6096
|
-
}) {
|
|
6097
|
-
return (
|
|
6098
|
-
<div style={{ display: "flex", justifyContent: "space-between", gap: 12 }}>
|
|
6099
|
-
<span style={{ opacity: 0.7 }}>{label}</span>
|
|
6100
|
-
<span>
|
|
6101
|
-
{value.toLocaleString()}
|
|
6102
|
-
{suffix && <span style={{ opacity: 0.65 }}>{suffix}</span>}
|
|
6103
|
-
</span>
|
|
6104
|
-
</div>
|
|
6105
|
-
);
|
|
6106
|
-
}
|
|
5407
|
+
// WfMiniStat, SectionLabel, StatCell, TokenLine moved to ./session-view/workflows-shared.tsx
|
|
6107
5408
|
|
|
6108
5409
|
/* ------------------------------------------------------------------ */
|
|
6109
5410
|
/* Drawer */
|
|
6110
5411
|
/* ------------------------------------------------------------------ */
|
|
6111
5412
|
|
|
6112
|
-
|
|
6113
|
-
event,
|
|
6114
|
-
row,
|
|
6115
|
-
onClose,
|
|
6116
|
-
}: {
|
|
6117
|
-
event: SessionEvent;
|
|
6118
|
-
row: PresentationRow | null;
|
|
6119
|
-
onClose: () => void;
|
|
6120
|
-
}) {
|
|
6121
|
-
const [showDev, setShowDev] = useState(false);
|
|
6122
|
-
const kind: PresentationRowKind = row?.kind ?? "agent";
|
|
6123
|
-
const theme = ROLE_THEMES[kind];
|
|
6124
|
-
const title = drawerTitle(row, event);
|
|
6125
|
-
const hasUsage = !!event.usage && (event.usage.input > 0 || event.usage.output > 0);
|
|
6126
|
-
|
|
6127
|
-
return (
|
|
6128
|
-
<div>
|
|
6129
|
-
{/* Sticky title bar */}
|
|
6130
|
-
<div
|
|
6131
|
-
style={{
|
|
6132
|
-
padding: "14px 20px",
|
|
6133
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
6134
|
-
display: "flex",
|
|
6135
|
-
alignItems: "center",
|
|
6136
|
-
gap: 10,
|
|
6137
|
-
background: "var(--af-surface)",
|
|
6138
|
-
position: "sticky",
|
|
6139
|
-
top: 0,
|
|
6140
|
-
zIndex: 1,
|
|
6141
|
-
}}
|
|
6142
|
-
>
|
|
6143
|
-
<span
|
|
6144
|
-
style={{
|
|
6145
|
-
fontSize: 11,
|
|
6146
|
-
fontWeight: 600,
|
|
6147
|
-
padding: "3px 10px",
|
|
6148
|
-
borderRadius: 4,
|
|
6149
|
-
background: theme.bg,
|
|
6150
|
-
color: theme.fg,
|
|
6151
|
-
}}
|
|
6152
|
-
>
|
|
6153
|
-
{theme.label}
|
|
6154
|
-
</span>
|
|
6155
|
-
<span
|
|
6156
|
-
style={{
|
|
6157
|
-
fontSize: 14,
|
|
6158
|
-
fontWeight: 600,
|
|
6159
|
-
color: "var(--af-text)",
|
|
6160
|
-
}}
|
|
6161
|
-
>
|
|
6162
|
-
{title}
|
|
6163
|
-
</span>
|
|
6164
|
-
<button
|
|
6165
|
-
onClick={onClose}
|
|
6166
|
-
style={{
|
|
6167
|
-
marginLeft: "auto",
|
|
6168
|
-
background: "transparent",
|
|
6169
|
-
border: "none",
|
|
6170
|
-
cursor: "pointer",
|
|
6171
|
-
color: "var(--af-text-tertiary)",
|
|
6172
|
-
padding: 4,
|
|
6173
|
-
borderRadius: 4,
|
|
6174
|
-
}}
|
|
6175
|
-
>
|
|
6176
|
-
<X size={16} />
|
|
6177
|
-
</button>
|
|
6178
|
-
</div>
|
|
6179
|
-
|
|
6180
|
-
{/* Meta line — compact */}
|
|
6181
|
-
<div
|
|
6182
|
-
style={{
|
|
6183
|
-
padding: "8px 20px",
|
|
6184
|
-
fontFamily: "var(--font-mono)",
|
|
6185
|
-
fontSize: 11,
|
|
6186
|
-
color: "var(--af-text-tertiary)",
|
|
6187
|
-
display: "flex",
|
|
6188
|
-
gap: 10,
|
|
6189
|
-
alignItems: "center",
|
|
6190
|
-
flexWrap: "wrap",
|
|
6191
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
6192
|
-
}}
|
|
6193
|
-
>
|
|
6194
|
-
<span>{formatOffset(event.tOffsetMs)}</span>
|
|
6195
|
-
{event.gapMs !== undefined && event.gapMs > 0 && <span>· {formatGap(event.gapMs)}</span>}
|
|
6196
|
-
{hasUsage && event.usage && (
|
|
6197
|
-
<span style={{ color: "var(--af-text-secondary)" }}>
|
|
6198
|
-
· {formatTokens(event.usage.input + event.usage.cacheRead + event.usage.cacheWrite)}/
|
|
6199
|
-
{formatTokens(event.usage.output)} tokens
|
|
6200
|
-
</span>
|
|
6201
|
-
)}
|
|
6202
|
-
<button
|
|
6203
|
-
onClick={() => setShowDev((s) => !s)}
|
|
6204
|
-
style={{
|
|
6205
|
-
marginLeft: "auto",
|
|
6206
|
-
background: "transparent",
|
|
6207
|
-
border: "none",
|
|
6208
|
-
fontSize: 10,
|
|
6209
|
-
color: "var(--af-text-tertiary)",
|
|
6210
|
-
cursor: "pointer",
|
|
6211
|
-
padding: 0,
|
|
6212
|
-
fontFamily: "inherit",
|
|
6213
|
-
textDecoration: "underline",
|
|
6214
|
-
textUnderlineOffset: 2,
|
|
6215
|
-
}}
|
|
6216
|
-
>
|
|
6217
|
-
{showDev ? "hide details" : "details"}
|
|
6218
|
-
</button>
|
|
6219
|
-
</div>
|
|
6220
|
-
|
|
6221
|
-
{/* Developer panel — collapsed by default */}
|
|
6222
|
-
{showDev && (
|
|
6223
|
-
<div
|
|
6224
|
-
style={{
|
|
6225
|
-
padding: "10px 20px 14px",
|
|
6226
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
6227
|
-
fontSize: 11,
|
|
6228
|
-
color: "var(--af-text-secondary)",
|
|
6229
|
-
fontFamily: "var(--font-mono)",
|
|
6230
|
-
display: "flex",
|
|
6231
|
-
flexDirection: "column",
|
|
6232
|
-
gap: 4,
|
|
6233
|
-
}}
|
|
6234
|
-
>
|
|
6235
|
-
{event.model && <div>model: {event.model}</div>}
|
|
6236
|
-
{event.requestId && <div>request: {event.requestId}</div>}
|
|
6237
|
-
{event.messageId && <div>message: {event.messageId}</div>}
|
|
6238
|
-
{event.stopReason && <div>stop_reason: {event.stopReason}</div>}
|
|
6239
|
-
{event.usage && (
|
|
6240
|
-
<>
|
|
6241
|
-
<div style={{ marginTop: 6, opacity: 0.7 }}>tokens</div>
|
|
6242
|
-
<div> input: {event.usage.input.toLocaleString()}</div>
|
|
6243
|
-
<div> output: {event.usage.output.toLocaleString()}</div>
|
|
6244
|
-
<div> cache read: {event.usage.cacheRead.toLocaleString()}</div>
|
|
6245
|
-
<div>
|
|
6246
|
-
{" "}cache write: {event.usage.cacheWrite.toLocaleString()}
|
|
6247
|
-
</div>
|
|
6248
|
-
</>
|
|
6249
|
-
)}
|
|
6250
|
-
</div>
|
|
6251
|
-
)}
|
|
6252
|
-
|
|
6253
|
-
{/* Content */}
|
|
6254
|
-
<div style={{ padding: "18px 22px" }}>
|
|
6255
|
-
<DrawerContent event={event} row={row} />
|
|
6256
|
-
</div>
|
|
6257
|
-
</div>
|
|
6258
|
-
);
|
|
6259
|
-
}
|
|
6260
|
-
|
|
6261
|
-
function drawerTitle(row: PresentationRow | null, event: SessionEvent): string {
|
|
6262
|
-
if (row) {
|
|
6263
|
-
switch (row.kind) {
|
|
6264
|
-
case "user":
|
|
6265
|
-
return "Message";
|
|
6266
|
-
case "agent":
|
|
6267
|
-
return "Message";
|
|
6268
|
-
case "tool-group":
|
|
6269
|
-
return `Tool use · ${formatToolSummary(row.toolNames)}`;
|
|
6270
|
-
case "interrupt":
|
|
6271
|
-
return "Interrupted";
|
|
6272
|
-
case "model":
|
|
6273
|
-
return "Model (zero-usage)";
|
|
6274
|
-
case "error":
|
|
6275
|
-
return "API error";
|
|
6276
|
-
case "task-notification":
|
|
6277
|
-
return `Background task · ${row.status}`;
|
|
6278
|
-
}
|
|
6279
|
-
}
|
|
6280
|
-
return event.rawType;
|
|
6281
|
-
}
|
|
6282
|
-
|
|
6283
|
-
function DrawerContent({ event, row }: { event: SessionEvent; row: PresentationRow | null }) {
|
|
6284
|
-
// User rows can override their blocks (e.g. slash commands get a cleaned
|
|
6285
|
-
// "/implement AGE-8" block instead of the raw XML).
|
|
6286
|
-
if (row?.kind === "user" && row.displayBlocks) {
|
|
6287
|
-
return (
|
|
6288
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
6289
|
-
{row.displayBlocks.map((b, i) => (
|
|
6290
|
-
<BlockView key={i} block={b} />
|
|
6291
|
-
))}
|
|
6292
|
-
</div>
|
|
6293
|
-
);
|
|
6294
|
-
}
|
|
6295
|
-
|
|
6296
|
-
// Task notifications: parsed fields in a clean key-value layout,
|
|
6297
|
-
// not the raw <task-notification> XML blob.
|
|
6298
|
-
if (row?.kind === "task-notification") {
|
|
6299
|
-
const statusColor =
|
|
6300
|
-
row.status === "success"
|
|
6301
|
-
? "var(--af-success)"
|
|
6302
|
-
: row.status === "failed"
|
|
6303
|
-
? "var(--af-danger)"
|
|
6304
|
-
: "var(--af-text-secondary)";
|
|
6305
|
-
return (
|
|
6306
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
6307
|
-
<div
|
|
6308
|
-
style={{
|
|
6309
|
-
display: "flex",
|
|
6310
|
-
alignItems: "center",
|
|
6311
|
-
gap: 8,
|
|
6312
|
-
}}
|
|
6313
|
-
>
|
|
6314
|
-
<span
|
|
6315
|
-
style={{
|
|
6316
|
-
fontSize: 11,
|
|
6317
|
-
fontWeight: 600,
|
|
6318
|
-
padding: "3px 10px",
|
|
6319
|
-
borderRadius: 4,
|
|
6320
|
-
background:
|
|
6321
|
-
row.status === "success"
|
|
6322
|
-
? "var(--af-success-subtle)"
|
|
6323
|
-
: row.status === "failed"
|
|
6324
|
-
? "var(--af-danger-subtle)"
|
|
6325
|
-
: "var(--af-border-subtle)",
|
|
6326
|
-
color: statusColor,
|
|
6327
|
-
textTransform: "uppercase",
|
|
6328
|
-
letterSpacing: "0.03em",
|
|
6329
|
-
}}
|
|
6330
|
-
>
|
|
6331
|
-
{row.status}
|
|
6332
|
-
</span>
|
|
6333
|
-
</div>
|
|
6334
|
-
<div style={{ fontSize: 13, lineHeight: 1.55, color: "var(--af-text)" }}>{row.summary}</div>
|
|
6335
|
-
{(row.taskId || row.toolUseId || row.outputFile) && (
|
|
6336
|
-
<div
|
|
6337
|
-
style={{
|
|
6338
|
-
fontSize: 11,
|
|
6339
|
-
color: "var(--af-text-secondary)",
|
|
6340
|
-
fontFamily: "var(--font-mono)",
|
|
6341
|
-
display: "flex",
|
|
6342
|
-
flexDirection: "column",
|
|
6343
|
-
gap: 4,
|
|
6344
|
-
paddingTop: 10,
|
|
6345
|
-
borderTop: "1px solid var(--af-border-subtle)",
|
|
6346
|
-
}}
|
|
6347
|
-
>
|
|
6348
|
-
{row.taskId && <div>task id: {row.taskId}</div>}
|
|
6349
|
-
{row.toolUseId && <div>tool use: {row.toolUseId.slice(0, 24)}…</div>}
|
|
6350
|
-
{row.outputFile && (
|
|
6351
|
-
<div style={{ wordBreak: "break-all" }}>output: {row.outputFile}</div>
|
|
6352
|
-
)}
|
|
6353
|
-
</div>
|
|
6354
|
-
)}
|
|
6355
|
-
</div>
|
|
6356
|
-
);
|
|
6357
|
-
}
|
|
6358
|
-
|
|
6359
|
-
// Tool group: list all individual tool calls with their inputs
|
|
6360
|
-
if (row?.kind === "tool-group" && row.count > 1) {
|
|
6361
|
-
return (
|
|
6362
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
6363
|
-
{row.events.map((e, i) => (
|
|
6364
|
-
<div key={e.index}>
|
|
6365
|
-
<div
|
|
6366
|
-
style={{
|
|
6367
|
-
fontSize: 11,
|
|
6368
|
-
color: "var(--af-text-tertiary)",
|
|
6369
|
-
marginBottom: 4,
|
|
6370
|
-
fontFamily: "var(--font-mono)",
|
|
6371
|
-
}}
|
|
6372
|
-
>
|
|
6373
|
-
#{i + 1} · {formatOffset(e.tOffsetMs)} · {e.toolUseId?.slice(0, 14)}…
|
|
6374
|
-
</div>
|
|
6375
|
-
{e.blocks.map((b, bi) => (
|
|
6376
|
-
<BlockView key={bi} block={b} />
|
|
6377
|
-
))}
|
|
6378
|
-
</div>
|
|
6379
|
-
))}
|
|
6380
|
-
</div>
|
|
6381
|
-
);
|
|
6382
|
-
}
|
|
6383
|
-
|
|
6384
|
-
// Agent row: include thinking blocks from the same message.id if present
|
|
6385
|
-
if (row?.kind === "agent" && row.groupedEvents.length > 1) {
|
|
6386
|
-
return (
|
|
6387
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
6388
|
-
{row.groupedEvents.flatMap((e, i) =>
|
|
6389
|
-
e.blocks.map((b, bi) => <BlockView key={`${i}-${bi}`} block={b} />),
|
|
6390
|
-
)}
|
|
6391
|
-
</div>
|
|
6392
|
-
);
|
|
6393
|
-
}
|
|
6394
|
-
|
|
6395
|
-
return (
|
|
6396
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
6397
|
-
{event.blocks.map((b, i) => (
|
|
6398
|
-
<BlockView key={i} block={b} />
|
|
6399
|
-
))}
|
|
6400
|
-
</div>
|
|
6401
|
-
);
|
|
6402
|
-
}
|
|
6403
|
-
function BlockView({ block }: { block: ContentBlock }) {
|
|
6404
|
-
if (block.type === "text") {
|
|
6405
|
-
return (
|
|
6406
|
-
<div className="sl-prose">
|
|
6407
|
-
<ReactMarkdown
|
|
6408
|
-
remarkPlugins={[remarkGfm]}
|
|
6409
|
-
components={{
|
|
6410
|
-
a: (props) => (
|
|
6411
|
-
<a
|
|
6412
|
-
{...props}
|
|
6413
|
-
target="_blank"
|
|
6414
|
-
rel="noopener noreferrer"
|
|
6415
|
-
style={{
|
|
6416
|
-
color: "var(--af-accent)",
|
|
6417
|
-
textDecoration: "underline",
|
|
6418
|
-
textUnderlineOffset: 2,
|
|
6419
|
-
}}
|
|
6420
|
-
/>
|
|
6421
|
-
),
|
|
6422
|
-
}}
|
|
6423
|
-
>
|
|
6424
|
-
{block.text}
|
|
6425
|
-
</ReactMarkdown>
|
|
6426
|
-
</div>
|
|
6427
|
-
);
|
|
6428
|
-
}
|
|
6429
|
-
if (block.type === "thinking") {
|
|
6430
|
-
return (
|
|
6431
|
-
<details>
|
|
6432
|
-
<summary
|
|
6433
|
-
style={{
|
|
6434
|
-
cursor: "pointer",
|
|
6435
|
-
fontSize: 11,
|
|
6436
|
-
color: "var(--af-text-tertiary)",
|
|
6437
|
-
marginBottom: 6,
|
|
6438
|
-
}}
|
|
6439
|
-
>
|
|
6440
|
-
Thinking · {block.thinking.length} chars
|
|
6441
|
-
</summary>
|
|
6442
|
-
<div
|
|
6443
|
-
style={{
|
|
6444
|
-
fontSize: 12,
|
|
6445
|
-
lineHeight: 1.6,
|
|
6446
|
-
color: "var(--af-text-secondary)",
|
|
6447
|
-
whiteSpace: "pre-wrap",
|
|
6448
|
-
fontStyle: "italic",
|
|
6449
|
-
borderLeft: "3px solid var(--af-border-subtle)",
|
|
6450
|
-
paddingLeft: 12,
|
|
6451
|
-
marginTop: 6,
|
|
6452
|
-
}}
|
|
6453
|
-
>
|
|
6454
|
-
{block.thinking}
|
|
6455
|
-
</div>
|
|
6456
|
-
</details>
|
|
6457
|
-
);
|
|
6458
|
-
}
|
|
6459
|
-
if (block.type === "tool_use") {
|
|
6460
|
-
return <ToolUseCard name={block.name} input={block.input} />;
|
|
6461
|
-
}
|
|
6462
|
-
if (block.type === "tool_result") {
|
|
6463
|
-
const text =
|
|
6464
|
-
typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
|
|
6465
|
-
return (
|
|
6466
|
-
<pre
|
|
6467
|
-
style={{
|
|
6468
|
-
fontSize: 12,
|
|
6469
|
-
padding: 12,
|
|
6470
|
-
background: "var(--background)",
|
|
6471
|
-
border: "1px solid var(--af-border-subtle)",
|
|
6472
|
-
borderRadius: 6,
|
|
6473
|
-
overflow: "auto",
|
|
6474
|
-
whiteSpace: "pre-wrap",
|
|
6475
|
-
color: "var(--af-text)",
|
|
6476
|
-
fontFamily: "var(--font-mono)",
|
|
6477
|
-
maxHeight: 500,
|
|
6478
|
-
}}
|
|
6479
|
-
>
|
|
6480
|
-
{text}
|
|
6481
|
-
</pre>
|
|
6482
|
-
);
|
|
6483
|
-
}
|
|
6484
|
-
return null;
|
|
6485
|
-
}
|
|
5413
|
+
// DrawerContent, BlockView moved to ./session-view/drawer.tsx
|
|
6486
5414
|
|
|
6487
5415
|
/* ------------------------------------------------------------------ */
|
|
6488
5416
|
/* Helpers */
|
|
6489
5417
|
/* ------------------------------------------------------------------ */
|
|
6490
5418
|
|
|
6491
|
-
|
|
6492
|
-
if (ms < 1000) return `${ms}ms`;
|
|
6493
|
-
const s = Math.round(ms / 1000);
|
|
6494
|
-
if (s < 60) return `${s}s`;
|
|
6495
|
-
const m = Math.floor(s / 60);
|
|
6496
|
-
if (m < 60) return `${m}m ${s % 60}s`;
|
|
6497
|
-
// Roll up to hours — agent time can reach many hours once workflow
|
|
6498
|
-
// execution is folded in, and "1116m" reads poorly.
|
|
6499
|
-
const h = Math.floor(m / 60);
|
|
6500
|
-
return `${h}h ${m % 60}m`;
|
|
6501
|
-
}
|
|
6502
|
-
|
|
6503
|
-
function EntryDayStrip({ entry }: { entry: Entry }) {
|
|
6504
|
-
const fmtDate = new Date(`${entry.local_day}T12:00:00`).toLocaleDateString("en-US", {
|
|
6505
|
-
weekday: "short", month: "short", day: "numeric",
|
|
6506
|
-
});
|
|
6507
|
-
const durMin = Math.round(entry.numbers.active_min);
|
|
6508
|
-
const enr = entry.enrichment;
|
|
6509
|
-
const isPending = enr.status === "pending" || enr.status === "error";
|
|
6510
|
-
const isTrivial = enr.status === "skipped_trivial";
|
|
5419
|
+
// formatDurationHeader moved to ./session-view/helpers.ts
|
|
6511
5420
|
|
|
6512
|
-
|
|
6513
|
-
<div
|
|
6514
|
-
style={{
|
|
6515
|
-
border: "1px solid var(--af-border-subtle)",
|
|
6516
|
-
borderRadius: 10,
|
|
6517
|
-
padding: "12px 16px",
|
|
6518
|
-
background: "var(--af-surface)",
|
|
6519
|
-
fontSize: 13,
|
|
6520
|
-
lineHeight: 1.5,
|
|
6521
|
-
}}
|
|
6522
|
-
>
|
|
6523
|
-
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
|
|
6524
|
-
<span
|
|
6525
|
-
style={{
|
|
6526
|
-
fontSize: 12,
|
|
6527
|
-
color: "var(--af-text-tertiary)",
|
|
6528
|
-
display: "inline-flex",
|
|
6529
|
-
alignItems: "center",
|
|
6530
|
-
gap: 4,
|
|
6531
|
-
}}
|
|
6532
|
-
>
|
|
6533
|
-
📅 {fmtDate}
|
|
6534
|
-
</span>
|
|
6535
|
-
{isTrivial ? (
|
|
6536
|
-
<OutcomePill outcome="trivial" size="md" agent={entry.agent} />
|
|
6537
|
-
) : isPending ? (
|
|
6538
|
-
<OutcomePill
|
|
6539
|
-
outcome={null}
|
|
6540
|
-
pending
|
|
6541
|
-
sessionId={entry.session_id}
|
|
6542
|
-
localDay={entry.local_day}
|
|
6543
|
-
size="md"
|
|
6544
|
-
agent={entry.agent}
|
|
6545
|
-
/>
|
|
6546
|
-
) : enr.outcome ? (
|
|
6547
|
-
<OutcomePill outcome={enr.outcome} size="md" agent={entry.agent} />
|
|
6548
|
-
) : null}
|
|
6549
|
-
<span
|
|
6550
|
-
style={{
|
|
6551
|
-
fontFamily: "var(--font-mono)",
|
|
6552
|
-
fontSize: 11,
|
|
6553
|
-
color: "var(--af-text-tertiary)",
|
|
6554
|
-
marginLeft: "auto",
|
|
6555
|
-
}}
|
|
6556
|
-
>
|
|
6557
|
-
{durMin}m active
|
|
6558
|
-
</span>
|
|
6559
|
-
</div>
|
|
6560
|
-
|
|
6561
|
-
{isTrivial && (
|
|
6562
|
-
<div style={{ fontSize: 12, color: "var(--af-text-tertiary)", fontStyle: "italic", marginTop: 8 }}>
|
|
6563
|
-
Warmup — no enrichment generated.
|
|
6564
|
-
</div>
|
|
6565
|
-
)}
|
|
6566
|
-
|
|
6567
|
-
{isPending && (
|
|
6568
|
-
<div style={{ fontSize: 12, color: "var(--af-text-secondary)", marginTop: 8 }}>
|
|
6569
|
-
Click the pending pill to generate this day's digest →
|
|
6570
|
-
</div>
|
|
6571
|
-
)}
|
|
6572
|
-
|
|
6573
|
-
{enr.status === "done" && (
|
|
6574
|
-
<>
|
|
6575
|
-
{enr.brief_summary && (
|
|
6576
|
-
<p style={{ margin: "10px 0 0", color: "var(--af-text)" }}>{enr.brief_summary}</p>
|
|
6577
|
-
)}
|
|
6578
|
-
{(enr.friction_detail || enr.user_instructions.length > 0) && (
|
|
6579
|
-
<div style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 6 }}>
|
|
6580
|
-
{enr.friction_detail && (
|
|
6581
|
-
<div style={{ fontSize: 12, color: "var(--af-text-secondary)" }}>
|
|
6582
|
-
<span style={{ color: "#ed8936" }}>⚠ </span>
|
|
6583
|
-
{enr.friction_detail}
|
|
6584
|
-
</div>
|
|
6585
|
-
)}
|
|
6586
|
-
{enr.user_instructions.length > 0 && (
|
|
6587
|
-
<details style={{ fontSize: 12, color: "var(--af-text-secondary)" }}>
|
|
6588
|
-
<summary style={{ cursor: "pointer", color: "var(--af-text-tertiary)" }}>
|
|
6589
|
-
Top user instructions ({enr.user_instructions.length})
|
|
6590
|
-
</summary>
|
|
6591
|
-
<ul style={{ margin: "6px 0 0", paddingLeft: 18 }}>
|
|
6592
|
-
{enr.user_instructions.slice(0, 5).map((u, i) => (
|
|
6593
|
-
<li key={i} style={{ marginBottom: 2 }}>{u}</li>
|
|
6594
|
-
))}
|
|
6595
|
-
</ul>
|
|
6596
|
-
</details>
|
|
6597
|
-
)}
|
|
6598
|
-
</div>
|
|
6599
|
-
)}
|
|
6600
|
-
<div style={{ marginTop: 10, fontSize: 11 }}>
|
|
6601
|
-
<Link
|
|
6602
|
-
href={`/digest/${entry.local_day}`}
|
|
6603
|
-
style={{ color: "var(--af-accent)", textDecoration: "none" }}
|
|
6604
|
-
>
|
|
6605
|
-
Open day digest →
|
|
6606
|
-
</Link>
|
|
6607
|
-
</div>
|
|
6608
|
-
</>
|
|
6609
|
-
)}
|
|
6610
|
-
</div>
|
|
6611
|
-
);
|
|
6612
|
-
}
|
|
5421
|
+
// EntryDayStrip moved to ./session-view/entry-day-strip.tsx
|