fleetlens 0.12.3 → 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 +2 -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/[externals]__0rv9ct5._.js +3 -0
- 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/node_modules_next_dist_esm_build_templates_app-route_02bw8.n.js +5 -0
- package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0-1_njm._.js → [root-of-the-server]__0k1x-c9._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o03ymy._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/apps_web_0.mu7c7._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_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_0b-4-4j._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0c-tpft._.js +1 -1
- 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_components_0txtsv5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
- package/app/apps/web/.next/server/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/02bv_ejzhhzvp.js +10 -0
- package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
- package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
- package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
- package/app/apps/web/.next/static/chunks/{12jvvc_w.cb45.css → 083~hzkv577.5.css} +1 -1
- package/app/apps/web/.next/static/chunks/{0osdgl2.9cl1q.js → 0d2x-wfu.m0-z.js} +1 -1
- package/app/apps/web/.next/static/chunks/{0lziv8y_6yp5w.js → 0e_fzy0n8ij-o.js} +1 -1
- 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/{13po66tt.68f3.js → 0wwcx42ng8i2e.js} +3 -3
- 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/api/events/route.ts +47 -4
- package/app/apps/web/app/globals.css +23 -0
- package/app/apps/web/app/insights/page.tsx +20 -5
- package/app/apps/web/app/layout.tsx +1 -0
- package/app/apps/web/app/page.tsx +1 -1
- 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/projects/[slug]/page.tsx +1 -1
- 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 +701 -1577
- 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 +43 -19
- 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/live-badge.tsx +11 -3
- package/app/apps/web/components/live-sessions-widget.tsx +9 -3
- package/app/apps/web/components/month-digest-view.tsx +19 -64
- package/app/apps/web/components/outcome-pill.tsx +24 -4
- 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 +83 -45
- package/dist/index.js +452 -198
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +0 -5
- 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/00xkqnvk4-d1-.js +0 -10
- package/app/apps/web/.next/static/chunks/0htgc_hr1bw67.js +0 -1
- 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/0vf-3rauafbek.js +0 -6
- package/app/apps/web/.next/static/chunks/0vl27-vjob2_9.js +0 -6
- /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_ssgManifest.js +0 -0
|
@@ -5,6 +5,7 @@ import React, { useEffect, useMemo, useRef, useState, type CSSProperties } from
|
|
|
5
5
|
import {
|
|
6
6
|
ArrowLeft,
|
|
7
7
|
ChevronDown,
|
|
8
|
+
ChevronLeft,
|
|
8
9
|
ChevronRight,
|
|
9
10
|
ChevronUp,
|
|
10
11
|
Clock,
|
|
@@ -19,7 +20,6 @@ import {
|
|
|
19
20
|
import ReactMarkdown from "react-markdown";
|
|
20
21
|
import remarkGfm from "remark-gfm";
|
|
21
22
|
import type {
|
|
22
|
-
ContentBlock,
|
|
23
23
|
SessionDetail,
|
|
24
24
|
SessionEvent,
|
|
25
25
|
SubagentRun,
|
|
@@ -32,13 +32,44 @@ import {
|
|
|
32
32
|
buildPresentation,
|
|
33
33
|
buildMegaRows,
|
|
34
34
|
detectPrMarkers,
|
|
35
|
-
rowPrimaryIndex as rowPrimaryIndexFromLib,
|
|
36
35
|
type MegaRow,
|
|
37
36
|
type PresentationRow,
|
|
38
37
|
type PresentationRowKind,
|
|
39
38
|
type TurnMegaRow,
|
|
40
39
|
type TurnSummary,
|
|
41
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";
|
|
42
73
|
import { estimateCost, formatCost, formatGap, formatOffset, formatRelative, formatTokens, shortId } from "@/lib/format";
|
|
43
74
|
import { LiveBadge } from "@/components/live-badge";
|
|
44
75
|
import { AskButton, AskDrawer } from "@/components/ask";
|
|
@@ -169,7 +200,20 @@ export function SessionView({
|
|
|
169
200
|
/** When a timeline click sets the index we also want to scroll. Track that
|
|
170
201
|
* intent separately so selection via row-click doesn't auto-scroll. */
|
|
171
202
|
const [scrollIntent, setScrollIntent] = useState(0);
|
|
203
|
+
/** Scroll-only target (no selection) — used by day navigation so jumping to
|
|
204
|
+
* a day scrolls the transcript there WITHOUT popping the event drawer the
|
|
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);
|
|
172
213
|
const rowRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
|
214
|
+
// Per-day digest cards (EntryDayStrip) are interleaved into the transcript at
|
|
215
|
+
// each day's first row, so day-nav scrolls to the card itself when present.
|
|
216
|
+
const dayStripRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
173
217
|
|
|
174
218
|
// The event/subagent drawers are transcript-scoped (position: fixed, shown
|
|
175
219
|
// whenever a selection exists). Clear the selection when leaving the
|
|
@@ -192,6 +236,12 @@ export function SessionView({
|
|
|
192
236
|
* at mount + on resize via ResizeObserver — no magic constants. */
|
|
193
237
|
const headerRef = useRef<HTMLDivElement>(null);
|
|
194
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;
|
|
195
245
|
useEffect(() => {
|
|
196
246
|
const el = headerRef.current;
|
|
197
247
|
if (!el) return;
|
|
@@ -212,6 +262,10 @@ export function SessionView({
|
|
|
212
262
|
// When the user explicitly clicks the toggle, suppress auto-collapse
|
|
213
263
|
// for a short period so the scroll listener doesn't immediately undo it.
|
|
214
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);
|
|
215
269
|
const toggleCollapsed = () => {
|
|
216
270
|
setCollapsed((v) => !v);
|
|
217
271
|
manualPinRef.current = Date.now();
|
|
@@ -289,6 +343,51 @@ export function SessionView({
|
|
|
289
343
|
el.scrollIntoView({ block: "start", behavior: "auto" });
|
|
290
344
|
}, [scrollIntent, selectedIndex]);
|
|
291
345
|
|
|
346
|
+
// Scroll-only effect for day navigation — scrolls to a row without setting
|
|
347
|
+
// selectedIndex (so no drawer opens). Same scrollIntoView contract as above.
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (!dayScrollTarget) return;
|
|
350
|
+
const main = headerRef.current?.closest("main") as HTMLElement | null;
|
|
351
|
+
// Prefer the day's digest card (it leads the day); fall back to the first
|
|
352
|
+
// row when that day has no entry.
|
|
353
|
+
const el =
|
|
354
|
+
dayStripRefs.current[dayScrollTarget.key] ?? rowRefs.current[dayScrollTarget.index];
|
|
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);
|
|
389
|
+
}, [dayScrollTarget]);
|
|
390
|
+
|
|
292
391
|
const { events, durationMs, totalUsage, model, eventCount, projectName } = session;
|
|
293
392
|
const airTimeMs = session.airTimeMs ?? durationMs;
|
|
294
393
|
const workflowCount = session.workflows?.length ?? 0;
|
|
@@ -310,11 +409,16 @@ export function SessionView({
|
|
|
310
409
|
[events, teammateCount],
|
|
311
410
|
);
|
|
312
411
|
|
|
313
|
-
// A session is "live" if its last
|
|
412
|
+
// A session is "live" if its last activity was within 45 seconds — where
|
|
413
|
+
// "activity" includes nested background-agent/workflow writes, not just the
|
|
414
|
+
// main transcript's last event (lastActivityMs).
|
|
314
415
|
const isSessionLive = (() => {
|
|
315
|
-
|
|
316
|
-
const ms =
|
|
317
|
-
|
|
416
|
+
const isoMs = session.lastTimestamp ? Date.parse(session.lastTimestamp) : NaN;
|
|
417
|
+
const ms = Math.max(
|
|
418
|
+
Number.isNaN(isoMs) ? 0 : isoMs,
|
|
419
|
+
session.lastActivityMs && Number.isFinite(session.lastActivityMs) ? session.lastActivityMs : 0,
|
|
420
|
+
);
|
|
421
|
+
return ms > 0 && Date.now() - ms < 45_000;
|
|
318
422
|
})();
|
|
319
423
|
|
|
320
424
|
/** Detect PR creations in this session. */
|
|
@@ -488,6 +592,135 @@ export function SessionView({
|
|
|
488
592
|
return out;
|
|
489
593
|
}, [events]);
|
|
490
594
|
|
|
595
|
+
/** Local-day buckets the session spans, in event order. Each day's
|
|
596
|
+
* `startMs`/`endMs` are tOffsetMs bounds (offset from session start) so
|
|
597
|
+
* they plug straight into the minimap's offset coordinate space. Keys are
|
|
598
|
+
* derived as literal `YYYY-MM-DD` from local date parts so SSR and client
|
|
599
|
+
* agree (no locale/Date formatting that could drift across the hydration
|
|
600
|
+
* boundary). Long multi-day sessions use this to scope the minimap to one
|
|
601
|
+
* day at a time instead of squashing days into a single bar. */
|
|
602
|
+
const sessionDays = useMemo(() => {
|
|
603
|
+
const byDay = new Map<string, { startMs: number; endMs: number; count: number }>();
|
|
604
|
+
for (const e of events) {
|
|
605
|
+
if (e.tOffsetMs === undefined || !e.timestamp) continue;
|
|
606
|
+
const ms = Date.parse(e.timestamp);
|
|
607
|
+
if (!Number.isFinite(ms)) continue;
|
|
608
|
+
const d = new Date(ms);
|
|
609
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
610
|
+
const cur = byDay.get(key);
|
|
611
|
+
if (!cur) byDay.set(key, { startMs: e.tOffsetMs, endMs: e.tOffsetMs, count: 1 });
|
|
612
|
+
else {
|
|
613
|
+
cur.startMs = Math.min(cur.startMs, e.tOffsetMs);
|
|
614
|
+
cur.endMs = Math.max(cur.endMs, e.tOffsetMs);
|
|
615
|
+
cur.count += 1;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return [...byDay.entries()]
|
|
619
|
+
.map(([key, v]) => ({ key, ...v }))
|
|
620
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
621
|
+
}, [events]);
|
|
622
|
+
|
|
623
|
+
/** local_day → per-day digest card. `entry.local_day` and `sessionDays` keys
|
|
624
|
+
* are both built from local-time getFullYear/Month/Date (see parser's
|
|
625
|
+
* toLocalDay), so the strings match exactly and the card lands on the right
|
|
626
|
+
* day's first transcript row. */
|
|
627
|
+
const entryByDay = useMemo(() => {
|
|
628
|
+
const m = new Map<string, Entry>();
|
|
629
|
+
for (const e of entries) m.set(e.local_day, e);
|
|
630
|
+
return m;
|
|
631
|
+
}, [entries]);
|
|
632
|
+
|
|
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);
|
|
642
|
+
const [selectedDayKey, setSelectedDayKey] = useState<string>(
|
|
643
|
+
() => (sessionDays.length ? sessionDays[sessionDays.length - 1]!.key : "all"),
|
|
644
|
+
);
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
setSelectedDayKey(sessionDays.length ? sessionDays[sessionDays.length - 1]!.key : "all");
|
|
647
|
+
didInitialDayJump.current = false;
|
|
648
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
649
|
+
}, [session.sessionId]);
|
|
650
|
+
|
|
651
|
+
const dayIndex = sessionDays.findIndex((d) => d.key === selectedDayKey);
|
|
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.
|
|
658
|
+
const selectedDayKeyRef = useRef(selectedDayKey);
|
|
659
|
+
selectedDayKeyRef.current = selectedDayKey;
|
|
660
|
+
|
|
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.
|
|
676
|
+
useEffect(() => {
|
|
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
|
|
686
|
+
}, [tab, sessionDays]);
|
|
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
|
+
|
|
715
|
+
/** Offset window for the selected day, or null when there's a single day
|
|
716
|
+
* or "all" is chosen — null tells the minimap to render the full session
|
|
717
|
+
* exactly as before (no regression for single-day sessions). */
|
|
718
|
+
const dayWindow = useMemo(() => {
|
|
719
|
+
if (sessionDays.length <= 1 || selectedDayKey === "all") return null;
|
|
720
|
+
const d = sessionDays.find((x) => x.key === selectedDayKey);
|
|
721
|
+
return d ? { startMs: d.startMs, endMs: d.endMs } : null;
|
|
722
|
+
}, [sessionDays, selectedDayKey]);
|
|
723
|
+
|
|
491
724
|
/** Build the full presentation stream once. */
|
|
492
725
|
const allRows = useMemo(() => buildPresentation(visibleEvents), [visibleEvents]);
|
|
493
726
|
|
|
@@ -568,6 +801,67 @@ export function SessionView({
|
|
|
568
801
|
setScrollIntent((n) => n + 1);
|
|
569
802
|
}
|
|
570
803
|
|
|
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). */
|
|
808
|
+
function gotoDay(key: string) {
|
|
809
|
+
setSelectedDayKey(key);
|
|
810
|
+
tailNavLockRef.current = Date.now();
|
|
811
|
+
// Paging days scrolls the transcript; that programmatic scroll (often
|
|
812
|
+
// upward to an earlier day's first row) would otherwise read as a
|
|
813
|
+
// user scroll-up and pop the sticky header open. Force it collapsed and
|
|
814
|
+
// pin so the scroll listener leaves it alone — day-nav always stays
|
|
815
|
+
// collapsed.
|
|
816
|
+
setCollapsed(true);
|
|
817
|
+
manualPinRef.current = Date.now();
|
|
818
|
+
if (key === "all") return;
|
|
819
|
+
const d = sessionDays.find((x) => x.key === key);
|
|
820
|
+
if (!d) return;
|
|
821
|
+
const ev = events.find((e) => e.tOffsetMs !== undefined && e.tOffsetMs >= d.startMs);
|
|
822
|
+
if (!ev) return;
|
|
823
|
+
// In turns mode the day's first row may be inside a collapsed turn — expand
|
|
824
|
+
// it so the row exists in the DOM before the scroll effect runs.
|
|
825
|
+
if (filter === "turns") {
|
|
826
|
+
const turnIdx = turnByRowIndex.get(ev.index);
|
|
827
|
+
if (turnIdx !== undefined && !expandedTurns.has(turnIdx)) {
|
|
828
|
+
setExpandedTurns((prev) => new Set(prev).add(turnIdx));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
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
|
+
}));
|
|
863
|
+
}
|
|
864
|
+
|
|
571
865
|
/** Scroll the transcript to the row whose assistant message issued a given
|
|
572
866
|
* tool_use id. Used by workflow cards / lanes to jump to the originating
|
|
573
867
|
* Workflow tool call. */
|
|
@@ -657,7 +951,7 @@ export function SessionView({
|
|
|
657
951
|
className={`af-tab-btn ${tab === "transcript" ? "active" : ""}`}
|
|
658
952
|
onClick={() => setTab("transcript")}
|
|
659
953
|
>
|
|
660
|
-
|
|
954
|
+
Timeline
|
|
661
955
|
</button>
|
|
662
956
|
{hasWorkflows && (
|
|
663
957
|
<button
|
|
@@ -693,7 +987,7 @@ export function SessionView({
|
|
|
693
987
|
className={`af-tab-btn ${tab === "debug" ? "active" : ""}`}
|
|
694
988
|
onClick={() => setTab("debug")}
|
|
695
989
|
>
|
|
696
|
-
|
|
990
|
+
Log
|
|
697
991
|
</button>
|
|
698
992
|
</div>
|
|
699
993
|
<button
|
|
@@ -754,7 +1048,7 @@ export function SessionView({
|
|
|
754
1048
|
>
|
|
755
1049
|
sesn_{session.id.replace(/-/g, "").slice(0, 22)}
|
|
756
1050
|
</h1>
|
|
757
|
-
<LiveBadge mtimeIso={session.lastTimestamp} size="md" />
|
|
1051
|
+
<LiveBadge mtimeIso={session.lastTimestamp} activityMs={session.lastActivityMs} size="md" />
|
|
758
1052
|
<span
|
|
759
1053
|
style={{
|
|
760
1054
|
fontSize: 10.5,
|
|
@@ -767,7 +1061,7 @@ export function SessionView({
|
|
|
767
1061
|
top: -1,
|
|
768
1062
|
}}
|
|
769
1063
|
>
|
|
770
|
-
{
|
|
1064
|
+
{isSessionLive ? "Running" : "Idle"}
|
|
771
1065
|
</span>
|
|
772
1066
|
<InlineStatDivider />
|
|
773
1067
|
{model && <InlineStat icon={<Cpu size={12} />} value={model} mono />}
|
|
@@ -921,6 +1215,63 @@ export function SessionView({
|
|
|
921
1215
|
transition: "padding-bottom 0.2s ease",
|
|
922
1216
|
}}
|
|
923
1217
|
>
|
|
1218
|
+
{/* Day navigator — only for multi-day sessions on the non-team
|
|
1219
|
+
minimap. Lets you step the timeline through each local day (default
|
|
1220
|
+
= the most recent) so a long-running session reads one day at a
|
|
1221
|
+
time instead of squashed into one bar. */}
|
|
1222
|
+
{tab !== "team" && sessionDays.length > 1 && (
|
|
1223
|
+
<div
|
|
1224
|
+
style={{
|
|
1225
|
+
display: "flex",
|
|
1226
|
+
alignItems: "center",
|
|
1227
|
+
gap: 8,
|
|
1228
|
+
marginBottom: 6,
|
|
1229
|
+
fontSize: 12,
|
|
1230
|
+
color: "var(--af-text-secondary)",
|
|
1231
|
+
}}
|
|
1232
|
+
>
|
|
1233
|
+
<span style={{ color: "var(--af-text-tertiary)" }}>Timeline day</span>
|
|
1234
|
+
<button
|
|
1235
|
+
type="button"
|
|
1236
|
+
title="Previous day"
|
|
1237
|
+
aria-label="Previous day"
|
|
1238
|
+
onClick={() => dayIndex > 0 && gotoDay(sessionDays[dayIndex - 1]!.key)}
|
|
1239
|
+
disabled={selectedDayKey === "all" || dayIndex <= 0}
|
|
1240
|
+
className="sl-day-nav-btn"
|
|
1241
|
+
>
|
|
1242
|
+
<ChevronLeft size={13} />
|
|
1243
|
+
</button>
|
|
1244
|
+
<select
|
|
1245
|
+
value={selectedDayKey}
|
|
1246
|
+
onChange={(e) => gotoDay(e.target.value)}
|
|
1247
|
+
className="sl-compact-select"
|
|
1248
|
+
>
|
|
1249
|
+
{sessionDays.map((d, i) => (
|
|
1250
|
+
<option key={d.key} value={d.key}>
|
|
1251
|
+
{formatDayKey(d.key)} · {d.count} event{d.count === 1 ? "" : "s"} (day {i + 1}/{sessionDays.length})
|
|
1252
|
+
</option>
|
|
1253
|
+
))}
|
|
1254
|
+
<option value="all">All days ({sessionDays.length})</option>
|
|
1255
|
+
</select>
|
|
1256
|
+
<button
|
|
1257
|
+
type="button"
|
|
1258
|
+
title="Next day"
|
|
1259
|
+
aria-label="Next day"
|
|
1260
|
+
onClick={() =>
|
|
1261
|
+
dayIndex >= 0 && dayIndex < sessionDays.length - 1 && gotoDay(sessionDays[dayIndex + 1]!.key)
|
|
1262
|
+
}
|
|
1263
|
+
disabled={selectedDayKey === "all" || dayIndex < 0 || dayIndex >= sessionDays.length - 1}
|
|
1264
|
+
className="sl-day-nav-btn"
|
|
1265
|
+
>
|
|
1266
|
+
<ChevronRight size={13} />
|
|
1267
|
+
</button>
|
|
1268
|
+
<span style={{ color: "var(--af-text-tertiary)", marginLeft: 2 }}>
|
|
1269
|
+
{selectedDayKey === "all"
|
|
1270
|
+
? `showing all ${sessionDays.length} days`
|
|
1271
|
+
: `day ${dayIndex + 1} of ${sessionDays.length}`}
|
|
1272
|
+
</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
)}
|
|
924
1275
|
{tab === "team" && team ? (
|
|
925
1276
|
<TeamMinimap
|
|
926
1277
|
data={team}
|
|
@@ -937,6 +1288,7 @@ export function SessionView({
|
|
|
937
1288
|
<Minimap
|
|
938
1289
|
displayRows={displayRows}
|
|
939
1290
|
durationMs={durationMs ?? 0}
|
|
1291
|
+
dayWindow={dayWindow}
|
|
940
1292
|
selectedIndex={selectedIndex}
|
|
941
1293
|
onSelect={scrollToIndex}
|
|
942
1294
|
headerOffset={headerH}
|
|
@@ -958,6 +1310,7 @@ export function SessionView({
|
|
|
958
1310
|
// for the same right-side real estate.
|
|
959
1311
|
if (id) setSelectedIndex(null);
|
|
960
1312
|
}}
|
|
1313
|
+
onPlayheadChange={followDayToScroll}
|
|
961
1314
|
/>
|
|
962
1315
|
)}
|
|
963
1316
|
</div>
|
|
@@ -1002,17 +1355,13 @@ export function SessionView({
|
|
|
1002
1355
|
)
|
|
1003
1356
|
}
|
|
1004
1357
|
onJumpToParent={jumpToToolUse}
|
|
1358
|
+
onClearFocus={() => setSelectedWorkflowId(null)}
|
|
1005
1359
|
/>
|
|
1006
1360
|
</div>
|
|
1007
1361
|
) : tab === "debug" ? (
|
|
1008
1362
|
<DebugList events={events} />
|
|
1009
1363
|
) : (
|
|
1010
1364
|
<>
|
|
1011
|
-
{entries.length > 0 && (
|
|
1012
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 14 }}>
|
|
1013
|
-
{entries.map((e) => <EntryDayStrip key={e.local_day} entry={e} />)}
|
|
1014
|
-
</div>
|
|
1015
|
-
)}
|
|
1016
1365
|
{teammateCount > 0 && (
|
|
1017
1366
|
<div
|
|
1018
1367
|
style={{
|
|
@@ -1055,6 +1404,10 @@ export function SessionView({
|
|
|
1055
1404
|
team={team}
|
|
1056
1405
|
model={model}
|
|
1057
1406
|
rawIdleBands={rawIdleBands}
|
|
1407
|
+
sessionDays={sessionDays}
|
|
1408
|
+
entryByDay={entryByDay}
|
|
1409
|
+
dayStripRefs={dayStripRefs}
|
|
1410
|
+
onJumpAcrossDay={jumpAcrossDay}
|
|
1058
1411
|
/>
|
|
1059
1412
|
</>
|
|
1060
1413
|
)}
|
|
@@ -1185,7 +1538,7 @@ export function SessionView({
|
|
|
1185
1538
|
)}
|
|
1186
1539
|
|
|
1187
1540
|
{/* Tail mode FAB — auto-scroll to follow live events */}
|
|
1188
|
-
<TailMode isLive={isSessionLive} />
|
|
1541
|
+
<TailMode isLive={isSessionLive} navLockRef={tailNavLockRef} />
|
|
1189
1542
|
</div>
|
|
1190
1543
|
);
|
|
1191
1544
|
}
|
|
@@ -1194,353 +1547,19 @@ export function SessionView({
|
|
|
1194
1547
|
/* Row helpers */
|
|
1195
1548
|
/* ------------------------------------------------------------------ */
|
|
1196
1549
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
/* ------------------------------------------------------------------ */
|
|
1200
|
-
/* DisplayRow: one line in the transcript list */
|
|
1201
|
-
/* */
|
|
1202
|
-
/* Flat list item type — either a presentation row (user, agent, */
|
|
1203
|
-
/* tool-group, error, ...), a collapsed turn summary, or an */
|
|
1204
|
-
/* expanded-turn header with its inner rows following. */
|
|
1205
|
-
/* ------------------------------------------------------------------ */
|
|
1206
|
-
|
|
1207
|
-
type DisplayRow =
|
|
1208
|
-
| { kind: "presentation"; row: PresentationRow; indented?: boolean }
|
|
1209
|
-
| { kind: "turn-collapsed"; turn: TurnMegaRow }
|
|
1210
|
-
| { kind: "turn-expanded-header"; turn: TurnMegaRow }
|
|
1211
|
-
| { kind: "turn-expanded-footer"; turn: TurnMegaRow };
|
|
1212
|
-
|
|
1213
|
-
function flattenMegaRows(megaRows: MegaRow[], expanded: Set<number>): DisplayRow[] {
|
|
1214
|
-
const out: DisplayRow[] = [];
|
|
1215
|
-
for (const m of megaRows) {
|
|
1216
|
-
if (m.kind === "turn") {
|
|
1217
|
-
if (expanded.has(m.firstPrimaryIndex)) {
|
|
1218
|
-
out.push({ kind: "turn-expanded-header", turn: m });
|
|
1219
|
-
for (const r of m.rows) {
|
|
1220
|
-
out.push({ kind: "presentation", row: r, indented: true });
|
|
1221
|
-
}
|
|
1222
|
-
out.push({ kind: "turn-expanded-footer", turn: m });
|
|
1223
|
-
} else {
|
|
1224
|
-
out.push({ kind: "turn-collapsed", turn: m });
|
|
1225
|
-
}
|
|
1226
|
-
} else {
|
|
1227
|
-
out.push({ kind: "presentation", row: m });
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return out;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
/** Fallback for "All events" filter mode — wraps each raw event in a
|
|
1234
|
-
* synthetic PresentationRow so the transcript component can render
|
|
1235
|
-
* everything (attachments, meta, thinking, tool_result) without
|
|
1236
|
-
* going through the meaningful-transformation logic. */
|
|
1237
|
-
function allRowsAsRawRows(events: SessionEvent[]): PresentationRow[] {
|
|
1238
|
-
const out: PresentationRow[] = [];
|
|
1239
|
-
for (const e of events) {
|
|
1240
|
-
// Coerce every raw event to the closest presentation kind.
|
|
1241
|
-
let kind: PresentationRowKind;
|
|
1242
|
-
switch (e.role) {
|
|
1243
|
-
case "user":
|
|
1244
|
-
kind = "user";
|
|
1245
|
-
break;
|
|
1246
|
-
case "agent":
|
|
1247
|
-
case "agent-thinking":
|
|
1248
|
-
kind = "agent";
|
|
1249
|
-
break;
|
|
1250
|
-
case "tool-call":
|
|
1251
|
-
case "tool-result":
|
|
1252
|
-
kind = "tool-group";
|
|
1253
|
-
break;
|
|
1254
|
-
case "system":
|
|
1255
|
-
case "meta":
|
|
1256
|
-
kind = "model";
|
|
1257
|
-
break;
|
|
1258
|
-
}
|
|
1259
|
-
if (kind === "tool-group") {
|
|
1260
|
-
out.push({
|
|
1261
|
-
kind: "tool-group",
|
|
1262
|
-
toolNames: [{ name: e.toolName ?? e.rawType, count: 1 }],
|
|
1263
|
-
count: 1,
|
|
1264
|
-
events: [e],
|
|
1265
|
-
tOffsetMs: e.tOffsetMs,
|
|
1266
|
-
gapMs: e.gapMs,
|
|
1267
|
-
});
|
|
1268
|
-
} else if (kind === "agent") {
|
|
1269
|
-
out.push({
|
|
1270
|
-
kind: "agent",
|
|
1271
|
-
event: e,
|
|
1272
|
-
groupedEvents: [e],
|
|
1273
|
-
tOffsetMs: e.tOffsetMs,
|
|
1274
|
-
gapMs: e.gapMs,
|
|
1275
|
-
});
|
|
1276
|
-
} else if (kind === "user") {
|
|
1277
|
-
out.push({
|
|
1278
|
-
kind: "user",
|
|
1279
|
-
event: e,
|
|
1280
|
-
tOffsetMs: e.tOffsetMs,
|
|
1281
|
-
gapMs: e.gapMs,
|
|
1282
|
-
});
|
|
1283
|
-
} else {
|
|
1284
|
-
out.push({
|
|
1285
|
-
kind: "model",
|
|
1286
|
-
event: e,
|
|
1287
|
-
tOffsetMs: e.tOffsetMs,
|
|
1288
|
-
gapMs: e.gapMs,
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
return out;
|
|
1293
|
-
}
|
|
1550
|
+
// rowPrimaryIndex, DisplayRow, flattenMegaRows, allRowsAsRawRows moved to ./session-view/{types,helpers}.ts
|
|
1294
1551
|
|
|
1295
1552
|
/* ------------------------------------------------------------------ */
|
|
1296
1553
|
/* Header stats + token stat w/ tooltip */
|
|
1297
1554
|
/* ------------------------------------------------------------------ */
|
|
1298
1555
|
|
|
1299
|
-
|
|
1300
|
-
* Used in the single-line session header, modeled on Claude's Sessions page. */
|
|
1301
|
-
function InlineStat({
|
|
1302
|
-
icon,
|
|
1303
|
-
value,
|
|
1304
|
-
mono,
|
|
1305
|
-
truncate,
|
|
1306
|
-
}: {
|
|
1307
|
-
icon?: React.ReactNode;
|
|
1308
|
-
value: string;
|
|
1309
|
-
mono?: boolean;
|
|
1310
|
-
truncate?: boolean;
|
|
1311
|
-
}) {
|
|
1312
|
-
return (
|
|
1313
|
-
<span
|
|
1314
|
-
style={{
|
|
1315
|
-
display: "inline-flex",
|
|
1316
|
-
alignItems: "center",
|
|
1317
|
-
gap: 5,
|
|
1318
|
-
fontSize: 12,
|
|
1319
|
-
color: "var(--af-text-secondary)",
|
|
1320
|
-
maxWidth: truncate ? 260 : undefined,
|
|
1321
|
-
overflow: "hidden",
|
|
1322
|
-
textOverflow: "ellipsis",
|
|
1323
|
-
whiteSpace: "nowrap",
|
|
1324
|
-
}}
|
|
1325
|
-
>
|
|
1326
|
-
{icon && (
|
|
1327
|
-
<span
|
|
1328
|
-
style={{
|
|
1329
|
-
display: "inline-flex",
|
|
1330
|
-
color: "var(--af-text-tertiary)",
|
|
1331
|
-
}}
|
|
1332
|
-
>
|
|
1333
|
-
{icon}
|
|
1334
|
-
</span>
|
|
1335
|
-
)}
|
|
1336
|
-
<span
|
|
1337
|
-
style={{
|
|
1338
|
-
fontFamily: mono ? "var(--font-mono)" : "inherit",
|
|
1339
|
-
overflow: "hidden",
|
|
1340
|
-
textOverflow: "ellipsis",
|
|
1341
|
-
whiteSpace: "nowrap",
|
|
1342
|
-
}}
|
|
1343
|
-
>
|
|
1344
|
-
{value}
|
|
1345
|
-
</span>
|
|
1346
|
-
</span>
|
|
1347
|
-
);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
function InlineStatDivider() {
|
|
1351
|
-
return (
|
|
1352
|
-
<span
|
|
1353
|
-
style={{
|
|
1354
|
-
color: "var(--af-text-tertiary)",
|
|
1355
|
-
fontSize: 11,
|
|
1356
|
-
opacity: 0.6,
|
|
1357
|
-
}}
|
|
1358
|
-
>
|
|
1359
|
-
·
|
|
1360
|
-
</span>
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function EntrypointBadge({ entrypoint }: { entrypoint: string }) {
|
|
1365
|
-
// cli + claude-desktop are human-driven; sdk-* are programmatic.
|
|
1366
|
-
const isSdk = entrypoint.startsWith("sdk-");
|
|
1367
|
-
const tone = isSdk
|
|
1368
|
-
? { bg: "rgba(245, 158, 11, 0.16)", fg: "#b45309" }
|
|
1369
|
-
: { bg: "rgba(16, 185, 129, 0.16)", fg: "#047857" };
|
|
1370
|
-
return (
|
|
1371
|
-
<span
|
|
1372
|
-
title={`entrypoint: ${entrypoint}`}
|
|
1373
|
-
style={{
|
|
1374
|
-
fontSize: 10.5,
|
|
1375
|
-
padding: "2px 7px",
|
|
1376
|
-
borderRadius: 4,
|
|
1377
|
-
background: tone.bg,
|
|
1378
|
-
color: tone.fg,
|
|
1379
|
-
fontWeight: 600,
|
|
1380
|
-
fontFamily: "var(--font-mono)",
|
|
1381
|
-
letterSpacing: 0.2,
|
|
1382
|
-
}}
|
|
1383
|
-
>
|
|
1384
|
-
{entrypoint}
|
|
1385
|
-
</span>
|
|
1386
|
-
);
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
|
|
1390
|
-
const { ref, anchor, open, close } = useAnchoredTooltip();
|
|
1391
|
-
const u = usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1392
|
-
const totalIn = u.input + u.cacheRead + u.cacheWrite;
|
|
1393
|
-
const pctRead = totalIn > 0 ? Math.round((u.cacheRead / totalIn) * 100) : 0;
|
|
1394
|
-
const cached = u.cacheRead + u.cacheWrite;
|
|
1395
|
-
|
|
1396
|
-
return (
|
|
1397
|
-
<span
|
|
1398
|
-
ref={ref}
|
|
1399
|
-
style={{
|
|
1400
|
-
display: "inline-flex",
|
|
1401
|
-
alignItems: "center",
|
|
1402
|
-
gap: 6,
|
|
1403
|
-
fontSize: 12,
|
|
1404
|
-
color: "var(--af-text-secondary)",
|
|
1405
|
-
cursor: "default",
|
|
1406
|
-
fontFamily: "var(--font-mono)",
|
|
1407
|
-
}}
|
|
1408
|
-
onMouseEnter={open}
|
|
1409
|
-
onMouseLeave={close}
|
|
1410
|
-
>
|
|
1411
|
-
<span>
|
|
1412
|
-
{formatTokens(u.input)}
|
|
1413
|
-
<span style={{ color: "var(--af-text-tertiary)", margin: "0 3px" }}>in</span>
|
|
1414
|
-
{formatTokens(u.output)}
|
|
1415
|
-
<span style={{ color: "var(--af-text-tertiary)", marginLeft: 3 }}>out</span>
|
|
1416
|
-
</span>
|
|
1417
|
-
{cached > 0 && (
|
|
1418
|
-
<span
|
|
1419
|
-
style={{
|
|
1420
|
-
fontSize: 10.5,
|
|
1421
|
-
color: "var(--af-text-tertiary)",
|
|
1422
|
-
paddingLeft: 6,
|
|
1423
|
-
borderLeft: "1px solid var(--af-border-subtle)",
|
|
1424
|
-
}}
|
|
1425
|
-
>
|
|
1426
|
-
+{formatTokens(cached)} cached
|
|
1427
|
-
</span>
|
|
1428
|
-
)}
|
|
1429
|
-
{anchor && (
|
|
1430
|
-
<AnchoredTooltip anchor={anchor} width={280}>
|
|
1431
|
-
<TooltipRow label="Input (fresh)" value={u.input.toLocaleString()} />
|
|
1432
|
-
<TooltipRow label="Output" value={u.output.toLocaleString()} />
|
|
1433
|
-
<TooltipRow label="Cache read" value={`${u.cacheRead.toLocaleString()} (${pctRead}%)`} />
|
|
1434
|
-
<TooltipRow label="Cache write" value={u.cacheWrite.toLocaleString()} />
|
|
1435
|
-
<div
|
|
1436
|
-
style={{
|
|
1437
|
-
marginTop: 6,
|
|
1438
|
-
paddingTop: 6,
|
|
1439
|
-
borderTop: "1px solid rgba(241,245,249,0.12)",
|
|
1440
|
-
opacity: 0.65,
|
|
1441
|
-
fontSize: 10,
|
|
1442
|
-
whiteSpace: "normal",
|
|
1443
|
-
lineHeight: 1.4,
|
|
1444
|
-
}}
|
|
1445
|
-
>
|
|
1446
|
-
Cache reads are cumulative across all API requests and billed at ~10% of regular input.
|
|
1447
|
-
</div>
|
|
1448
|
-
</AnchoredTooltip>
|
|
1449
|
-
)}
|
|
1450
|
-
</span>
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// Used for tooltips that live inside an overflow:hidden ancestor (like the
|
|
1455
|
-
// session-header row). getBoundingClientRect + position:fixed lets the
|
|
1456
|
-
// tooltip escape the clipping context instead of being cropped.
|
|
1457
|
-
function useAnchoredTooltip() {
|
|
1458
|
-
const ref = useRef<HTMLSpanElement>(null);
|
|
1459
|
-
const [anchor, setAnchor] = useState<{ top: number; left: number } | null>(null);
|
|
1460
|
-
const open = () => {
|
|
1461
|
-
if (!ref.current) return;
|
|
1462
|
-
const r = ref.current.getBoundingClientRect();
|
|
1463
|
-
setAnchor({ top: r.bottom + 6, left: r.left });
|
|
1464
|
-
};
|
|
1465
|
-
const close = () => setAnchor(null);
|
|
1466
|
-
return { ref, anchor, open, close };
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
function AnchoredTooltip({
|
|
1470
|
-
anchor,
|
|
1471
|
-
width,
|
|
1472
|
-
children,
|
|
1473
|
-
}: {
|
|
1474
|
-
anchor: { top: number; left: number };
|
|
1475
|
-
width: number;
|
|
1476
|
-
children: React.ReactNode;
|
|
1477
|
-
}) {
|
|
1478
|
-
return (
|
|
1479
|
-
<div
|
|
1480
|
-
style={{
|
|
1481
|
-
position: "fixed",
|
|
1482
|
-
top: anchor.top,
|
|
1483
|
-
left: anchor.left,
|
|
1484
|
-
zIndex: 1000,
|
|
1485
|
-
background: "#1A1A1A",
|
|
1486
|
-
color: "#F5F1EC",
|
|
1487
|
-
padding: "8px 12px",
|
|
1488
|
-
borderRadius: 6,
|
|
1489
|
-
fontSize: 11,
|
|
1490
|
-
fontFamily: "var(--font-mono)",
|
|
1491
|
-
lineHeight: 1.5,
|
|
1492
|
-
pointerEvents: "none",
|
|
1493
|
-
boxShadow: "0 4px 16px rgba(15,23,42,0.24)",
|
|
1494
|
-
width,
|
|
1495
|
-
}}
|
|
1496
|
-
>
|
|
1497
|
-
{children}
|
|
1498
|
-
</div>
|
|
1499
|
-
);
|
|
1500
|
-
}
|
|
1556
|
+
// InlineStat, InlineStatDivider, EntrypointBadge, InlineTokenStat, useAnchoredTooltip, AnchoredTooltip moved to ./session-view/header-stats.tsx
|
|
1501
1557
|
|
|
1502
1558
|
/* ------------------------------------------------------------------ */
|
|
1503
1559
|
/* Generic Tooltip */
|
|
1504
1560
|
/* ------------------------------------------------------------------ */
|
|
1505
1561
|
|
|
1506
|
-
|
|
1507
|
-
return (
|
|
1508
|
-
<div
|
|
1509
|
-
style={{
|
|
1510
|
-
position: "absolute",
|
|
1511
|
-
zIndex: 100,
|
|
1512
|
-
background: "#1A1A1A",
|
|
1513
|
-
color: "#F5F1EC",
|
|
1514
|
-
borderRadius: 8,
|
|
1515
|
-
padding: "10px 12px",
|
|
1516
|
-
fontSize: 11,
|
|
1517
|
-
fontFamily: "var(--font-mono)",
|
|
1518
|
-
lineHeight: 1.5,
|
|
1519
|
-
boxShadow: "0 4px 20px rgba(0,0,0,0.18)",
|
|
1520
|
-
pointerEvents: "none",
|
|
1521
|
-
whiteSpace: "nowrap",
|
|
1522
|
-
...style,
|
|
1523
|
-
}}
|
|
1524
|
-
>
|
|
1525
|
-
{children}
|
|
1526
|
-
</div>
|
|
1527
|
-
);
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function TooltipRow({ label, value }: { label: string; value: string }) {
|
|
1531
|
-
return (
|
|
1532
|
-
<div
|
|
1533
|
-
style={{
|
|
1534
|
-
display: "flex",
|
|
1535
|
-
justifyContent: "space-between",
|
|
1536
|
-
gap: 12,
|
|
1537
|
-
}}
|
|
1538
|
-
>
|
|
1539
|
-
<span style={{ opacity: 0.7 }}>{label}</span>
|
|
1540
|
-
<span>{value}</span>
|
|
1541
|
-
</div>
|
|
1542
|
-
);
|
|
1543
|
-
}
|
|
1562
|
+
// Tooltip, TooltipRow moved to ./session-view/tooltip.tsx
|
|
1544
1563
|
|
|
1545
1564
|
/* ------------------------------------------------------------------ */
|
|
1546
1565
|
/* Mini-map */
|
|
@@ -1561,22 +1580,7 @@ function TooltipRow({ label, value }: { label: string; value: string }) {
|
|
|
1561
1580
|
/* - Rich hover card (role pill + preview) instead of bare SVG title. */
|
|
1562
1581
|
/* ------------------------------------------------------------------ */
|
|
1563
1582
|
|
|
1564
|
-
|
|
1565
|
-
| {
|
|
1566
|
-
kind: "row";
|
|
1567
|
-
row: PresentationRow;
|
|
1568
|
-
primaryIndex: number;
|
|
1569
|
-
start: number;
|
|
1570
|
-
end: number;
|
|
1571
|
-
}
|
|
1572
|
-
| {
|
|
1573
|
-
kind: "turn";
|
|
1574
|
-
turn: TurnMegaRow;
|
|
1575
|
-
primaryIndex: number;
|
|
1576
|
-
start: number;
|
|
1577
|
-
end: number;
|
|
1578
|
-
}
|
|
1579
|
-
| { kind: "idle"; start: number; end: number; durationMs: number };
|
|
1583
|
+
// MinimapSeg moved to ./session-view/types.ts
|
|
1580
1584
|
|
|
1581
1585
|
/** All rows render at full bar height — distinction comes from color/pattern
|
|
1582
1586
|
* alone, matching Claude Managed Agents' unified-height timeline. */
|
|
@@ -1593,6 +1597,7 @@ const ROW_IMPORTANCE: Record<PresentationRowKind, number> = {
|
|
|
1593
1597
|
function Minimap({
|
|
1594
1598
|
displayRows,
|
|
1595
1599
|
durationMs,
|
|
1600
|
+
dayWindow,
|
|
1596
1601
|
selectedIndex,
|
|
1597
1602
|
onSelect,
|
|
1598
1603
|
headerOffset,
|
|
@@ -1606,9 +1611,13 @@ function Minimap({
|
|
|
1606
1611
|
coldResumeMarkers,
|
|
1607
1612
|
rawIdleBands,
|
|
1608
1613
|
model,
|
|
1614
|
+
onPlayheadChange,
|
|
1609
1615
|
}: {
|
|
1610
1616
|
displayRows: DisplayRow[];
|
|
1611
1617
|
durationMs: number;
|
|
1618
|
+
/** When set, scope the timeline to this offset window (one local day).
|
|
1619
|
+
* null renders the full session. */
|
|
1620
|
+
dayWindow?: { startMs: number; endMs: number } | null;
|
|
1612
1621
|
selectedIndex: number | null;
|
|
1613
1622
|
onSelect: (i: number) => void;
|
|
1614
1623
|
headerOffset: number;
|
|
@@ -1629,6 +1638,10 @@ function Minimap({
|
|
|
1629
1638
|
* surface their dead-air on the minimap. */
|
|
1630
1639
|
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
1631
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;
|
|
1632
1645
|
}) {
|
|
1633
1646
|
const WIDTH = 1400;
|
|
1634
1647
|
/** Main timeline height. Sub-agent lanes stack below this. */
|
|
@@ -1667,6 +1680,10 @@ function Minimap({
|
|
|
1667
1680
|
};
|
|
1668
1681
|
} | null>(null);
|
|
1669
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;
|
|
1670
1687
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
1671
1688
|
|
|
1672
1689
|
/* Playhead — track which transcript row is at the top of the viewport.
|
|
@@ -1675,60 +1692,54 @@ function Minimap({
|
|
|
1675
1692
|
* on every scroll event. For a session with ~2000 rows that's thousands
|
|
1676
1693
|
* of DOM queries per second during scroll — enough to pin a CPU core
|
|
1677
1694
|
* and make Chrome ask to kill the tab. IntersectionObserver is event-
|
|
1678
|
-
* driven: the browser only notifies us when rows
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
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.
|
|
1681
1698
|
*/
|
|
1682
1699
|
useEffect(() => {
|
|
1683
1700
|
const main = containerRef.current?.closest("main") as HTMLElement | null;
|
|
1684
1701
|
if (!main) return;
|
|
1685
1702
|
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
//
|
|
1689
|
-
//
|
|
1690
|
-
//
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
// the playhead. Using a Set keeps updates O(1).
|
|
1699
|
-
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>();
|
|
1700
1715
|
|
|
1701
1716
|
const publishTopmost = () => {
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
if (
|
|
1714
|
-
|
|
1715
|
-
|
|
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;
|
|
1716
1731
|
}
|
|
1717
1732
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
const tOff = Number(topmost.getAttribute("data-sl-toffset"));
|
|
1723
|
-
setPlayheadMs(Number.isNaN(tOff) ? null : tOff);
|
|
1733
|
+
setPlayheadMs(value);
|
|
1734
|
+
onPlayheadChangeRef.current?.(value);
|
|
1724
1735
|
};
|
|
1725
1736
|
|
|
1726
1737
|
const observer = new IntersectionObserver(
|
|
1727
1738
|
(entries) => {
|
|
1728
1739
|
for (const e of entries) {
|
|
1729
1740
|
const el = e.target as HTMLElement;
|
|
1730
|
-
if (e.isIntersecting)
|
|
1731
|
-
else
|
|
1741
|
+
if (e.isIntersecting) visibleRows.add(el);
|
|
1742
|
+
else visibleRows.delete(el);
|
|
1732
1743
|
}
|
|
1733
1744
|
publishTopmost();
|
|
1734
1745
|
},
|
|
@@ -1743,7 +1754,39 @@ function Minimap({
|
|
|
1743
1754
|
return () => observer.disconnect();
|
|
1744
1755
|
}, [displayRows.length, headerOffset]);
|
|
1745
1756
|
|
|
1746
|
-
|
|
1757
|
+
// Offset window for the rendered timeline. `winEnd` is the absolute offset
|
|
1758
|
+
// of the right edge (session/day end); `safeDur` is the window LENGTH used
|
|
1759
|
+
// as the proportional-width divisor. For a single-day session (dayWindow
|
|
1760
|
+
// null) these collapse to [0, durationMs] — identical to the old behavior.
|
|
1761
|
+
const winStart = dayWindow?.startMs ?? 0;
|
|
1762
|
+
const winEnd = dayWindow?.endMs ?? durationMs;
|
|
1763
|
+
const safeDur = Math.max(winEnd - winStart, 1);
|
|
1764
|
+
|
|
1765
|
+
// Subagent/workflow lanes are scoped to the same window as the main
|
|
1766
|
+
// timeline so a day view doesn't reserve empty lanes for runs that happened
|
|
1767
|
+
// on other days. Overlap test, not containment — a run straddling the
|
|
1768
|
+
// window edge still shows.
|
|
1769
|
+
const winSubagents = useMemo(
|
|
1770
|
+
() =>
|
|
1771
|
+
(subagents ?? []).filter(
|
|
1772
|
+
(s) =>
|
|
1773
|
+
s.startTOffsetMs !== undefined &&
|
|
1774
|
+
s.endTOffsetMs !== undefined &&
|
|
1775
|
+
s.endTOffsetMs >= winStart &&
|
|
1776
|
+
s.startTOffsetMs <= winEnd,
|
|
1777
|
+
),
|
|
1778
|
+
[subagents, winStart, winEnd],
|
|
1779
|
+
);
|
|
1780
|
+
const winWorkflows = useMemo(
|
|
1781
|
+
() =>
|
|
1782
|
+
(workflows ?? []).filter(
|
|
1783
|
+
(w) =>
|
|
1784
|
+
w.startTOffsetMs !== undefined &&
|
|
1785
|
+
w.startTOffsetMs <= winEnd &&
|
|
1786
|
+
(w.endTOffsetMs ?? winEnd) >= winStart,
|
|
1787
|
+
),
|
|
1788
|
+
[workflows, winStart, winEnd],
|
|
1789
|
+
);
|
|
1747
1790
|
|
|
1748
1791
|
/* Build segments from the same display-row stream the transcript
|
|
1749
1792
|
uses. Collapsed turns become one wide segment; expanded-turn
|
|
@@ -1769,6 +1812,7 @@ function Minimap({
|
|
|
1769
1812
|
if (d.kind === "turn-expanded-footer") continue;
|
|
1770
1813
|
if (d.kind === "turn-collapsed") {
|
|
1771
1814
|
if (d.turn.tOffsetMs === undefined) continue;
|
|
1815
|
+
if (d.turn.tOffsetMs < winStart || d.turn.tOffsetMs > winEnd) continue;
|
|
1772
1816
|
withTime.push({
|
|
1773
1817
|
kind: "turn",
|
|
1774
1818
|
turn: d.turn,
|
|
@@ -1778,6 +1822,7 @@ function Minimap({
|
|
|
1778
1822
|
} else {
|
|
1779
1823
|
// presentation row
|
|
1780
1824
|
if (d.row.tOffsetMs === undefined) continue;
|
|
1825
|
+
if (d.row.tOffsetMs < winStart || d.row.tOffsetMs > winEnd) continue;
|
|
1781
1826
|
withTime.push({
|
|
1782
1827
|
kind: "row",
|
|
1783
1828
|
row: d.row,
|
|
@@ -1791,7 +1836,9 @@ function Minimap({
|
|
|
1791
1836
|
// the start of the next idle band (or the next row, whichever comes
|
|
1792
1837
|
// first). This stops a row's rectangle from spanning across an idle
|
|
1793
1838
|
// gap and visually swallowing it.
|
|
1794
|
-
const idleBands = (rawIdleBands ?? [])
|
|
1839
|
+
const idleBands = (rawIdleBands ?? [])
|
|
1840
|
+
.filter((b) => b.end > winStart && b.start < winEnd)
|
|
1841
|
+
.sort((a, b) => a.start - b.start);
|
|
1795
1842
|
const nextIdleStartFrom = (t: number): number | undefined => {
|
|
1796
1843
|
for (const b of idleBands) {
|
|
1797
1844
|
if (b.start >= t) return b.start;
|
|
@@ -1805,12 +1852,15 @@ function Minimap({
|
|
|
1805
1852
|
const start = item.start;
|
|
1806
1853
|
const nextRowStart = next?.start;
|
|
1807
1854
|
const nextIdleStart = nextIdleStartFrom(start + 1);
|
|
1808
|
-
// Cap end to whichever comes first: next visible row, next idle band
|
|
1855
|
+
// Cap end to whichever comes first: next visible row, next idle band,
|
|
1856
|
+
// or the window's right edge (so a day's last segment doesn't bleed
|
|
1857
|
+
// past the scoped window into the next day).
|
|
1809
1858
|
const cap = (raw: number) =>
|
|
1810
1859
|
Math.min(
|
|
1811
1860
|
raw,
|
|
1812
1861
|
nextRowStart ?? Number.POSITIVE_INFINITY,
|
|
1813
1862
|
nextIdleStart ?? Number.POSITIVE_INFINITY,
|
|
1863
|
+
winEnd,
|
|
1814
1864
|
);
|
|
1815
1865
|
|
|
1816
1866
|
if (item.kind === "turn") {
|
|
@@ -1891,7 +1941,7 @@ function Minimap({
|
|
|
1891
1941
|
}
|
|
1892
1942
|
}
|
|
1893
1943
|
return bucketed;
|
|
1894
|
-
}, [displayRows, safeDur, rawIdleBands]);
|
|
1944
|
+
}, [displayRows, safeDur, rawIdleBands, winStart, winEnd]);
|
|
1895
1945
|
|
|
1896
1946
|
/* Sequential layout with a minimum displayed width per segment.
|
|
1897
1947
|
- Raw proportional width = (seg.end - seg.start) / safeDur * WIDTH
|
|
@@ -1944,10 +1994,10 @@ function Minimap({
|
|
|
1944
1994
|
// sweep — O(N×L) where L is the number of lanes (small in practice).
|
|
1945
1995
|
// Returns a Map agentId → laneIndex plus the total lane count.
|
|
1946
1996
|
const { laneOf, laneCount } = useMemo(() => {
|
|
1947
|
-
if (
|
|
1997
|
+
if (winSubagents.length === 0) {
|
|
1948
1998
|
return { laneOf: new Map<string, number>(), laneCount: 0 };
|
|
1949
1999
|
}
|
|
1950
|
-
const sorted = [...
|
|
2000
|
+
const sorted = [...winSubagents].sort(
|
|
1951
2001
|
(a, b) => (a.startTOffsetMs ?? 0) - (b.startTOffsetMs ?? 0),
|
|
1952
2002
|
);
|
|
1953
2003
|
const laneEnds: number[] = [];
|
|
@@ -1970,7 +2020,7 @@ function Minimap({
|
|
|
1970
2020
|
laneOf.set(s.agentId, assigned);
|
|
1971
2021
|
}
|
|
1972
2022
|
return { laneOf, laneCount: laneEnds.length };
|
|
1973
|
-
}, [
|
|
2023
|
+
}, [winSubagents]);
|
|
1974
2024
|
|
|
1975
2025
|
const SUB_BLOCK_TOP = MAIN_H + SUB_LANE_GAP;
|
|
1976
2026
|
const SUB_LANES_H = laneCount > 0 ? laneCount * SUB_LANE_H : 0;
|
|
@@ -1985,7 +2035,7 @@ function Minimap({
|
|
|
1985
2035
|
// matching the open-ended bar the render draws. Counting only placeable
|
|
1986
2036
|
// runs keeps wfLaneCount in lockstep with what renders, so no empty
|
|
1987
2037
|
// reserved band appears.
|
|
1988
|
-
const placeable =
|
|
2038
|
+
const placeable = winWorkflows;
|
|
1989
2039
|
if (placeable.length === 0) {
|
|
1990
2040
|
return { wfLaneOf: new Map<string, number>(), wfLaneCount: 0 };
|
|
1991
2041
|
}
|
|
@@ -1996,7 +2046,7 @@ function Minimap({
|
|
|
1996
2046
|
const wfLaneOf = new Map<string, number>();
|
|
1997
2047
|
for (const w of sorted) {
|
|
1998
2048
|
const start = w.startTOffsetMs ?? 0;
|
|
1999
|
-
const end = w.endTOffsetMs ??
|
|
2049
|
+
const end = w.endTOffsetMs ?? winEnd;
|
|
2000
2050
|
let assigned = -1;
|
|
2001
2051
|
for (let i = 0; i < laneEnds.length; i++) {
|
|
2002
2052
|
if (laneEnds[i]! <= start) {
|
|
@@ -2012,7 +2062,7 @@ function Minimap({
|
|
|
2012
2062
|
wfLaneOf.set(w.runId, assigned);
|
|
2013
2063
|
}
|
|
2014
2064
|
return { wfLaneOf, wfLaneCount: laneEnds.length };
|
|
2015
|
-
}, [
|
|
2065
|
+
}, [winWorkflows, winEnd]);
|
|
2016
2066
|
|
|
2017
2067
|
const SUB_BLOCK_BOTTOM = MAIN_H + (laneCount > 0 ? SUB_LANE_GAP + SUB_LANES_H : 0);
|
|
2018
2068
|
const WF_BLOCK_TOP = SUB_BLOCK_BOTTOM + (wfLaneCount > 0 ? SUB_LANE_GAP : 0);
|
|
@@ -2153,7 +2203,7 @@ function Minimap({
|
|
|
2153
2203
|
glance. Bars use the same msToX mapping as the main timeline
|
|
2154
2204
|
so the subagent's start/end aligns vertically with whatever
|
|
2155
2205
|
was happening in the main session at that time. */}
|
|
2156
|
-
{laneCount > 0 &&
|
|
2206
|
+
{laneCount > 0 && winSubagents.length > 0 && (
|
|
2157
2207
|
<g>
|
|
2158
2208
|
{/* Faint divider line above the subagent lane block */}
|
|
2159
2209
|
<line
|
|
@@ -2165,7 +2215,7 @@ function Minimap({
|
|
|
2165
2215
|
strokeWidth="0.6"
|
|
2166
2216
|
strokeDasharray="2 4"
|
|
2167
2217
|
/>
|
|
2168
|
-
{
|
|
2218
|
+
{winSubagents.map((s) => {
|
|
2169
2219
|
const lane = laneOf.get(s.agentId) ?? 0;
|
|
2170
2220
|
if (s.startTOffsetMs === undefined || s.endTOffsetMs === undefined) return null;
|
|
2171
2221
|
const startX = msToX(s.startTOffsetMs);
|
|
@@ -2217,7 +2267,7 @@ function Minimap({
|
|
|
2217
2267
|
time axis as the main timeline, colored by status (orange tier,
|
|
2218
2268
|
distinct from the blue/purple subagents). The agent count is the
|
|
2219
2269
|
headline — a single Workflow tool call hides a whole fleet. */}
|
|
2220
|
-
{wfLaneCount > 0 &&
|
|
2270
|
+
{wfLaneCount > 0 && winWorkflows.length > 0 && (
|
|
2221
2271
|
<g>
|
|
2222
2272
|
<line
|
|
2223
2273
|
x1={0}
|
|
@@ -2228,7 +2278,7 @@ function Minimap({
|
|
|
2228
2278
|
strokeWidth="0.6"
|
|
2229
2279
|
strokeDasharray="2 4"
|
|
2230
2280
|
/>
|
|
2231
|
-
{
|
|
2281
|
+
{winWorkflows.map((w) => {
|
|
2232
2282
|
if (w.startTOffsetMs === undefined) return null;
|
|
2233
2283
|
const lane = wfLaneOf.get(w.runId) ?? 0;
|
|
2234
2284
|
const startX = msToX(w.startTOffsetMs);
|
|
@@ -2236,7 +2286,7 @@ function Minimap({
|
|
|
2236
2286
|
// the session end so it's visible (and dashed, to read as ongoing)
|
|
2237
2287
|
// rather than vanishing while still occupying a reserved lane.
|
|
2238
2288
|
const inProgress = w.endTOffsetMs === undefined;
|
|
2239
|
-
const endX = msToX(w.endTOffsetMs ??
|
|
2289
|
+
const endX = msToX(w.endTOffsetMs ?? winEnd);
|
|
2240
2290
|
const wWidth = Math.max(endX - startX, 8);
|
|
2241
2291
|
const y = WF_BLOCK_TOP + lane * WF_LANE_H;
|
|
2242
2292
|
const h = WF_LANE_H - 2;
|
|
@@ -2283,6 +2333,7 @@ function Minimap({
|
|
|
2283
2333
|
{/* PR markers — diamond + vertical line at each `gh pr create` */}
|
|
2284
2334
|
{prMarkers?.map((pr, i) => {
|
|
2285
2335
|
if (pr.tOffsetMs === undefined) return null;
|
|
2336
|
+
if (pr.tOffsetMs < winStart || pr.tOffsetMs > winEnd) return null;
|
|
2286
2337
|
const x = msToX(pr.tOffsetMs);
|
|
2287
2338
|
return (
|
|
2288
2339
|
<g
|
|
@@ -2320,6 +2371,7 @@ function Minimap({
|
|
|
2320
2371
|
})}
|
|
2321
2372
|
|
|
2322
2373
|
{coldResumeMarkers?.map((cr, i) => {
|
|
2374
|
+
if (cr.tOffsetMs < winStart || cr.tOffsetMs > winEnd) return null;
|
|
2323
2375
|
const x = msToX(cr.tOffsetMs);
|
|
2324
2376
|
return (
|
|
2325
2377
|
<g
|
|
@@ -2336,12 +2388,17 @@ function Minimap({
|
|
|
2336
2388
|
strokeWidth="1.5"
|
|
2337
2389
|
strokeDasharray="4 3"
|
|
2338
2390
|
opacity="0.75"
|
|
2391
|
+
pointerEvents="none"
|
|
2339
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. */}
|
|
2340
2397
|
<rect
|
|
2341
|
-
x={x -
|
|
2342
|
-
y={
|
|
2343
|
-
width={
|
|
2344
|
-
height={
|
|
2398
|
+
x={x - 6}
|
|
2399
|
+
y={MAIN_H / 2 - 8}
|
|
2400
|
+
width={12}
|
|
2401
|
+
height={16}
|
|
2345
2402
|
fill="transparent"
|
|
2346
2403
|
/>
|
|
2347
2404
|
<polygon
|
|
@@ -2349,6 +2406,7 @@ function Minimap({
|
|
|
2349
2406
|
fill="#D97706"
|
|
2350
2407
|
stroke="var(--af-surface)"
|
|
2351
2408
|
strokeWidth="1.5"
|
|
2409
|
+
pointerEvents="none"
|
|
2352
2410
|
/>
|
|
2353
2411
|
</g>
|
|
2354
2412
|
);
|
|
@@ -2446,33 +2504,7 @@ function Minimap({
|
|
|
2446
2504
|
);
|
|
2447
2505
|
}
|
|
2448
2506
|
|
|
2449
|
-
|
|
2450
|
-
* variant of their color so true parallelism is visually distinct from
|
|
2451
|
-
* blocking subagent calls (where the parent waits and there's no
|
|
2452
|
-
* meaningful overlap with main session work). */
|
|
2453
|
-
function subagentColor(agentType: string, background?: boolean): string {
|
|
2454
|
-
const palette: Record<string, [string, string]> = {
|
|
2455
|
-
"general-purpose": ["#5C84C3", "#7BA3DC"],
|
|
2456
|
-
Explore: ["#A855F7", "#C57BFF"],
|
|
2457
|
-
Plan: ["#F59E0B", "#FFBD3D"],
|
|
2458
|
-
"code-reviewer": ["#34D399", "#5EE5B0"],
|
|
2459
|
-
"playwright-qa-verifier": ["#22D3EE", "#67E8F9"],
|
|
2460
|
-
"claude-code-guide": ["#EC4899", "#F472B6"],
|
|
2461
|
-
};
|
|
2462
|
-
const [base, bright] = palette[agentType] ?? ["#8A8580", "#A8A19A"];
|
|
2463
|
-
return background ? bright : base;
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
/** Workflow signature color, modulated by run status. Deliberately a warm
|
|
2467
|
-
* orange — distinct from the blue/purple subagent palette so workflow lanes
|
|
2468
|
-
* read as a different tier on the minimap. Failed runs go red, in-flight
|
|
2469
|
-
* runs go green. */
|
|
2470
|
-
function workflowColor(status: string): string {
|
|
2471
|
-
const s = status.toLowerCase();
|
|
2472
|
-
if (s === "failed" || s === "aborted" || s === "error") return "#EF4444";
|
|
2473
|
-
if (s === "running" || s === "in_progress" || s === "active") return "#10B981";
|
|
2474
|
-
return "#EA580C";
|
|
2475
|
-
}
|
|
2507
|
+
// subagentColor, workflowColor moved to ./session-view/colors.ts
|
|
2476
2508
|
|
|
2477
2509
|
function MinimapHoverCard({
|
|
2478
2510
|
containerRef,
|
|
@@ -3070,6 +3102,10 @@ function TranscriptList({
|
|
|
3070
3102
|
team,
|
|
3071
3103
|
model,
|
|
3072
3104
|
rawIdleBands,
|
|
3105
|
+
sessionDays,
|
|
3106
|
+
entryByDay,
|
|
3107
|
+
dayStripRefs,
|
|
3108
|
+
onJumpAcrossDay,
|
|
3073
3109
|
}: {
|
|
3074
3110
|
displayRows: DisplayRow[];
|
|
3075
3111
|
rowRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
|
|
@@ -3084,6 +3120,20 @@ function TranscriptList({
|
|
|
3084
3120
|
* row whose start matches a band's end, so Codex sessions get the same
|
|
3085
3121
|
* "Session idle X minutes" markers Claude has always had. */
|
|
3086
3122
|
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
3123
|
+
/** Day buckets (sorted by startMs) + per-day digest cards. The body drops a
|
|
3124
|
+
* card at each day's first row so per-day perception sits inline with that
|
|
3125
|
+
* day's work instead of stacked at the top. */
|
|
3126
|
+
sessionDays: { key: string; startMs: number; endMs: number; count: number }[];
|
|
3127
|
+
entryByDay: Map<string, Entry>;
|
|
3128
|
+
dayStripRefs: React.MutableRefObject<Record<string, HTMLDivElement | null>>;
|
|
3129
|
+
/** Hop to an adjacent day from an inline day-boundary button.
|
|
3130
|
+
* (selectKey, scrollKey, block, fallbackRowIndex). */
|
|
3131
|
+
onJumpAcrossDay: (
|
|
3132
|
+
selectKey: string,
|
|
3133
|
+
scrollKey: string,
|
|
3134
|
+
block: "start" | "end",
|
|
3135
|
+
fallbackIndex: number,
|
|
3136
|
+
) => void;
|
|
3087
3137
|
}) {
|
|
3088
3138
|
// Find the last collapsed turn index so we can mark it as in-progress
|
|
3089
3139
|
// when the session is live.
|
|
@@ -3116,8 +3166,26 @@ function TranscriptList({
|
|
|
3116
3166
|
if (d.kind === "turn-expanded-footer") return undefined;
|
|
3117
3167
|
return d.row.tOffsetMs;
|
|
3118
3168
|
};
|
|
3169
|
+
// A display row → its event PRIMARY index (what jumpAcrossDay's turn/row
|
|
3170
|
+
// lookups are keyed by — NOT the displayRows loop position).
|
|
3171
|
+
const rowPrimaryIndexOf = (d: DisplayRow): number =>
|
|
3172
|
+
d.kind === "presentation" ? rowPrimaryIndex(d.row) : d.turn.firstPrimaryIndex;
|
|
3173
|
+
/** Which local day a row falls in: the last day bucket whose start is at or
|
|
3174
|
+
* before the row's offset. sessionDays is pre-sorted by startMs. */
|
|
3175
|
+
const dayKeyForOffset = (tOff: number | undefined): string | null => {
|
|
3176
|
+
if (tOff === undefined) return null;
|
|
3177
|
+
let key: string | null = null;
|
|
3178
|
+
for (const dd of sessionDays) {
|
|
3179
|
+
if (dd.startMs <= tOff) key = dd.key;
|
|
3180
|
+
else break;
|
|
3181
|
+
}
|
|
3182
|
+
return key;
|
|
3183
|
+
};
|
|
3119
3184
|
|
|
3120
3185
|
const out: React.ReactNode[] = [];
|
|
3186
|
+
// Tracks the day of the last emitted row so a day's digest card is dropped
|
|
3187
|
+
// once, before that day's first non-indented row.
|
|
3188
|
+
let currentDayKey: string | null = null;
|
|
3121
3189
|
// A user row and the turn-collapsed row that follows it share a
|
|
3122
3190
|
// tOffsetMs (turn anchors at the user message — see buildMegaRows), so
|
|
3123
3191
|
// both would match the same between-turn band without this guard. Track
|
|
@@ -3139,6 +3207,54 @@ function TranscriptList({
|
|
|
3139
3207
|
emittedBandStarts.add(band.start);
|
|
3140
3208
|
out.push(<IdleDivider key={`idle-before-${i}`} gapMs={band.durationMs} />);
|
|
3141
3209
|
}
|
|
3210
|
+
// Day boundary: drop this day's digest card at its first row (after any
|
|
3211
|
+
// overnight idle divider that led into the day) so per-day perception
|
|
3212
|
+
// sits inline with that day's work, not stacked at the top. The boundary
|
|
3213
|
+
// also gets inline jump controls so you can page across days while
|
|
3214
|
+
// reading — "↓ start of next day" sits at the bottom of the day that just
|
|
3215
|
+
// ended, "↑ end of previous day" at the top of the new one.
|
|
3216
|
+
const dk = dayKeyForOffset(rowTOffset(d));
|
|
3217
|
+
if (dk && dk !== currentDayKey) {
|
|
3218
|
+
const endedKey = currentDayKey; // day that just ended; null on the first day
|
|
3219
|
+
currentDayKey = dk;
|
|
3220
|
+
// Fallback scroll target for days with no digest card: the day's first
|
|
3221
|
+
// row by PRIMARY index (i is the displayRows position — wrong key space).
|
|
3222
|
+
const firstIdx = rowPrimaryIndexOf(d);
|
|
3223
|
+
if (endedKey) {
|
|
3224
|
+
out.push(
|
|
3225
|
+
<DayJumpRow
|
|
3226
|
+
key={`daydown-${dk}`}
|
|
3227
|
+
dir="down"
|
|
3228
|
+
label={formatDayKey(dk)}
|
|
3229
|
+
onClick={() => onJumpAcrossDay(dk, dk, "start", firstIdx)}
|
|
3230
|
+
/>,
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
const dayEntry = entryByDay.get(dk);
|
|
3234
|
+
if (dayEntry) {
|
|
3235
|
+
out.push(
|
|
3236
|
+
<div
|
|
3237
|
+
key={`daystrip-${dk}`}
|
|
3238
|
+
ref={(el) => {
|
|
3239
|
+
dayStripRefs.current[dk] = el;
|
|
3240
|
+
}}
|
|
3241
|
+
style={{ marginBottom: 12, scrollMarginTop: stickyOffset }}
|
|
3242
|
+
>
|
|
3243
|
+
<EntryDayStrip entry={dayEntry} />
|
|
3244
|
+
</div>,
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
if (endedKey) {
|
|
3248
|
+
out.push(
|
|
3249
|
+
<DayJumpRow
|
|
3250
|
+
key={`dayup-${dk}`}
|
|
3251
|
+
dir="up"
|
|
3252
|
+
label={formatDayKey(endedKey)}
|
|
3253
|
+
onClick={() => onJumpAcrossDay(endedKey, dk, "end", firstIdx)}
|
|
3254
|
+
/>,
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3142
3258
|
}
|
|
3143
3259
|
|
|
3144
3260
|
// Collapsed turn summary row
|
|
@@ -3235,6 +3351,51 @@ function IdleDivider({ gapMs }: { gapMs: number }) {
|
|
|
3235
3351
|
);
|
|
3236
3352
|
}
|
|
3237
3353
|
|
|
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
|
+
|
|
3238
3399
|
function findColdResumeInDisplayRow(
|
|
3239
3400
|
d: DisplayRow,
|
|
3240
3401
|
): SessionEvent["coldResume"] | null {
|
|
@@ -4328,170 +4489,13 @@ function ToolGroupLabel({
|
|
|
4328
4489
|
);
|
|
4329
4490
|
}
|
|
4330
4491
|
|
|
4331
|
-
|
|
4332
|
-
const [hover, setHover] = useState(false);
|
|
4333
|
-
const totalIn = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
4334
|
-
const pctRead = totalIn > 0 ? Math.round((usage.cacheRead / totalIn) * 100) : 0;
|
|
4335
|
-
return (
|
|
4336
|
-
<span
|
|
4337
|
-
style={{
|
|
4338
|
-
position: "relative",
|
|
4339
|
-
fontFamily: "var(--font-mono)",
|
|
4340
|
-
fontSize: 11,
|
|
4341
|
-
color: "var(--af-text-secondary)",
|
|
4342
|
-
display: "inline-flex",
|
|
4343
|
-
alignItems: "center",
|
|
4344
|
-
gap: 4,
|
|
4345
|
-
whiteSpace: "nowrap",
|
|
4346
|
-
}}
|
|
4347
|
-
onMouseEnter={() => setHover(true)}
|
|
4348
|
-
onMouseLeave={() => setHover(false)}
|
|
4349
|
-
>
|
|
4350
|
-
<span style={{ opacity: 0.6 }}>▤</span>
|
|
4351
|
-
{formatTokens(totalIn)} / {formatTokens(usage.output)}
|
|
4352
|
-
{hover && (
|
|
4353
|
-
<Tooltip style={{ right: 0, top: "calc(100% + 6px)", minWidth: 180 }}>
|
|
4354
|
-
<TooltipRow label="Input" value={usage.input.toLocaleString()} />
|
|
4355
|
-
<TooltipRow
|
|
4356
|
-
label="Cache read"
|
|
4357
|
-
value={`${usage.cacheRead.toLocaleString()} (${pctRead}%)`}
|
|
4358
|
-
/>
|
|
4359
|
-
<TooltipRow label="Cache write" value={usage.cacheWrite.toLocaleString()} />
|
|
4360
|
-
<TooltipRow label="Output" value={usage.output.toLocaleString()} />
|
|
4361
|
-
</Tooltip>
|
|
4362
|
-
)}
|
|
4363
|
-
</span>
|
|
4364
|
-
);
|
|
4365
|
-
}
|
|
4366
|
-
|
|
4367
|
-
/** Like TokenChip but for per-turn aggregates. Primary display is
|
|
4368
|
-
* fresh input / output (not the cache-inflated sum), consistent with
|
|
4369
|
-
* the session header. Tooltip shows the full breakdown plus a footnote
|
|
4370
|
-
* reminding that cache reads are cumulative. */
|
|
4371
|
-
function TurnTokenChip({
|
|
4372
|
-
usage,
|
|
4373
|
-
}: {
|
|
4374
|
-
usage: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
4375
|
-
}) {
|
|
4376
|
-
const [hover, setHover] = useState(false);
|
|
4377
|
-
const totalIn = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
4378
|
-
const pctRead = totalIn > 0 ? Math.round((usage.cacheRead / totalIn) * 100) : 0;
|
|
4379
|
-
return (
|
|
4380
|
-
<span
|
|
4381
|
-
style={{
|
|
4382
|
-
position: "relative",
|
|
4383
|
-
fontFamily: "var(--font-mono)",
|
|
4384
|
-
fontSize: 11,
|
|
4385
|
-
color: "var(--af-text-tertiary)",
|
|
4386
|
-
display: "inline-flex",
|
|
4387
|
-
alignItems: "center",
|
|
4388
|
-
gap: 4,
|
|
4389
|
-
whiteSpace: "nowrap",
|
|
4390
|
-
cursor: "default",
|
|
4391
|
-
}}
|
|
4392
|
-
onMouseEnter={() => setHover(true)}
|
|
4393
|
-
onMouseLeave={() => setHover(false)}
|
|
4394
|
-
>
|
|
4395
|
-
<span style={{ opacity: 0.6 }}>▤</span>
|
|
4396
|
-
{formatTokens(usage.input)} / {formatTokens(usage.output)}
|
|
4397
|
-
{hover && (
|
|
4398
|
-
<Tooltip style={{ right: 0, top: "calc(100% + 6px)", minWidth: 220 }}>
|
|
4399
|
-
<TooltipRow label="Input (fresh)" value={usage.input.toLocaleString()} />
|
|
4400
|
-
<TooltipRow label="Output" value={usage.output.toLocaleString()} />
|
|
4401
|
-
<TooltipRow
|
|
4402
|
-
label="Cache read"
|
|
4403
|
-
value={`${usage.cacheRead.toLocaleString()} (${pctRead}%)`}
|
|
4404
|
-
/>
|
|
4405
|
-
<TooltipRow label="Cache write" value={usage.cacheWrite.toLocaleString()} />
|
|
4406
|
-
<div
|
|
4407
|
-
style={{
|
|
4408
|
-
marginTop: 6,
|
|
4409
|
-
paddingTop: 6,
|
|
4410
|
-
borderTop: "1px solid rgba(241,245,249,0.12)",
|
|
4411
|
-
opacity: 0.65,
|
|
4412
|
-
fontSize: 10,
|
|
4413
|
-
whiteSpace: "normal",
|
|
4414
|
-
lineHeight: 1.4,
|
|
4415
|
-
}}
|
|
4416
|
-
>
|
|
4417
|
-
Cumulative across all requests in this turn.
|
|
4418
|
-
</div>
|
|
4419
|
-
</Tooltip>
|
|
4420
|
-
)}
|
|
4421
|
-
</span>
|
|
4422
|
-
);
|
|
4423
|
-
}
|
|
4492
|
+
// TokenChip, TurnTokenChip moved to ./session-view/token-stats.tsx
|
|
4424
4493
|
|
|
4425
4494
|
/* ------------------------------------------------------------------ */
|
|
4426
4495
|
/* Debug list */
|
|
4427
4496
|
/* ------------------------------------------------------------------ */
|
|
4428
4497
|
|
|
4429
|
-
|
|
4430
|
-
// Rebuild a JSON view from the structured fields on the event. The
|
|
4431
|
-
// original `raw` field is stripped server-side before serialization
|
|
4432
|
-
// (see app/sessions/[id]/page.tsx) because including the full JSONL
|
|
4433
|
-
// line per event doubles the RSC payload on large sessions. All the
|
|
4434
|
-
// useful data is already on the structured event; this view surfaces
|
|
4435
|
-
// it in the same shape you'd get from the raw JSONL.
|
|
4436
|
-
const shapeForDebug = (e: SessionEvent) => ({
|
|
4437
|
-
type: e.rawType,
|
|
4438
|
-
index: e.index,
|
|
4439
|
-
uuid: e.uuid,
|
|
4440
|
-
parentUuid: e.parentUuid,
|
|
4441
|
-
timestamp: e.timestamp,
|
|
4442
|
-
role: e.role,
|
|
4443
|
-
messageId: e.messageId,
|
|
4444
|
-
stopReason: e.stopReason,
|
|
4445
|
-
model: e.model,
|
|
4446
|
-
requestId: e.requestId,
|
|
4447
|
-
toolName: e.toolName,
|
|
4448
|
-
toolUseId: e.toolUseId,
|
|
4449
|
-
attachmentType: e.attachmentType,
|
|
4450
|
-
usage: e.usage,
|
|
4451
|
-
blocks: e.blocks,
|
|
4452
|
-
});
|
|
4453
|
-
|
|
4454
|
-
return (
|
|
4455
|
-
<div style={{ padding: "8px 0" }}>
|
|
4456
|
-
{events.map((e) => (
|
|
4457
|
-
<details
|
|
4458
|
-
key={e.index}
|
|
4459
|
-
style={{
|
|
4460
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4461
|
-
padding: "8px 12px",
|
|
4462
|
-
}}
|
|
4463
|
-
>
|
|
4464
|
-
<summary
|
|
4465
|
-
style={{
|
|
4466
|
-
cursor: "pointer",
|
|
4467
|
-
fontFamily: "var(--font-mono)",
|
|
4468
|
-
fontSize: 12,
|
|
4469
|
-
color: "var(--af-text-secondary)",
|
|
4470
|
-
}}
|
|
4471
|
-
>
|
|
4472
|
-
#{e.index} · {e.rawType}
|
|
4473
|
-
{e.attachmentType ? `/${e.attachmentType}` : ""} · {e.timestamp ?? "(no ts)"}
|
|
4474
|
-
</summary>
|
|
4475
|
-
<pre
|
|
4476
|
-
style={{
|
|
4477
|
-
marginTop: 8,
|
|
4478
|
-
padding: 12,
|
|
4479
|
-
background: "var(--background)",
|
|
4480
|
-
border: "1px solid var(--af-border-subtle)",
|
|
4481
|
-
borderRadius: 6,
|
|
4482
|
-
fontSize: 11,
|
|
4483
|
-
overflow: "auto",
|
|
4484
|
-
maxHeight: 400,
|
|
4485
|
-
color: "var(--af-text-secondary)",
|
|
4486
|
-
}}
|
|
4487
|
-
>
|
|
4488
|
-
{JSON.stringify(shapeForDebug(e), null, 2)}
|
|
4489
|
-
</pre>
|
|
4490
|
-
</details>
|
|
4491
|
-
))}
|
|
4492
|
-
</div>
|
|
4493
|
-
);
|
|
4494
|
-
}
|
|
4498
|
+
// DebugList moved to ./session-view/debug-list.tsx
|
|
4495
4499
|
|
|
4496
4500
|
/* ------------------------------------------------------------------ */
|
|
4497
4501
|
/* SubagentDrawer */
|
|
@@ -4504,412 +4508,98 @@ function DebugList({ events }: { events: SessionEvent[] }) {
|
|
|
4504
4508
|
/* this subagent off. */
|
|
4505
4509
|
/* ------------------------------------------------------------------ */
|
|
4506
4510
|
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
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,
|
|
4510
4528
|
onJumpToParent,
|
|
4529
|
+
onClearFocus,
|
|
4511
4530
|
}: {
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
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;
|
|
4515
4538
|
}) {
|
|
4516
|
-
const
|
|
4517
|
-
const
|
|
4518
|
-
const startOff = formatOffset(s.startTOffsetMs);
|
|
4519
|
-
const endOff = formatOffset(s.endTOffsetMs);
|
|
4520
|
-
const dur = s.durationMs !== undefined ? formatGap(s.durationMs) : "—";
|
|
4521
|
-
const totalIn = s.totalUsage.input + s.totalUsage.cacheRead + s.totalUsage.cacheWrite;
|
|
4522
|
-
const pctRead = totalIn > 0 ? Math.round((s.totalUsage.cacheRead / totalIn) * 100) : 0;
|
|
4523
|
-
|
|
4539
|
+
const totalTokens = workflows.reduce((n, w) => n + w.totalTokens, 0);
|
|
4540
|
+
const totalTools = workflows.reduce((n, w) => n + w.toolCallCount, 0);
|
|
4524
4541
|
return (
|
|
4525
|
-
<
|
|
4526
|
-
{
|
|
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
|
+
>
|
|
4527
4551
|
<div
|
|
4528
4552
|
style={{
|
|
4529
|
-
padding: "14px 20px",
|
|
4530
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4531
4553
|
display: "flex",
|
|
4532
4554
|
alignItems: "center",
|
|
4533
4555
|
gap: 10,
|
|
4534
|
-
background: "var(--af-surface)",
|
|
4535
|
-
position: "sticky",
|
|
4536
|
-
top: 0,
|
|
4537
|
-
zIndex: 1,
|
|
4538
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))",
|
|
4539
4560
|
}}
|
|
4540
4561
|
>
|
|
4541
4562
|
<span
|
|
4542
4563
|
style={{
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
background: fill,
|
|
4548
|
-
color: "#fff",
|
|
4549
|
-
}}
|
|
4550
|
-
>
|
|
4551
|
-
{s.agentType}
|
|
4552
|
-
</span>
|
|
4553
|
-
{s.runInBackground && (
|
|
4554
|
-
<span
|
|
4555
|
-
style={{
|
|
4556
|
-
fontSize: 9,
|
|
4557
|
-
fontWeight: 600,
|
|
4558
|
-
padding: "2px 8px",
|
|
4559
|
-
borderRadius: 4,
|
|
4560
|
-
background: "var(--af-warning-subtle)",
|
|
4561
|
-
color: "var(--af-warning)",
|
|
4562
|
-
textTransform: "uppercase",
|
|
4563
|
-
letterSpacing: "0.04em",
|
|
4564
|
-
}}
|
|
4565
|
-
>
|
|
4566
|
-
background
|
|
4567
|
-
</span>
|
|
4568
|
-
)}
|
|
4569
|
-
<span
|
|
4570
|
-
style={{
|
|
4571
|
-
fontSize: 14,
|
|
4564
|
+
display: "inline-flex",
|
|
4565
|
+
alignItems: "center",
|
|
4566
|
+
gap: 7,
|
|
4567
|
+
fontSize: 13,
|
|
4572
4568
|
fontWeight: 600,
|
|
4573
4569
|
color: "var(--af-text)",
|
|
4574
|
-
flex: 1,
|
|
4575
|
-
minWidth: 0,
|
|
4576
|
-
overflow: "hidden",
|
|
4577
|
-
textOverflow: "ellipsis",
|
|
4578
|
-
whiteSpace: "nowrap",
|
|
4579
4570
|
}}
|
|
4580
|
-
title={s.description}
|
|
4581
4571
|
>
|
|
4582
|
-
{
|
|
4572
|
+
<WorkflowIcon size={14} style={{ color: "#EA580C" }} />
|
|
4573
|
+
Workflows
|
|
4583
4574
|
</span>
|
|
4584
|
-
<
|
|
4585
|
-
onClick={onClose}
|
|
4575
|
+
<span
|
|
4586
4576
|
style={{
|
|
4587
|
-
|
|
4588
|
-
border: "none",
|
|
4589
|
-
cursor: "pointer",
|
|
4577
|
+
fontSize: 11.5,
|
|
4590
4578
|
color: "var(--af-text-tertiary)",
|
|
4591
|
-
|
|
4592
|
-
borderRadius: 4,
|
|
4579
|
+
fontFamily: "var(--font-mono)",
|
|
4593
4580
|
}}
|
|
4594
|
-
aria-label="Close"
|
|
4595
4581
|
>
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
{/* Meta strip */}
|
|
4601
|
-
<div
|
|
4602
|
-
style={{
|
|
4603
|
-
padding: "10px 20px",
|
|
4604
|
-
fontFamily: "var(--font-mono)",
|
|
4605
|
-
fontSize: 11,
|
|
4606
|
-
color: "var(--af-text-tertiary)",
|
|
4607
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4608
|
-
display: "grid",
|
|
4609
|
-
gridTemplateColumns: "auto 1fr",
|
|
4610
|
-
columnGap: 10,
|
|
4611
|
-
rowGap: 3,
|
|
4612
|
-
}}
|
|
4613
|
-
>
|
|
4614
|
-
<span style={{ opacity: 0.7 }}>range</span>
|
|
4615
|
-
<span style={{ color: "var(--af-text-secondary)" }}>
|
|
4616
|
-
{startOff} → {endOff}
|
|
4582
|
+
{workflows.length} run{workflows.length === 1 ? "" : "s"} ·{" "}
|
|
4583
|
+
{spawnedAgentCount.toLocaleString()} agents · {totalTools.toLocaleString()} tool calls ·{" "}
|
|
4584
|
+
{formatTokens(totalTokens)} tok orchestrated
|
|
4617
4585
|
</span>
|
|
4618
|
-
<span style={{ opacity: 0.7 }}>duration</span>
|
|
4619
|
-
<span style={{ color: "var(--af-text-secondary)" }}>{dur}</span>
|
|
4620
|
-
{s.model && (
|
|
4621
|
-
<>
|
|
4622
|
-
<span style={{ opacity: 0.7 }}>model</span>
|
|
4623
|
-
<span style={{ color: "var(--af-text-secondary)" }}>{s.model}</span>
|
|
4624
|
-
</>
|
|
4625
|
-
)}
|
|
4626
|
-
{s.parentToolUseId && (
|
|
4627
|
-
<>
|
|
4628
|
-
<span style={{ opacity: 0.7 }}>parent</span>
|
|
4629
|
-
<span style={{ color: "var(--af-text-secondary)" }}>
|
|
4630
|
-
{s.parentToolUseId.slice(0, 24)}…
|
|
4631
|
-
</span>
|
|
4632
|
-
</>
|
|
4633
|
-
)}
|
|
4634
4586
|
</div>
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
<StatCell label="Messages" value={String(s.assistantMessageCount ?? 0)} />
|
|
4648
|
-
<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
|
+
))}
|
|
4649
4599
|
</div>
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
style={{
|
|
4654
|
-
padding: "12px 20px",
|
|
4655
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4656
|
-
fontSize: 11,
|
|
4657
|
-
fontFamily: "var(--font-mono)",
|
|
4658
|
-
color: "var(--af-text-secondary)",
|
|
4659
|
-
display: "flex",
|
|
4660
|
-
flexDirection: "column",
|
|
4661
|
-
gap: 3,
|
|
4662
|
-
}}
|
|
4663
|
-
>
|
|
4664
|
-
<TokenLine label="Input (fresh)" value={s.totalUsage.input} />
|
|
4665
|
-
<TokenLine label="Output" value={s.totalUsage.output} />
|
|
4666
|
-
<TokenLine
|
|
4667
|
-
label="Cache read"
|
|
4668
|
-
value={s.totalUsage.cacheRead}
|
|
4669
|
-
suffix={` (${pctRead}%)`}
|
|
4670
|
-
/>
|
|
4671
|
-
<TokenLine label="Cache write" value={s.totalUsage.cacheWrite} />
|
|
4672
|
-
</div>
|
|
4673
|
-
|
|
4674
|
-
{/* Tool breakdown */}
|
|
4675
|
-
{s.toolCalls && s.toolCalls.length > 0 && (
|
|
4676
|
-
<div
|
|
4677
|
-
style={{
|
|
4678
|
-
padding: "14px 20px",
|
|
4679
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4680
|
-
}}
|
|
4681
|
-
>
|
|
4682
|
-
<div
|
|
4683
|
-
style={{
|
|
4684
|
-
fontSize: 10,
|
|
4685
|
-
fontWeight: 600,
|
|
4686
|
-
textTransform: "uppercase",
|
|
4687
|
-
letterSpacing: "0.04em",
|
|
4688
|
-
color: "var(--af-text-tertiary)",
|
|
4689
|
-
marginBottom: 8,
|
|
4690
|
-
}}
|
|
4691
|
-
>
|
|
4692
|
-
Tools used
|
|
4693
|
-
</div>
|
|
4694
|
-
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
|
4695
|
-
{s.toolCalls.map((t) => (
|
|
4696
|
-
<span
|
|
4697
|
-
key={t.name}
|
|
4698
|
-
style={{
|
|
4699
|
-
display: "inline-flex",
|
|
4700
|
-
alignItems: "center",
|
|
4701
|
-
gap: 4,
|
|
4702
|
-
fontSize: 11,
|
|
4703
|
-
padding: "2px 8px",
|
|
4704
|
-
borderRadius: 4,
|
|
4705
|
-
background: "var(--af-border-subtle)",
|
|
4706
|
-
color: "var(--af-text)",
|
|
4707
|
-
fontFamily: "var(--font-mono)",
|
|
4708
|
-
}}
|
|
4709
|
-
>
|
|
4710
|
-
<b style={{ fontWeight: 600 }}>{shortenToolName(t.name)}</b>
|
|
4711
|
-
<span style={{ color: "var(--af-text-tertiary)" }}>×{t.count}</span>
|
|
4712
|
-
</span>
|
|
4713
|
-
))}
|
|
4714
|
-
</div>
|
|
4715
|
-
</div>
|
|
4716
|
-
)}
|
|
4717
|
-
|
|
4718
|
-
{/* Parent prompt (what the parent asked the subagent to do) */}
|
|
4719
|
-
{s.prompt && (
|
|
4720
|
-
<div
|
|
4721
|
-
style={{
|
|
4722
|
-
padding: "14px 20px",
|
|
4723
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4724
|
-
}}
|
|
4725
|
-
>
|
|
4726
|
-
<div
|
|
4727
|
-
style={{
|
|
4728
|
-
display: "flex",
|
|
4729
|
-
alignItems: "center",
|
|
4730
|
-
justifyContent: "space-between",
|
|
4731
|
-
marginBottom: 8,
|
|
4732
|
-
}}
|
|
4733
|
-
>
|
|
4734
|
-
<div
|
|
4735
|
-
style={{
|
|
4736
|
-
fontSize: 10,
|
|
4737
|
-
fontWeight: 600,
|
|
4738
|
-
textTransform: "uppercase",
|
|
4739
|
-
letterSpacing: "0.04em",
|
|
4740
|
-
color: "var(--af-text-tertiary)",
|
|
4741
|
-
}}
|
|
4742
|
-
>
|
|
4743
|
-
Prompt
|
|
4744
|
-
</div>
|
|
4745
|
-
{s.parentToolUseId && (
|
|
4746
|
-
<button
|
|
4747
|
-
type="button"
|
|
4748
|
-
onClick={onJumpToParent}
|
|
4749
|
-
style={{
|
|
4750
|
-
fontSize: 10,
|
|
4751
|
-
color: "var(--af-accent)",
|
|
4752
|
-
background: "transparent",
|
|
4753
|
-
border: "none",
|
|
4754
|
-
cursor: "pointer",
|
|
4755
|
-
padding: 0,
|
|
4756
|
-
}}
|
|
4757
|
-
>
|
|
4758
|
-
Jump to parent →
|
|
4759
|
-
</button>
|
|
4760
|
-
)}
|
|
4761
|
-
</div>
|
|
4762
|
-
<div className="sl-prose" style={{ fontSize: 12.5 }}>
|
|
4763
|
-
<ReactMarkdown
|
|
4764
|
-
remarkPlugins={[remarkGfm]}
|
|
4765
|
-
components={{
|
|
4766
|
-
a: (props) => (
|
|
4767
|
-
<a
|
|
4768
|
-
{...props}
|
|
4769
|
-
target="_blank"
|
|
4770
|
-
rel="noopener noreferrer"
|
|
4771
|
-
style={{
|
|
4772
|
-
color: "var(--af-accent)",
|
|
4773
|
-
textDecoration: "underline",
|
|
4774
|
-
}}
|
|
4775
|
-
/>
|
|
4776
|
-
),
|
|
4777
|
-
}}
|
|
4778
|
-
>
|
|
4779
|
-
{s.prompt}
|
|
4780
|
-
</ReactMarkdown>
|
|
4781
|
-
</div>
|
|
4782
|
-
</div>
|
|
4783
|
-
)}
|
|
4784
|
-
|
|
4785
|
-
{/* Final text */}
|
|
4786
|
-
{s.finalText && (
|
|
4787
|
-
<div style={{ padding: "14px 20px" }}>
|
|
4788
|
-
<div
|
|
4789
|
-
style={{
|
|
4790
|
-
fontSize: 10,
|
|
4791
|
-
fontWeight: 600,
|
|
4792
|
-
textTransform: "uppercase",
|
|
4793
|
-
letterSpacing: "0.04em",
|
|
4794
|
-
color: "var(--af-text-tertiary)",
|
|
4795
|
-
marginBottom: 8,
|
|
4796
|
-
}}
|
|
4797
|
-
>
|
|
4798
|
-
Final result
|
|
4799
|
-
</div>
|
|
4800
|
-
<div className="sl-prose" style={{ fontSize: 13 }}>
|
|
4801
|
-
<ReactMarkdown
|
|
4802
|
-
remarkPlugins={[remarkGfm]}
|
|
4803
|
-
components={{
|
|
4804
|
-
a: (props) => (
|
|
4805
|
-
<a
|
|
4806
|
-
{...props}
|
|
4807
|
-
target="_blank"
|
|
4808
|
-
rel="noopener noreferrer"
|
|
4809
|
-
style={{
|
|
4810
|
-
color: "var(--af-accent)",
|
|
4811
|
-
textDecoration: "underline",
|
|
4812
|
-
}}
|
|
4813
|
-
/>
|
|
4814
|
-
),
|
|
4815
|
-
}}
|
|
4816
|
-
>
|
|
4817
|
-
{s.finalText}
|
|
4818
|
-
</ReactMarkdown>
|
|
4819
|
-
</div>
|
|
4820
|
-
</div>
|
|
4821
|
-
)}
|
|
4822
|
-
</div>
|
|
4823
|
-
);
|
|
4824
|
-
}
|
|
4825
|
-
|
|
4826
|
-
/* ------------------------------------------------------------------ */
|
|
4827
|
-
/* Workflows panel */
|
|
4828
|
-
/* */
|
|
4829
|
-
/* A single `Workflow` tool call collapses a whole dynamic-workflow */
|
|
4830
|
-
/* fan-out into one transcript row. This panel surfaces what that row */
|
|
4831
|
-
/* actually did: each run's spawned-agent count, tool calls, tokens, */
|
|
4832
|
-
/* phases, and progress log — the fleet work the transcript hides. */
|
|
4833
|
-
/* ------------------------------------------------------------------ */
|
|
4834
|
-
|
|
4835
|
-
function WorkflowsPanel({
|
|
4836
|
-
workflows,
|
|
4837
|
-
spawnedAgentCount,
|
|
4838
|
-
focusRunId,
|
|
4839
|
-
selectedAgentKey,
|
|
4840
|
-
onOpenAgent,
|
|
4841
|
-
onJumpToParent,
|
|
4842
|
-
}: {
|
|
4843
|
-
workflows: WorkflowRun[];
|
|
4844
|
-
spawnedAgentCount: number;
|
|
4845
|
-
focusRunId?: string | null;
|
|
4846
|
-
selectedAgentKey?: string | null;
|
|
4847
|
-
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
4848
|
-
onJumpToParent: (toolUseId?: string) => void;
|
|
4849
|
-
}) {
|
|
4850
|
-
const totalTokens = workflows.reduce((n, w) => n + w.totalTokens, 0);
|
|
4851
|
-
const totalTools = workflows.reduce((n, w) => n + w.toolCallCount, 0);
|
|
4852
|
-
return (
|
|
4853
|
-
<section
|
|
4854
|
-
style={{
|
|
4855
|
-
marginBottom: 16,
|
|
4856
|
-
border: "1px solid var(--af-border-subtle)",
|
|
4857
|
-
borderRadius: 10,
|
|
4858
|
-
background: "var(--af-surface)",
|
|
4859
|
-
overflow: "hidden",
|
|
4860
|
-
}}
|
|
4861
|
-
>
|
|
4862
|
-
<div
|
|
4863
|
-
style={{
|
|
4864
|
-
display: "flex",
|
|
4865
|
-
alignItems: "center",
|
|
4866
|
-
gap: 10,
|
|
4867
|
-
flexWrap: "wrap",
|
|
4868
|
-
padding: "10px 14px",
|
|
4869
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
4870
|
-
background: "var(--af-surface-subtle, rgba(234,88,12,0.04))",
|
|
4871
|
-
}}
|
|
4872
|
-
>
|
|
4873
|
-
<span
|
|
4874
|
-
style={{
|
|
4875
|
-
display: "inline-flex",
|
|
4876
|
-
alignItems: "center",
|
|
4877
|
-
gap: 7,
|
|
4878
|
-
fontSize: 13,
|
|
4879
|
-
fontWeight: 600,
|
|
4880
|
-
color: "var(--af-text)",
|
|
4881
|
-
}}
|
|
4882
|
-
>
|
|
4883
|
-
<WorkflowIcon size={14} style={{ color: "#EA580C" }} />
|
|
4884
|
-
Workflows
|
|
4885
|
-
</span>
|
|
4886
|
-
<span
|
|
4887
|
-
style={{
|
|
4888
|
-
fontSize: 11.5,
|
|
4889
|
-
color: "var(--af-text-tertiary)",
|
|
4890
|
-
fontFamily: "var(--font-mono)",
|
|
4891
|
-
}}
|
|
4892
|
-
>
|
|
4893
|
-
{workflows.length} run{workflows.length === 1 ? "" : "s"} ·{" "}
|
|
4894
|
-
{spawnedAgentCount.toLocaleString()} agents · {totalTools.toLocaleString()} tool calls ·{" "}
|
|
4895
|
-
{formatTokens(totalTokens)} tok orchestrated
|
|
4896
|
-
</span>
|
|
4897
|
-
</div>
|
|
4898
|
-
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
4899
|
-
{workflows.map((w) => (
|
|
4900
|
-
<WorkflowCard
|
|
4901
|
-
key={w.runId}
|
|
4902
|
-
w={w}
|
|
4903
|
-
focused={focusRunId === w.runId}
|
|
4904
|
-
selectedAgentKey={selectedAgentKey}
|
|
4905
|
-
onOpenAgent={onOpenAgent}
|
|
4906
|
-
onJumpToParent={onJumpToParent}
|
|
4907
|
-
/>
|
|
4908
|
-
))}
|
|
4909
|
-
</div>
|
|
4910
|
-
</section>
|
|
4911
|
-
);
|
|
4912
|
-
}
|
|
4600
|
+
</section>
|
|
4601
|
+
);
|
|
4602
|
+
}
|
|
4913
4603
|
|
|
4914
4604
|
function WorkflowCard({
|
|
4915
4605
|
w,
|
|
@@ -4917,12 +4607,14 @@ function WorkflowCard({
|
|
|
4917
4607
|
selectedAgentKey,
|
|
4918
4608
|
onOpenAgent,
|
|
4919
4609
|
onJumpToParent,
|
|
4610
|
+
onClearFocus,
|
|
4920
4611
|
}: {
|
|
4921
4612
|
w: WorkflowRun;
|
|
4922
4613
|
focused?: boolean;
|
|
4923
4614
|
selectedAgentKey?: string | null;
|
|
4924
4615
|
onOpenAgent: (runId: string, agent: WorkflowRun["agents"][number]) => void;
|
|
4925
4616
|
onJumpToParent: (toolUseId?: string) => void;
|
|
4617
|
+
onClearFocus?: () => void;
|
|
4926
4618
|
}) {
|
|
4927
4619
|
const [open, setOpen] = useState(focused);
|
|
4928
4620
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
@@ -4942,14 +4634,17 @@ function WorkflowCard({
|
|
|
4942
4634
|
ref={cardRef}
|
|
4943
4635
|
style={{
|
|
4944
4636
|
borderTop: "1px solid var(--af-border-subtle)",
|
|
4945
|
-
background: focused ? "rgba(234,88,12,0.07)" : "transparent",
|
|
4946
|
-
transition: "background 0.5s ease",
|
|
4947
4637
|
scrollMarginTop: 120,
|
|
4948
4638
|
}}
|
|
4949
4639
|
>
|
|
4950
4640
|
{/* Collapsed header — click to expand */}
|
|
4951
4641
|
<button
|
|
4952
|
-
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
|
+
}}
|
|
4953
4648
|
style={{
|
|
4954
4649
|
width: "100%",
|
|
4955
4650
|
display: "flex",
|
|
@@ -4972,7 +4667,11 @@ function WorkflowCard({
|
|
|
4972
4667
|
fontFamily: "var(--font-mono)",
|
|
4973
4668
|
fontSize: 13,
|
|
4974
4669
|
fontWeight: 600,
|
|
4975
|
-
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",
|
|
4976
4675
|
minWidth: 0,
|
|
4977
4676
|
overflow: "hidden",
|
|
4978
4677
|
textOverflow: "ellipsis",
|
|
@@ -5054,7 +4753,7 @@ function WorkflowCard({
|
|
|
5054
4753
|
fontFamily: "var(--font-mono)",
|
|
5055
4754
|
}}
|
|
5056
4755
|
>
|
|
5057
|
-
Jump to
|
|
4756
|
+
Jump to timeline →
|
|
5058
4757
|
</button>
|
|
5059
4758
|
)}
|
|
5060
4759
|
</div>
|
|
@@ -5705,593 +5404,18 @@ function WorkflowRunLog({ logs }: { logs: string[] }) {
|
|
|
5705
5404
|
);
|
|
5706
5405
|
}
|
|
5707
5406
|
|
|
5708
|
-
|
|
5709
|
-
return (
|
|
5710
|
-
<span
|
|
5711
|
-
style={{
|
|
5712
|
-
display: "inline-flex",
|
|
5713
|
-
alignItems: "center",
|
|
5714
|
-
gap: 4,
|
|
5715
|
-
fontSize: 11.5,
|
|
5716
|
-
color: "var(--af-text-secondary)",
|
|
5717
|
-
fontFamily: "var(--font-mono)",
|
|
5718
|
-
whiteSpace: "nowrap",
|
|
5719
|
-
}}
|
|
5720
|
-
>
|
|
5721
|
-
{icon && <span style={{ color: "var(--af-text-tertiary)", display: "inline-flex" }}>{icon}</span>}
|
|
5722
|
-
{value}
|
|
5723
|
-
</span>
|
|
5724
|
-
);
|
|
5725
|
-
}
|
|
5726
|
-
|
|
5727
|
-
function SectionLabel({ children }: { children: React.ReactNode }) {
|
|
5728
|
-
return (
|
|
5729
|
-
<div
|
|
5730
|
-
style={{
|
|
5731
|
-
fontSize: 10,
|
|
5732
|
-
fontWeight: 600,
|
|
5733
|
-
textTransform: "uppercase",
|
|
5734
|
-
letterSpacing: "0.04em",
|
|
5735
|
-
color: "var(--af-text-tertiary)",
|
|
5736
|
-
marginBottom: 8,
|
|
5737
|
-
}}
|
|
5738
|
-
>
|
|
5739
|
-
{children}
|
|
5740
|
-
</div>
|
|
5741
|
-
);
|
|
5742
|
-
}
|
|
5743
|
-
|
|
5744
|
-
function StatCell({ label, value }: { label: string; value: string }) {
|
|
5745
|
-
return (
|
|
5746
|
-
<div>
|
|
5747
|
-
<div
|
|
5748
|
-
style={{
|
|
5749
|
-
fontSize: 9,
|
|
5750
|
-
color: "var(--af-text-tertiary)",
|
|
5751
|
-
textTransform: "uppercase",
|
|
5752
|
-
letterSpacing: "0.04em",
|
|
5753
|
-
fontWeight: 600,
|
|
5754
|
-
}}
|
|
5755
|
-
>
|
|
5756
|
-
{label}
|
|
5757
|
-
</div>
|
|
5758
|
-
<div
|
|
5759
|
-
style={{
|
|
5760
|
-
fontSize: 18,
|
|
5761
|
-
fontWeight: 700,
|
|
5762
|
-
fontFamily: "var(--font-mono)",
|
|
5763
|
-
color: "var(--af-text)",
|
|
5764
|
-
marginTop: 2,
|
|
5765
|
-
}}
|
|
5766
|
-
>
|
|
5767
|
-
{value}
|
|
5768
|
-
</div>
|
|
5769
|
-
</div>
|
|
5770
|
-
);
|
|
5771
|
-
}
|
|
5772
|
-
|
|
5773
|
-
function TokenLine({
|
|
5774
|
-
label,
|
|
5775
|
-
value,
|
|
5776
|
-
suffix,
|
|
5777
|
-
}: {
|
|
5778
|
-
label: string;
|
|
5779
|
-
value: number;
|
|
5780
|
-
suffix?: string;
|
|
5781
|
-
}) {
|
|
5782
|
-
return (
|
|
5783
|
-
<div style={{ display: "flex", justifyContent: "space-between", gap: 12 }}>
|
|
5784
|
-
<span style={{ opacity: 0.7 }}>{label}</span>
|
|
5785
|
-
<span>
|
|
5786
|
-
{value.toLocaleString()}
|
|
5787
|
-
{suffix && <span style={{ opacity: 0.65 }}>{suffix}</span>}
|
|
5788
|
-
</span>
|
|
5789
|
-
</div>
|
|
5790
|
-
);
|
|
5791
|
-
}
|
|
5407
|
+
// WfMiniStat, SectionLabel, StatCell, TokenLine moved to ./session-view/workflows-shared.tsx
|
|
5792
5408
|
|
|
5793
5409
|
/* ------------------------------------------------------------------ */
|
|
5794
5410
|
/* Drawer */
|
|
5795
5411
|
/* ------------------------------------------------------------------ */
|
|
5796
5412
|
|
|
5797
|
-
|
|
5798
|
-
event,
|
|
5799
|
-
row,
|
|
5800
|
-
onClose,
|
|
5801
|
-
}: {
|
|
5802
|
-
event: SessionEvent;
|
|
5803
|
-
row: PresentationRow | null;
|
|
5804
|
-
onClose: () => void;
|
|
5805
|
-
}) {
|
|
5806
|
-
const [showDev, setShowDev] = useState(false);
|
|
5807
|
-
const kind: PresentationRowKind = row?.kind ?? "agent";
|
|
5808
|
-
const theme = ROLE_THEMES[kind];
|
|
5809
|
-
const title = drawerTitle(row, event);
|
|
5810
|
-
const hasUsage = !!event.usage && (event.usage.input > 0 || event.usage.output > 0);
|
|
5811
|
-
|
|
5812
|
-
return (
|
|
5813
|
-
<div>
|
|
5814
|
-
{/* Sticky title bar */}
|
|
5815
|
-
<div
|
|
5816
|
-
style={{
|
|
5817
|
-
padding: "14px 20px",
|
|
5818
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
5819
|
-
display: "flex",
|
|
5820
|
-
alignItems: "center",
|
|
5821
|
-
gap: 10,
|
|
5822
|
-
background: "var(--af-surface)",
|
|
5823
|
-
position: "sticky",
|
|
5824
|
-
top: 0,
|
|
5825
|
-
zIndex: 1,
|
|
5826
|
-
}}
|
|
5827
|
-
>
|
|
5828
|
-
<span
|
|
5829
|
-
style={{
|
|
5830
|
-
fontSize: 11,
|
|
5831
|
-
fontWeight: 600,
|
|
5832
|
-
padding: "3px 10px",
|
|
5833
|
-
borderRadius: 4,
|
|
5834
|
-
background: theme.bg,
|
|
5835
|
-
color: theme.fg,
|
|
5836
|
-
}}
|
|
5837
|
-
>
|
|
5838
|
-
{theme.label}
|
|
5839
|
-
</span>
|
|
5840
|
-
<span
|
|
5841
|
-
style={{
|
|
5842
|
-
fontSize: 14,
|
|
5843
|
-
fontWeight: 600,
|
|
5844
|
-
color: "var(--af-text)",
|
|
5845
|
-
}}
|
|
5846
|
-
>
|
|
5847
|
-
{title}
|
|
5848
|
-
</span>
|
|
5849
|
-
<button
|
|
5850
|
-
onClick={onClose}
|
|
5851
|
-
style={{
|
|
5852
|
-
marginLeft: "auto",
|
|
5853
|
-
background: "transparent",
|
|
5854
|
-
border: "none",
|
|
5855
|
-
cursor: "pointer",
|
|
5856
|
-
color: "var(--af-text-tertiary)",
|
|
5857
|
-
padding: 4,
|
|
5858
|
-
borderRadius: 4,
|
|
5859
|
-
}}
|
|
5860
|
-
>
|
|
5861
|
-
<X size={16} />
|
|
5862
|
-
</button>
|
|
5863
|
-
</div>
|
|
5864
|
-
|
|
5865
|
-
{/* Meta line — compact */}
|
|
5866
|
-
<div
|
|
5867
|
-
style={{
|
|
5868
|
-
padding: "8px 20px",
|
|
5869
|
-
fontFamily: "var(--font-mono)",
|
|
5870
|
-
fontSize: 11,
|
|
5871
|
-
color: "var(--af-text-tertiary)",
|
|
5872
|
-
display: "flex",
|
|
5873
|
-
gap: 10,
|
|
5874
|
-
alignItems: "center",
|
|
5875
|
-
flexWrap: "wrap",
|
|
5876
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
5877
|
-
}}
|
|
5878
|
-
>
|
|
5879
|
-
<span>{formatOffset(event.tOffsetMs)}</span>
|
|
5880
|
-
{event.gapMs !== undefined && event.gapMs > 0 && <span>· {formatGap(event.gapMs)}</span>}
|
|
5881
|
-
{hasUsage && event.usage && (
|
|
5882
|
-
<span style={{ color: "var(--af-text-secondary)" }}>
|
|
5883
|
-
· {formatTokens(event.usage.input + event.usage.cacheRead + event.usage.cacheWrite)}/
|
|
5884
|
-
{formatTokens(event.usage.output)} tokens
|
|
5885
|
-
</span>
|
|
5886
|
-
)}
|
|
5887
|
-
<button
|
|
5888
|
-
onClick={() => setShowDev((s) => !s)}
|
|
5889
|
-
style={{
|
|
5890
|
-
marginLeft: "auto",
|
|
5891
|
-
background: "transparent",
|
|
5892
|
-
border: "none",
|
|
5893
|
-
fontSize: 10,
|
|
5894
|
-
color: "var(--af-text-tertiary)",
|
|
5895
|
-
cursor: "pointer",
|
|
5896
|
-
padding: 0,
|
|
5897
|
-
fontFamily: "inherit",
|
|
5898
|
-
textDecoration: "underline",
|
|
5899
|
-
textUnderlineOffset: 2,
|
|
5900
|
-
}}
|
|
5901
|
-
>
|
|
5902
|
-
{showDev ? "hide details" : "details"}
|
|
5903
|
-
</button>
|
|
5904
|
-
</div>
|
|
5905
|
-
|
|
5906
|
-
{/* Developer panel — collapsed by default */}
|
|
5907
|
-
{showDev && (
|
|
5908
|
-
<div
|
|
5909
|
-
style={{
|
|
5910
|
-
padding: "10px 20px 14px",
|
|
5911
|
-
borderBottom: "1px solid var(--af-border-subtle)",
|
|
5912
|
-
fontSize: 11,
|
|
5913
|
-
color: "var(--af-text-secondary)",
|
|
5914
|
-
fontFamily: "var(--font-mono)",
|
|
5915
|
-
display: "flex",
|
|
5916
|
-
flexDirection: "column",
|
|
5917
|
-
gap: 4,
|
|
5918
|
-
}}
|
|
5919
|
-
>
|
|
5920
|
-
{event.model && <div>model: {event.model}</div>}
|
|
5921
|
-
{event.requestId && <div>request: {event.requestId}</div>}
|
|
5922
|
-
{event.messageId && <div>message: {event.messageId}</div>}
|
|
5923
|
-
{event.stopReason && <div>stop_reason: {event.stopReason}</div>}
|
|
5924
|
-
{event.usage && (
|
|
5925
|
-
<>
|
|
5926
|
-
<div style={{ marginTop: 6, opacity: 0.7 }}>tokens</div>
|
|
5927
|
-
<div> input: {event.usage.input.toLocaleString()}</div>
|
|
5928
|
-
<div> output: {event.usage.output.toLocaleString()}</div>
|
|
5929
|
-
<div> cache read: {event.usage.cacheRead.toLocaleString()}</div>
|
|
5930
|
-
<div>
|
|
5931
|
-
{" "}cache write: {event.usage.cacheWrite.toLocaleString()}
|
|
5932
|
-
</div>
|
|
5933
|
-
</>
|
|
5934
|
-
)}
|
|
5935
|
-
</div>
|
|
5936
|
-
)}
|
|
5937
|
-
|
|
5938
|
-
{/* Content */}
|
|
5939
|
-
<div style={{ padding: "18px 22px" }}>
|
|
5940
|
-
<DrawerContent event={event} row={row} />
|
|
5941
|
-
</div>
|
|
5942
|
-
</div>
|
|
5943
|
-
);
|
|
5944
|
-
}
|
|
5945
|
-
|
|
5946
|
-
function drawerTitle(row: PresentationRow | null, event: SessionEvent): string {
|
|
5947
|
-
if (row) {
|
|
5948
|
-
switch (row.kind) {
|
|
5949
|
-
case "user":
|
|
5950
|
-
return "Message";
|
|
5951
|
-
case "agent":
|
|
5952
|
-
return "Message";
|
|
5953
|
-
case "tool-group":
|
|
5954
|
-
return `Tool use · ${formatToolSummary(row.toolNames)}`;
|
|
5955
|
-
case "interrupt":
|
|
5956
|
-
return "Interrupted";
|
|
5957
|
-
case "model":
|
|
5958
|
-
return "Model (zero-usage)";
|
|
5959
|
-
case "error":
|
|
5960
|
-
return "API error";
|
|
5961
|
-
case "task-notification":
|
|
5962
|
-
return `Background task · ${row.status}`;
|
|
5963
|
-
}
|
|
5964
|
-
}
|
|
5965
|
-
return event.rawType;
|
|
5966
|
-
}
|
|
5967
|
-
|
|
5968
|
-
function DrawerContent({ event, row }: { event: SessionEvent; row: PresentationRow | null }) {
|
|
5969
|
-
// User rows can override their blocks (e.g. slash commands get a cleaned
|
|
5970
|
-
// "/implement AGE-8" block instead of the raw XML).
|
|
5971
|
-
if (row?.kind === "user" && row.displayBlocks) {
|
|
5972
|
-
return (
|
|
5973
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
5974
|
-
{row.displayBlocks.map((b, i) => (
|
|
5975
|
-
<BlockView key={i} block={b} />
|
|
5976
|
-
))}
|
|
5977
|
-
</div>
|
|
5978
|
-
);
|
|
5979
|
-
}
|
|
5980
|
-
|
|
5981
|
-
// Task notifications: parsed fields in a clean key-value layout,
|
|
5982
|
-
// not the raw <task-notification> XML blob.
|
|
5983
|
-
if (row?.kind === "task-notification") {
|
|
5984
|
-
const statusColor =
|
|
5985
|
-
row.status === "success"
|
|
5986
|
-
? "var(--af-success)"
|
|
5987
|
-
: row.status === "failed"
|
|
5988
|
-
? "var(--af-danger)"
|
|
5989
|
-
: "var(--af-text-secondary)";
|
|
5990
|
-
return (
|
|
5991
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
5992
|
-
<div
|
|
5993
|
-
style={{
|
|
5994
|
-
display: "flex",
|
|
5995
|
-
alignItems: "center",
|
|
5996
|
-
gap: 8,
|
|
5997
|
-
}}
|
|
5998
|
-
>
|
|
5999
|
-
<span
|
|
6000
|
-
style={{
|
|
6001
|
-
fontSize: 11,
|
|
6002
|
-
fontWeight: 600,
|
|
6003
|
-
padding: "3px 10px",
|
|
6004
|
-
borderRadius: 4,
|
|
6005
|
-
background:
|
|
6006
|
-
row.status === "success"
|
|
6007
|
-
? "var(--af-success-subtle)"
|
|
6008
|
-
: row.status === "failed"
|
|
6009
|
-
? "var(--af-danger-subtle)"
|
|
6010
|
-
: "var(--af-border-subtle)",
|
|
6011
|
-
color: statusColor,
|
|
6012
|
-
textTransform: "uppercase",
|
|
6013
|
-
letterSpacing: "0.03em",
|
|
6014
|
-
}}
|
|
6015
|
-
>
|
|
6016
|
-
{row.status}
|
|
6017
|
-
</span>
|
|
6018
|
-
</div>
|
|
6019
|
-
<div style={{ fontSize: 13, lineHeight: 1.55, color: "var(--af-text)" }}>{row.summary}</div>
|
|
6020
|
-
{(row.taskId || row.toolUseId || row.outputFile) && (
|
|
6021
|
-
<div
|
|
6022
|
-
style={{
|
|
6023
|
-
fontSize: 11,
|
|
6024
|
-
color: "var(--af-text-secondary)",
|
|
6025
|
-
fontFamily: "var(--font-mono)",
|
|
6026
|
-
display: "flex",
|
|
6027
|
-
flexDirection: "column",
|
|
6028
|
-
gap: 4,
|
|
6029
|
-
paddingTop: 10,
|
|
6030
|
-
borderTop: "1px solid var(--af-border-subtle)",
|
|
6031
|
-
}}
|
|
6032
|
-
>
|
|
6033
|
-
{row.taskId && <div>task id: {row.taskId}</div>}
|
|
6034
|
-
{row.toolUseId && <div>tool use: {row.toolUseId.slice(0, 24)}…</div>}
|
|
6035
|
-
{row.outputFile && (
|
|
6036
|
-
<div style={{ wordBreak: "break-all" }}>output: {row.outputFile}</div>
|
|
6037
|
-
)}
|
|
6038
|
-
</div>
|
|
6039
|
-
)}
|
|
6040
|
-
</div>
|
|
6041
|
-
);
|
|
6042
|
-
}
|
|
6043
|
-
|
|
6044
|
-
// Tool group: list all individual tool calls with their inputs
|
|
6045
|
-
if (row?.kind === "tool-group" && row.count > 1) {
|
|
6046
|
-
return (
|
|
6047
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
6048
|
-
{row.events.map((e, i) => (
|
|
6049
|
-
<div key={e.index}>
|
|
6050
|
-
<div
|
|
6051
|
-
style={{
|
|
6052
|
-
fontSize: 11,
|
|
6053
|
-
color: "var(--af-text-tertiary)",
|
|
6054
|
-
marginBottom: 4,
|
|
6055
|
-
fontFamily: "var(--font-mono)",
|
|
6056
|
-
}}
|
|
6057
|
-
>
|
|
6058
|
-
#{i + 1} · {formatOffset(e.tOffsetMs)} · {e.toolUseId?.slice(0, 14)}…
|
|
6059
|
-
</div>
|
|
6060
|
-
{e.blocks.map((b, bi) => (
|
|
6061
|
-
<BlockView key={bi} block={b} />
|
|
6062
|
-
))}
|
|
6063
|
-
</div>
|
|
6064
|
-
))}
|
|
6065
|
-
</div>
|
|
6066
|
-
);
|
|
6067
|
-
}
|
|
6068
|
-
|
|
6069
|
-
// Agent row: include thinking blocks from the same message.id if present
|
|
6070
|
-
if (row?.kind === "agent" && row.groupedEvents.length > 1) {
|
|
6071
|
-
return (
|
|
6072
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
6073
|
-
{row.groupedEvents.flatMap((e, i) =>
|
|
6074
|
-
e.blocks.map((b, bi) => <BlockView key={`${i}-${bi}`} block={b} />),
|
|
6075
|
-
)}
|
|
6076
|
-
</div>
|
|
6077
|
-
);
|
|
6078
|
-
}
|
|
6079
|
-
|
|
6080
|
-
return (
|
|
6081
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
6082
|
-
{event.blocks.map((b, i) => (
|
|
6083
|
-
<BlockView key={i} block={b} />
|
|
6084
|
-
))}
|
|
6085
|
-
</div>
|
|
6086
|
-
);
|
|
6087
|
-
}
|
|
6088
|
-
function BlockView({ block }: { block: ContentBlock }) {
|
|
6089
|
-
if (block.type === "text") {
|
|
6090
|
-
return (
|
|
6091
|
-
<div className="sl-prose">
|
|
6092
|
-
<ReactMarkdown
|
|
6093
|
-
remarkPlugins={[remarkGfm]}
|
|
6094
|
-
components={{
|
|
6095
|
-
a: (props) => (
|
|
6096
|
-
<a
|
|
6097
|
-
{...props}
|
|
6098
|
-
target="_blank"
|
|
6099
|
-
rel="noopener noreferrer"
|
|
6100
|
-
style={{
|
|
6101
|
-
color: "var(--af-accent)",
|
|
6102
|
-
textDecoration: "underline",
|
|
6103
|
-
textUnderlineOffset: 2,
|
|
6104
|
-
}}
|
|
6105
|
-
/>
|
|
6106
|
-
),
|
|
6107
|
-
}}
|
|
6108
|
-
>
|
|
6109
|
-
{block.text}
|
|
6110
|
-
</ReactMarkdown>
|
|
6111
|
-
</div>
|
|
6112
|
-
);
|
|
6113
|
-
}
|
|
6114
|
-
if (block.type === "thinking") {
|
|
6115
|
-
return (
|
|
6116
|
-
<details>
|
|
6117
|
-
<summary
|
|
6118
|
-
style={{
|
|
6119
|
-
cursor: "pointer",
|
|
6120
|
-
fontSize: 11,
|
|
6121
|
-
color: "var(--af-text-tertiary)",
|
|
6122
|
-
marginBottom: 6,
|
|
6123
|
-
}}
|
|
6124
|
-
>
|
|
6125
|
-
Thinking · {block.thinking.length} chars
|
|
6126
|
-
</summary>
|
|
6127
|
-
<div
|
|
6128
|
-
style={{
|
|
6129
|
-
fontSize: 12,
|
|
6130
|
-
lineHeight: 1.6,
|
|
6131
|
-
color: "var(--af-text-secondary)",
|
|
6132
|
-
whiteSpace: "pre-wrap",
|
|
6133
|
-
fontStyle: "italic",
|
|
6134
|
-
borderLeft: "3px solid var(--af-border-subtle)",
|
|
6135
|
-
paddingLeft: 12,
|
|
6136
|
-
marginTop: 6,
|
|
6137
|
-
}}
|
|
6138
|
-
>
|
|
6139
|
-
{block.thinking}
|
|
6140
|
-
</div>
|
|
6141
|
-
</details>
|
|
6142
|
-
);
|
|
6143
|
-
}
|
|
6144
|
-
if (block.type === "tool_use") {
|
|
6145
|
-
return <ToolUseCard name={block.name} input={block.input} />;
|
|
6146
|
-
}
|
|
6147
|
-
if (block.type === "tool_result") {
|
|
6148
|
-
const text =
|
|
6149
|
-
typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
|
|
6150
|
-
return (
|
|
6151
|
-
<pre
|
|
6152
|
-
style={{
|
|
6153
|
-
fontSize: 12,
|
|
6154
|
-
padding: 12,
|
|
6155
|
-
background: "var(--background)",
|
|
6156
|
-
border: "1px solid var(--af-border-subtle)",
|
|
6157
|
-
borderRadius: 6,
|
|
6158
|
-
overflow: "auto",
|
|
6159
|
-
whiteSpace: "pre-wrap",
|
|
6160
|
-
color: "var(--af-text)",
|
|
6161
|
-
fontFamily: "var(--font-mono)",
|
|
6162
|
-
maxHeight: 500,
|
|
6163
|
-
}}
|
|
6164
|
-
>
|
|
6165
|
-
{text}
|
|
6166
|
-
</pre>
|
|
6167
|
-
);
|
|
6168
|
-
}
|
|
6169
|
-
return null;
|
|
6170
|
-
}
|
|
5413
|
+
// DrawerContent, BlockView moved to ./session-view/drawer.tsx
|
|
6171
5414
|
|
|
6172
5415
|
/* ------------------------------------------------------------------ */
|
|
6173
5416
|
/* Helpers */
|
|
6174
5417
|
/* ------------------------------------------------------------------ */
|
|
6175
5418
|
|
|
6176
|
-
|
|
6177
|
-
if (ms < 1000) return `${ms}ms`;
|
|
6178
|
-
const s = Math.round(ms / 1000);
|
|
6179
|
-
if (s < 60) return `${s}s`;
|
|
6180
|
-
const m = Math.floor(s / 60);
|
|
6181
|
-
if (m < 60) return `${m}m ${s % 60}s`;
|
|
6182
|
-
// Roll up to hours — agent time can reach many hours once workflow
|
|
6183
|
-
// execution is folded in, and "1116m" reads poorly.
|
|
6184
|
-
const h = Math.floor(m / 60);
|
|
6185
|
-
return `${h}h ${m % 60}m`;
|
|
6186
|
-
}
|
|
6187
|
-
|
|
6188
|
-
function EntryDayStrip({ entry }: { entry: Entry }) {
|
|
6189
|
-
const fmtDate = new Date(`${entry.local_day}T12:00:00`).toLocaleDateString("en-US", {
|
|
6190
|
-
weekday: "short", month: "short", day: "numeric",
|
|
6191
|
-
});
|
|
6192
|
-
const durMin = Math.round(entry.numbers.active_min);
|
|
6193
|
-
const enr = entry.enrichment;
|
|
6194
|
-
const isPending = enr.status === "pending" || enr.status === "error";
|
|
6195
|
-
const isTrivial = enr.status === "skipped_trivial";
|
|
6196
|
-
|
|
6197
|
-
return (
|
|
6198
|
-
<div
|
|
6199
|
-
style={{
|
|
6200
|
-
border: "1px solid var(--af-border-subtle)",
|
|
6201
|
-
borderRadius: 10,
|
|
6202
|
-
padding: "12px 16px",
|
|
6203
|
-
background: "var(--af-surface)",
|
|
6204
|
-
fontSize: 13,
|
|
6205
|
-
lineHeight: 1.5,
|
|
6206
|
-
}}
|
|
6207
|
-
>
|
|
6208
|
-
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
|
|
6209
|
-
<span
|
|
6210
|
-
style={{
|
|
6211
|
-
fontSize: 12,
|
|
6212
|
-
color: "var(--af-text-tertiary)",
|
|
6213
|
-
display: "inline-flex",
|
|
6214
|
-
alignItems: "center",
|
|
6215
|
-
gap: 4,
|
|
6216
|
-
}}
|
|
6217
|
-
>
|
|
6218
|
-
📅 {fmtDate}
|
|
6219
|
-
</span>
|
|
6220
|
-
{isTrivial ? (
|
|
6221
|
-
<OutcomePill outcome="trivial" size="md" agent={entry.agent} />
|
|
6222
|
-
) : isPending ? (
|
|
6223
|
-
<OutcomePill
|
|
6224
|
-
outcome={null}
|
|
6225
|
-
pending
|
|
6226
|
-
sessionId={entry.session_id}
|
|
6227
|
-
localDay={entry.local_day}
|
|
6228
|
-
size="md"
|
|
6229
|
-
agent={entry.agent}
|
|
6230
|
-
/>
|
|
6231
|
-
) : enr.outcome ? (
|
|
6232
|
-
<OutcomePill outcome={enr.outcome} size="md" agent={entry.agent} />
|
|
6233
|
-
) : null}
|
|
6234
|
-
<span
|
|
6235
|
-
style={{
|
|
6236
|
-
fontFamily: "var(--font-mono)",
|
|
6237
|
-
fontSize: 11,
|
|
6238
|
-
color: "var(--af-text-tertiary)",
|
|
6239
|
-
marginLeft: "auto",
|
|
6240
|
-
}}
|
|
6241
|
-
>
|
|
6242
|
-
{durMin}m active
|
|
6243
|
-
</span>
|
|
6244
|
-
</div>
|
|
6245
|
-
|
|
6246
|
-
{isTrivial && (
|
|
6247
|
-
<div style={{ fontSize: 12, color: "var(--af-text-tertiary)", fontStyle: "italic", marginTop: 8 }}>
|
|
6248
|
-
Warmup — no enrichment generated.
|
|
6249
|
-
</div>
|
|
6250
|
-
)}
|
|
5419
|
+
// formatDurationHeader moved to ./session-view/helpers.ts
|
|
6251
5420
|
|
|
6252
|
-
|
|
6253
|
-
<div style={{ fontSize: 12, color: "var(--af-text-secondary)", marginTop: 8 }}>
|
|
6254
|
-
Click the pending pill to generate this day's digest →
|
|
6255
|
-
</div>
|
|
6256
|
-
)}
|
|
6257
|
-
|
|
6258
|
-
{enr.status === "done" && (
|
|
6259
|
-
<>
|
|
6260
|
-
{enr.brief_summary && (
|
|
6261
|
-
<p style={{ margin: "10px 0 0", color: "var(--af-text)" }}>{enr.brief_summary}</p>
|
|
6262
|
-
)}
|
|
6263
|
-
{(enr.friction_detail || enr.user_instructions.length > 0) && (
|
|
6264
|
-
<div style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 6 }}>
|
|
6265
|
-
{enr.friction_detail && (
|
|
6266
|
-
<div style={{ fontSize: 12, color: "var(--af-text-secondary)" }}>
|
|
6267
|
-
<span style={{ color: "#ed8936" }}>⚠ </span>
|
|
6268
|
-
{enr.friction_detail}
|
|
6269
|
-
</div>
|
|
6270
|
-
)}
|
|
6271
|
-
{enr.user_instructions.length > 0 && (
|
|
6272
|
-
<details style={{ fontSize: 12, color: "var(--af-text-secondary)" }}>
|
|
6273
|
-
<summary style={{ cursor: "pointer", color: "var(--af-text-tertiary)" }}>
|
|
6274
|
-
Top user instructions ({enr.user_instructions.length})
|
|
6275
|
-
</summary>
|
|
6276
|
-
<ul style={{ margin: "6px 0 0", paddingLeft: 18 }}>
|
|
6277
|
-
{enr.user_instructions.slice(0, 5).map((u, i) => (
|
|
6278
|
-
<li key={i} style={{ marginBottom: 2 }}>{u}</li>
|
|
6279
|
-
))}
|
|
6280
|
-
</ul>
|
|
6281
|
-
</details>
|
|
6282
|
-
)}
|
|
6283
|
-
</div>
|
|
6284
|
-
)}
|
|
6285
|
-
<div style={{ marginTop: 10, fontSize: 11 }}>
|
|
6286
|
-
<Link
|
|
6287
|
-
href={`/digest/${entry.local_day}`}
|
|
6288
|
-
style={{ color: "var(--af-accent)", textDecoration: "none" }}
|
|
6289
|
-
>
|
|
6290
|
-
Open day digest →
|
|
6291
|
-
</Link>
|
|
6292
|
-
</div>
|
|
6293
|
-
</>
|
|
6294
|
-
)}
|
|
6295
|
-
</div>
|
|
6296
|
-
);
|
|
6297
|
-
}
|
|
5421
|
+
// EntryDayStrip moved to ./session-view/entry-day-strip.tsx
|