fleetlens 0.4.0 → 0.4.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 +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +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_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +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_client-reference-manifest.js +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_client-reference-manifest.js +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_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/0u-n8fz-ms.ee.js +6 -0
- package/app/apps/web/.next/static/chunks/{063oimt30jeo9.js → 15w5_bct86ut6.js} +2 -2
- package/app/apps/web/app/sessions/[id]/session-view.tsx +450 -21
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +87 -0
- package/dist/index.js +92 -5
- package/package.json +1 -1
- package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +0 -6
- /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 95-RytHk7ZQtfpZj5BLST}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 95-RytHk7ZQtfpZj5BLST}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 95-RytHk7ZQtfpZj5BLST}/_ssgManifest.js +0 -0
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
type TurnMegaRow,
|
|
36
36
|
type TurnSummary,
|
|
37
37
|
} from "@claude-lens/parser";
|
|
38
|
-
import { formatGap, formatOffset, formatRelative, formatTokens, shortId } from "@/lib/format";
|
|
38
|
+
import { estimateCost, formatCost, formatGap, formatOffset, formatRelative, formatTokens, shortId } from "@/lib/format";
|
|
39
39
|
import { LiveBadge } from "@/components/live-badge";
|
|
40
40
|
import { AskClaudeButton, AskClaudeDrawer } from "@/components/ask-claude";
|
|
41
41
|
import { TailMode } from "@/components/tail-mode";
|
|
@@ -286,6 +286,21 @@ export function SessionView({
|
|
|
286
286
|
/** Detect PR creations in this session. */
|
|
287
287
|
const prMarkers = useMemo(() => detectPrMarkers(session), [session]);
|
|
288
288
|
|
|
289
|
+
const coldResumeMarkers = useMemo(() => {
|
|
290
|
+
const seen = new Set<string>();
|
|
291
|
+
const out: {
|
|
292
|
+
tOffsetMs: number;
|
|
293
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
294
|
+
}[] = [];
|
|
295
|
+
for (const e of events) {
|
|
296
|
+
if (!e.coldResume || !e.messageId || e.tOffsetMs === undefined) continue;
|
|
297
|
+
if (seen.has(e.messageId)) continue;
|
|
298
|
+
seen.add(e.messageId);
|
|
299
|
+
out.push({ tOffsetMs: e.tOffsetMs, info: e.coldResume });
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}, [events]);
|
|
303
|
+
|
|
289
304
|
/** Build the full presentation stream once. */
|
|
290
305
|
const allRows = useMemo(() => buildPresentation(visibleEvents), [visibleEvents]);
|
|
291
306
|
|
|
@@ -541,6 +556,18 @@ export function SessionView({
|
|
|
541
556
|
)}
|
|
542
557
|
<InlineStatDivider />
|
|
543
558
|
<InlineTokenStat usage={totalUsage} />
|
|
559
|
+
{session.coldResumeCount !== undefined &&
|
|
560
|
+
session.coldResumeCount > 0 &&
|
|
561
|
+
session.cacheRebuildTokens !== undefined && (
|
|
562
|
+
<>
|
|
563
|
+
<InlineStatDivider />
|
|
564
|
+
<ColdResumeSessionStat
|
|
565
|
+
count={session.coldResumeCount}
|
|
566
|
+
writeTokens={session.cacheRebuildTokens}
|
|
567
|
+
model={model}
|
|
568
|
+
/>
|
|
569
|
+
</>
|
|
570
|
+
)}
|
|
544
571
|
<InlineStatDivider />
|
|
545
572
|
<InlineStat value={`${eventCount} events`} />
|
|
546
573
|
{prMarkers.length > 0 && (
|
|
@@ -675,6 +702,8 @@ export function SessionView({
|
|
|
675
702
|
subagents={session.subagents}
|
|
676
703
|
collapsed={collapsed}
|
|
677
704
|
prMarkers={prMarkers}
|
|
705
|
+
coldResumeMarkers={coldResumeMarkers}
|
|
706
|
+
model={model}
|
|
678
707
|
selectedSubagentId={selectedSubagentId}
|
|
679
708
|
onSelectSubagent={(id) => {
|
|
680
709
|
setSelectedSubagentId(id);
|
|
@@ -749,6 +778,7 @@ export function SessionView({
|
|
|
749
778
|
stickyOffset={headerH + 16}
|
|
750
779
|
isSessionLive={isSessionLive}
|
|
751
780
|
team={team}
|
|
781
|
+
model={model}
|
|
752
782
|
/>
|
|
753
783
|
</>
|
|
754
784
|
) : (
|
|
@@ -1031,7 +1061,7 @@ function InlineStatDivider() {
|
|
|
1031
1061
|
}
|
|
1032
1062
|
|
|
1033
1063
|
function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
|
|
1034
|
-
const
|
|
1064
|
+
const { ref, anchor, open, close } = useAnchoredTooltip();
|
|
1035
1065
|
const u = usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1036
1066
|
const totalIn = u.input + u.cacheRead + u.cacheWrite;
|
|
1037
1067
|
const pctRead = totalIn > 0 ? Math.round((u.cacheRead / totalIn) * 100) : 0;
|
|
@@ -1039,8 +1069,8 @@ function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
|
|
|
1039
1069
|
|
|
1040
1070
|
return (
|
|
1041
1071
|
<span
|
|
1072
|
+
ref={ref}
|
|
1042
1073
|
style={{
|
|
1043
|
-
position: "relative",
|
|
1044
1074
|
display: "inline-flex",
|
|
1045
1075
|
alignItems: "center",
|
|
1046
1076
|
gap: 6,
|
|
@@ -1049,20 +1079,15 @@ function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
|
|
|
1049
1079
|
cursor: "default",
|
|
1050
1080
|
fontFamily: "var(--font-mono)",
|
|
1051
1081
|
}}
|
|
1052
|
-
onMouseEnter={
|
|
1053
|
-
onMouseLeave={
|
|
1082
|
+
onMouseEnter={open}
|
|
1083
|
+
onMouseLeave={close}
|
|
1054
1084
|
>
|
|
1055
|
-
{/* Primary: fresh input / output. This is the actually-new
|
|
1056
|
-
per-request context — distinct from the cached context that
|
|
1057
|
-
dominates most agentic sessions (often 99%+). */}
|
|
1058
1085
|
<span>
|
|
1059
1086
|
{formatTokens(u.input)}
|
|
1060
1087
|
<span style={{ color: "var(--af-text-tertiary)", margin: "0 3px" }}>in</span>
|
|
1061
1088
|
{formatTokens(u.output)}
|
|
1062
1089
|
<span style={{ color: "var(--af-text-tertiary)", marginLeft: 3 }}>out</span>
|
|
1063
1090
|
</span>
|
|
1064
|
-
{/* Secondary: cached context as a separate hint so the header
|
|
1065
|
-
doesn't lump 50M+ cache hits into the "tokens" number. */}
|
|
1066
1091
|
{cached > 0 && (
|
|
1067
1092
|
<span
|
|
1068
1093
|
style={{
|
|
@@ -1075,14 +1100,8 @@ function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
|
|
|
1075
1100
|
+{formatTokens(cached)} cached
|
|
1076
1101
|
</span>
|
|
1077
1102
|
)}
|
|
1078
|
-
{
|
|
1079
|
-
<
|
|
1080
|
-
style={{
|
|
1081
|
-
bottom: "calc(100% + 6px)",
|
|
1082
|
-
left: 0,
|
|
1083
|
-
minWidth: 240,
|
|
1084
|
-
}}
|
|
1085
|
-
>
|
|
1103
|
+
{anchor && (
|
|
1104
|
+
<AnchoredTooltip anchor={anchor} width={280}>
|
|
1086
1105
|
<TooltipRow label="Input (fresh)" value={u.input.toLocaleString()} />
|
|
1087
1106
|
<TooltipRow label="Output" value={u.output.toLocaleString()} />
|
|
1088
1107
|
<TooltipRow label="Cache read" value={`${u.cacheRead.toLocaleString()} (${pctRead}%)`} />
|
|
@@ -1100,12 +1119,60 @@ function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
|
|
|
1100
1119
|
>
|
|
1101
1120
|
Cache reads are cumulative across all API requests and billed at ~10% of regular input.
|
|
1102
1121
|
</div>
|
|
1103
|
-
</
|
|
1122
|
+
</AnchoredTooltip>
|
|
1104
1123
|
)}
|
|
1105
1124
|
</span>
|
|
1106
1125
|
);
|
|
1107
1126
|
}
|
|
1108
1127
|
|
|
1128
|
+
// Used for tooltips that live inside an overflow:hidden ancestor (like the
|
|
1129
|
+
// session-header row). getBoundingClientRect + position:fixed lets the
|
|
1130
|
+
// tooltip escape the clipping context instead of being cropped.
|
|
1131
|
+
function useAnchoredTooltip() {
|
|
1132
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
1133
|
+
const [anchor, setAnchor] = useState<{ top: number; left: number } | null>(null);
|
|
1134
|
+
const open = () => {
|
|
1135
|
+
if (!ref.current) return;
|
|
1136
|
+
const r = ref.current.getBoundingClientRect();
|
|
1137
|
+
setAnchor({ top: r.bottom + 6, left: r.left });
|
|
1138
|
+
};
|
|
1139
|
+
const close = () => setAnchor(null);
|
|
1140
|
+
return { ref, anchor, open, close };
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function AnchoredTooltip({
|
|
1144
|
+
anchor,
|
|
1145
|
+
width,
|
|
1146
|
+
children,
|
|
1147
|
+
}: {
|
|
1148
|
+
anchor: { top: number; left: number };
|
|
1149
|
+
width: number;
|
|
1150
|
+
children: React.ReactNode;
|
|
1151
|
+
}) {
|
|
1152
|
+
return (
|
|
1153
|
+
<div
|
|
1154
|
+
style={{
|
|
1155
|
+
position: "fixed",
|
|
1156
|
+
top: anchor.top,
|
|
1157
|
+
left: anchor.left,
|
|
1158
|
+
zIndex: 1000,
|
|
1159
|
+
background: "#1A1A1A",
|
|
1160
|
+
color: "#F5F1EC",
|
|
1161
|
+
padding: "8px 12px",
|
|
1162
|
+
borderRadius: 6,
|
|
1163
|
+
fontSize: 11,
|
|
1164
|
+
fontFamily: "var(--font-mono)",
|
|
1165
|
+
lineHeight: 1.5,
|
|
1166
|
+
pointerEvents: "none",
|
|
1167
|
+
boxShadow: "0 4px 16px rgba(15,23,42,0.24)",
|
|
1168
|
+
width,
|
|
1169
|
+
}}
|
|
1170
|
+
>
|
|
1171
|
+
{children}
|
|
1172
|
+
</div>
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1109
1176
|
/* ------------------------------------------------------------------ */
|
|
1110
1177
|
/* Generic Tooltip */
|
|
1111
1178
|
/* ------------------------------------------------------------------ */
|
|
@@ -1208,6 +1275,8 @@ function Minimap({
|
|
|
1208
1275
|
selectedSubagentId,
|
|
1209
1276
|
onSelectSubagent,
|
|
1210
1277
|
prMarkers,
|
|
1278
|
+
coldResumeMarkers,
|
|
1279
|
+
model,
|
|
1211
1280
|
}: {
|
|
1212
1281
|
displayRows: DisplayRow[];
|
|
1213
1282
|
durationMs: number;
|
|
@@ -1219,6 +1288,11 @@ function Minimap({
|
|
|
1219
1288
|
selectedSubagentId?: string | null;
|
|
1220
1289
|
onSelectSubagent?: (id: string | null) => void;
|
|
1221
1290
|
prMarkers?: PrMarker[];
|
|
1291
|
+
coldResumeMarkers?: {
|
|
1292
|
+
tOffsetMs: number;
|
|
1293
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
1294
|
+
}[];
|
|
1295
|
+
model?: string;
|
|
1222
1296
|
}) {
|
|
1223
1297
|
const WIDTH = 1400;
|
|
1224
1298
|
/** Main timeline height. Sub-agent lanes stack below this. */
|
|
@@ -1247,6 +1321,10 @@ function Minimap({
|
|
|
1247
1321
|
idleMs?: number;
|
|
1248
1322
|
subagent?: SubagentRun;
|
|
1249
1323
|
pr?: PrMarker;
|
|
1324
|
+
cold?: {
|
|
1325
|
+
tOffsetMs: number;
|
|
1326
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
1327
|
+
};
|
|
1250
1328
|
} | null>(null);
|
|
1251
1329
|
const [playheadMs, setPlayheadMs] = useState<number | null>(null);
|
|
1252
1330
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -1777,6 +1855,41 @@ function Minimap({
|
|
|
1777
1855
|
);
|
|
1778
1856
|
})}
|
|
1779
1857
|
|
|
1858
|
+
{coldResumeMarkers?.map((cr, i) => {
|
|
1859
|
+
const x = msToX(cr.tOffsetMs);
|
|
1860
|
+
return (
|
|
1861
|
+
<g
|
|
1862
|
+
key={`cold-${i}`}
|
|
1863
|
+
style={{ cursor: "pointer" }}
|
|
1864
|
+
onMouseEnter={(e) => setHover({ clientX: e.clientX, cold: cr })}
|
|
1865
|
+
>
|
|
1866
|
+
<line
|
|
1867
|
+
x1={x}
|
|
1868
|
+
x2={x}
|
|
1869
|
+
y1={0}
|
|
1870
|
+
y2={TOTAL_H}
|
|
1871
|
+
stroke="#D97706"
|
|
1872
|
+
strokeWidth="1.5"
|
|
1873
|
+
strokeDasharray="4 3"
|
|
1874
|
+
opacity="0.75"
|
|
1875
|
+
/>
|
|
1876
|
+
<rect
|
|
1877
|
+
x={x - 8}
|
|
1878
|
+
y={0}
|
|
1879
|
+
width={16}
|
|
1880
|
+
height={TOTAL_H}
|
|
1881
|
+
fill="transparent"
|
|
1882
|
+
/>
|
|
1883
|
+
<polygon
|
|
1884
|
+
points={`${x},${MAIN_H / 2 - 5} ${x + 5},${MAIN_H / 2} ${x},${MAIN_H / 2 + 5} ${x - 5},${MAIN_H / 2}`}
|
|
1885
|
+
fill="#D97706"
|
|
1886
|
+
stroke="var(--af-surface)"
|
|
1887
|
+
strokeWidth="1.5"
|
|
1888
|
+
/>
|
|
1889
|
+
</g>
|
|
1890
|
+
);
|
|
1891
|
+
})}
|
|
1892
|
+
|
|
1780
1893
|
{/* Playhead — positioned against the relaxed layout, not raw time.
|
|
1781
1894
|
Spans the full SVG height (main + subagent lanes) so you can see
|
|
1782
1895
|
which subagents were running at the current scroll position. */}
|
|
@@ -1804,6 +1917,8 @@ function Minimap({
|
|
|
1804
1917
|
idleMs={hover.idleMs}
|
|
1805
1918
|
subagent={hover.subagent}
|
|
1806
1919
|
pr={hover.pr}
|
|
1920
|
+
cold={hover.cold}
|
|
1921
|
+
model={model}
|
|
1807
1922
|
/>
|
|
1808
1923
|
)}
|
|
1809
1924
|
|
|
@@ -1891,6 +2006,8 @@ function MinimapHoverCard({
|
|
|
1891
2006
|
idleMs,
|
|
1892
2007
|
subagent,
|
|
1893
2008
|
pr,
|
|
2009
|
+
cold,
|
|
2010
|
+
model,
|
|
1894
2011
|
}: {
|
|
1895
2012
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
1896
2013
|
clientX: number;
|
|
@@ -1899,6 +2016,8 @@ function MinimapHoverCard({
|
|
|
1899
2016
|
idleMs?: number;
|
|
1900
2017
|
subagent?: SubagentRun;
|
|
1901
2018
|
pr?: PrMarker;
|
|
2019
|
+
cold?: { tOffsetMs: number; info: NonNullable<SessionEvent["coldResume"]> };
|
|
2020
|
+
model?: string;
|
|
1902
2021
|
}) {
|
|
1903
2022
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
1904
2023
|
const localX = rect ? clientX - rect.left : 0;
|
|
@@ -2014,6 +2133,77 @@ function MinimapHoverCard({
|
|
|
2014
2133
|
);
|
|
2015
2134
|
}
|
|
2016
2135
|
|
|
2136
|
+
if (cold) {
|
|
2137
|
+
const { trigger, gapMs, writeTokens, compact } = cold.info;
|
|
2138
|
+
const isCompact = trigger === "compact";
|
|
2139
|
+
const estUsd = estimateCost(
|
|
2140
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2141
|
+
model,
|
|
2142
|
+
);
|
|
2143
|
+
let label: string;
|
|
2144
|
+
if (!isCompact) label = "⚡ CACHE REBUILD";
|
|
2145
|
+
else if (compact?.trigger === "auto") label = "⚡ AUTO-COMPACT";
|
|
2146
|
+
else label = "⚡ /COMPACT";
|
|
2147
|
+
const detailLine = isCompact
|
|
2148
|
+
? `${formatTokens(writeTokens)} rewritten · pre-compact ${formatTokens(compact?.preTokens ?? 0)}`
|
|
2149
|
+
: `${formatTokens(writeTokens)} rewritten · idle ${formatGap(gapMs)}`;
|
|
2150
|
+
const hint = isCompact
|
|
2151
|
+
? "Conversation was summarized; prefix had to be rewritten into a fresh cache."
|
|
2152
|
+
: "Prompt cache expired during idle; resuming within 5 min keeps it warm.";
|
|
2153
|
+
return (
|
|
2154
|
+
<div
|
|
2155
|
+
style={{
|
|
2156
|
+
position: "absolute",
|
|
2157
|
+
...posStyle,
|
|
2158
|
+
zIndex: 100,
|
|
2159
|
+
background: "#0F172A",
|
|
2160
|
+
color: "#F1F5F9",
|
|
2161
|
+
borderRadius: 10,
|
|
2162
|
+
padding: "10px 14px",
|
|
2163
|
+
fontSize: 11,
|
|
2164
|
+
pointerEvents: "none",
|
|
2165
|
+
boxShadow: "0 6px 24px rgba(15,23,42,0.22)",
|
|
2166
|
+
maxWidth: 340,
|
|
2167
|
+
minWidth: 220,
|
|
2168
|
+
lineHeight: 1.45,
|
|
2169
|
+
}}
|
|
2170
|
+
>
|
|
2171
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
|
2172
|
+
<span
|
|
2173
|
+
style={{
|
|
2174
|
+
fontSize: 10,
|
|
2175
|
+
fontWeight: 600,
|
|
2176
|
+
padding: "2px 8px",
|
|
2177
|
+
borderRadius: 4,
|
|
2178
|
+
background: "#D97706",
|
|
2179
|
+
color: "#fff",
|
|
2180
|
+
}}
|
|
2181
|
+
>
|
|
2182
|
+
{label}
|
|
2183
|
+
</span>
|
|
2184
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.7 }}>
|
|
2185
|
+
at {formatOffset(cold.tOffsetMs)}
|
|
2186
|
+
</span>
|
|
2187
|
+
</div>
|
|
2188
|
+
<div style={{ fontWeight: 500, fontFamily: "var(--font-mono)" }}>
|
|
2189
|
+
{detailLine}
|
|
2190
|
+
{estUsd >= 0.005 && ` · est. ${formatCost(estUsd)}`}
|
|
2191
|
+
</div>
|
|
2192
|
+
<div
|
|
2193
|
+
style={{
|
|
2194
|
+
marginTop: 4,
|
|
2195
|
+
fontSize: 10,
|
|
2196
|
+
opacity: 0.65,
|
|
2197
|
+
fontStyle: "italic",
|
|
2198
|
+
whiteSpace: "normal",
|
|
2199
|
+
}}
|
|
2200
|
+
>
|
|
2201
|
+
{hint}
|
|
2202
|
+
</div>
|
|
2203
|
+
</div>
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2017
2207
|
if (pr) {
|
|
2018
2208
|
const label = pr.title
|
|
2019
2209
|
? `PR${pr.prNumber ? ` #${pr.prNumber}` : ""}: ${pr.title}`
|
|
@@ -2314,6 +2504,7 @@ function TranscriptList({
|
|
|
2314
2504
|
stickyOffset,
|
|
2315
2505
|
isSessionLive,
|
|
2316
2506
|
team,
|
|
2507
|
+
model,
|
|
2317
2508
|
}: {
|
|
2318
2509
|
displayRows: DisplayRow[];
|
|
2319
2510
|
rowRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
|
|
@@ -2323,6 +2514,7 @@ function TranscriptList({
|
|
|
2323
2514
|
stickyOffset: number;
|
|
2324
2515
|
isSessionLive?: boolean;
|
|
2325
2516
|
team?: (TimelineData & { teamName: string }) | null;
|
|
2517
|
+
model?: string;
|
|
2326
2518
|
}) {
|
|
2327
2519
|
// Find the last collapsed turn index so we can mark it as in-progress
|
|
2328
2520
|
// when the session is live.
|
|
@@ -2343,6 +2535,7 @@ function TranscriptList({
|
|
|
2343
2535
|
// Collapsed turn summary row
|
|
2344
2536
|
if (d.kind === "turn-collapsed") {
|
|
2345
2537
|
const idx = d.turn.firstPrimaryIndex;
|
|
2538
|
+
const turnCold = findColdResumeInDisplayRow(d);
|
|
2346
2539
|
out.push(
|
|
2347
2540
|
<CollapsedTurnRow
|
|
2348
2541
|
key={`turn-${idx}`}
|
|
@@ -2351,6 +2544,8 @@ function TranscriptList({
|
|
|
2351
2544
|
onClick={() => onToggleTurn(idx)}
|
|
2352
2545
|
inProgress={i === lastCollapsedTurnIdx}
|
|
2353
2546
|
team={team}
|
|
2547
|
+
coldResume={turnCold ?? undefined}
|
|
2548
|
+
model={model}
|
|
2354
2549
|
refCb={(el) => {
|
|
2355
2550
|
rowRefs.current[idx] = el;
|
|
2356
2551
|
}}
|
|
@@ -2361,11 +2556,14 @@ function TranscriptList({
|
|
|
2361
2556
|
|
|
2362
2557
|
// Expanded turn header
|
|
2363
2558
|
if (d.kind === "turn-expanded-header") {
|
|
2559
|
+
const turnCold = findColdResumeInDisplayRow(d);
|
|
2364
2560
|
out.push(
|
|
2365
2561
|
<ExpandedTurnHeader
|
|
2366
2562
|
key={`turnhead-${d.turn.firstPrimaryIndex}`}
|
|
2367
2563
|
turn={d.turn}
|
|
2368
2564
|
onClick={() => onToggleTurn(d.turn.firstPrimaryIndex)}
|
|
2565
|
+
coldResume={turnCold ?? undefined}
|
|
2566
|
+
model={model}
|
|
2369
2567
|
/>,
|
|
2370
2568
|
);
|
|
2371
2569
|
continue;
|
|
@@ -2430,6 +2628,212 @@ function IdleDivider({ gapMs }: { gapMs: number }) {
|
|
|
2430
2628
|
);
|
|
2431
2629
|
}
|
|
2432
2630
|
|
|
2631
|
+
function findColdResumeInDisplayRow(
|
|
2632
|
+
d: DisplayRow,
|
|
2633
|
+
): SessionEvent["coldResume"] | null {
|
|
2634
|
+
if (d.kind === "presentation") return findColdResumeInRow(d.row);
|
|
2635
|
+
if (d.kind === "turn-collapsed" || d.kind === "turn-expanded-header") {
|
|
2636
|
+
for (const r of d.turn.rows) {
|
|
2637
|
+
const hit = findColdResumeInRow(r);
|
|
2638
|
+
if (hit) return hit;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
function findColdResumeInRow(
|
|
2645
|
+
r: PresentationRow,
|
|
2646
|
+
): SessionEvent["coldResume"] | null {
|
|
2647
|
+
if (r.kind === "agent") {
|
|
2648
|
+
if (r.event.coldResume) return r.event.coldResume;
|
|
2649
|
+
for (const ge of r.groupedEvents) {
|
|
2650
|
+
if (ge.coldResume) return ge.coldResume;
|
|
2651
|
+
}
|
|
2652
|
+
} else if (r.kind === "tool-group") {
|
|
2653
|
+
for (const e of r.events) if (e.coldResume) return e.coldResume;
|
|
2654
|
+
}
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
function ColdResumeNotice({
|
|
2659
|
+
info,
|
|
2660
|
+
model,
|
|
2661
|
+
}: {
|
|
2662
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
2663
|
+
model?: string;
|
|
2664
|
+
}) {
|
|
2665
|
+
const { trigger, gapMs, writeTokens, writeRatio, compact } = info;
|
|
2666
|
+
const estUsd = estimateCost(
|
|
2667
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2668
|
+
model,
|
|
2669
|
+
);
|
|
2670
|
+
const isCompact = trigger === "compact";
|
|
2671
|
+
const fullyCold = writeRatio >= 0.9;
|
|
2672
|
+
let title: string;
|
|
2673
|
+
if (!isCompact) title = `Session resumed cold · idle ${formatGap(gapMs)}`;
|
|
2674
|
+
else if (compact?.trigger === "auto") title = "Auto-compact rebuilt the cache";
|
|
2675
|
+
else title = "Conversation compacted · cache rebuilt";
|
|
2676
|
+
const hint = isCompact
|
|
2677
|
+
? "Compaction summarizes the conversation, so the prefix must be rewritten into a fresh cache. Any /compact or auto-compact will cost this rewrite."
|
|
2678
|
+
: "Prompt cache expired during idle. Resuming within 5 min keeps the cache warm and avoids the rewrite tax.";
|
|
2679
|
+
return (
|
|
2680
|
+
<div
|
|
2681
|
+
style={{
|
|
2682
|
+
padding: "8px 12px",
|
|
2683
|
+
background: "rgba(217, 119, 6, 0.08)",
|
|
2684
|
+
border: "1px solid rgba(217, 119, 6, 0.35)",
|
|
2685
|
+
borderLeft: "3px solid rgba(217, 119, 6, 0.9)",
|
|
2686
|
+
borderRadius: 6,
|
|
2687
|
+
fontSize: 11.5,
|
|
2688
|
+
color: "var(--af-text-secondary)",
|
|
2689
|
+
lineHeight: 1.5,
|
|
2690
|
+
}}
|
|
2691
|
+
>
|
|
2692
|
+
<div
|
|
2693
|
+
style={{
|
|
2694
|
+
display: "flex",
|
|
2695
|
+
alignItems: "center",
|
|
2696
|
+
gap: 8,
|
|
2697
|
+
fontWeight: 600,
|
|
2698
|
+
color: "#78350F",
|
|
2699
|
+
letterSpacing: "0.01em",
|
|
2700
|
+
}}
|
|
2701
|
+
>
|
|
2702
|
+
<span style={{ fontSize: 13 }}>⚡</span>
|
|
2703
|
+
{title}
|
|
2704
|
+
</div>
|
|
2705
|
+
<div style={{ marginTop: 4, fontFamily: "var(--font-mono)", fontSize: 11 }}>
|
|
2706
|
+
Rewrote <b>{formatTokens(writeTokens)}</b> tokens into prompt cache
|
|
2707
|
+
{estUsd >= 0.005 && (
|
|
2708
|
+
<>
|
|
2709
|
+
{" "}
|
|
2710
|
+
· est. <b>{formatCost(estUsd)}</b>
|
|
2711
|
+
</>
|
|
2712
|
+
)}
|
|
2713
|
+
{isCompact && compact && (
|
|
2714
|
+
<>
|
|
2715
|
+
{" "}
|
|
2716
|
+
· pre-compact <b>{formatTokens(compact.preTokens)}</b>
|
|
2717
|
+
</>
|
|
2718
|
+
)}
|
|
2719
|
+
{!isCompact && (
|
|
2720
|
+
<>
|
|
2721
|
+
{" "}
|
|
2722
|
+
· {fullyCold ? "fully cold" : "partial"} ({Math.round(writeRatio * 100)}% write)
|
|
2723
|
+
</>
|
|
2724
|
+
)}
|
|
2725
|
+
</div>
|
|
2726
|
+
<div
|
|
2727
|
+
style={{
|
|
2728
|
+
marginTop: 3,
|
|
2729
|
+
fontSize: 10.5,
|
|
2730
|
+
color: "var(--af-text-tertiary)",
|
|
2731
|
+
fontStyle: "italic",
|
|
2732
|
+
}}
|
|
2733
|
+
>
|
|
2734
|
+
{hint}
|
|
2735
|
+
</div>
|
|
2736
|
+
</div>
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
function ColdResumeSessionStat({
|
|
2741
|
+
count,
|
|
2742
|
+
writeTokens,
|
|
2743
|
+
model,
|
|
2744
|
+
}: {
|
|
2745
|
+
count: number;
|
|
2746
|
+
writeTokens: number;
|
|
2747
|
+
model?: string;
|
|
2748
|
+
}) {
|
|
2749
|
+
const { ref, anchor, open, close } = useAnchoredTooltip();
|
|
2750
|
+
const estUsd = estimateCost(
|
|
2751
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2752
|
+
model,
|
|
2753
|
+
);
|
|
2754
|
+
return (
|
|
2755
|
+
<span
|
|
2756
|
+
ref={ref}
|
|
2757
|
+
style={{
|
|
2758
|
+
display: "inline-flex",
|
|
2759
|
+
alignItems: "center",
|
|
2760
|
+
gap: 5,
|
|
2761
|
+
fontSize: 11.5,
|
|
2762
|
+
fontWeight: 600,
|
|
2763
|
+
color: "#78350F",
|
|
2764
|
+
background: "rgba(217, 119, 6, 0.12)",
|
|
2765
|
+
padding: "2px 8px",
|
|
2766
|
+
borderRadius: 100,
|
|
2767
|
+
cursor: "default",
|
|
2768
|
+
}}
|
|
2769
|
+
onMouseEnter={open}
|
|
2770
|
+
onMouseLeave={close}
|
|
2771
|
+
>
|
|
2772
|
+
<span style={{ fontSize: 12 }}>⚡</span>
|
|
2773
|
+
{count} cache rebuild{count === 1 ? "" : "s"} · {formatTokens(writeTokens)} rewritten
|
|
2774
|
+
{anchor && (
|
|
2775
|
+
<AnchoredTooltip anchor={anchor} width={320}>
|
|
2776
|
+
<TooltipRow label="Cache rebuilds" value={count.toLocaleString()} />
|
|
2777
|
+
<TooltipRow label="Tokens rewritten" value={writeTokens.toLocaleString()} />
|
|
2778
|
+
{estUsd >= 0.005 && (
|
|
2779
|
+
<TooltipRow label="Est. rebuild cost" value={formatCost(estUsd)} />
|
|
2780
|
+
)}
|
|
2781
|
+
<div
|
|
2782
|
+
style={{
|
|
2783
|
+
marginTop: 6,
|
|
2784
|
+
paddingTop: 6,
|
|
2785
|
+
borderTop: "1px solid rgba(241,245,249,0.12)",
|
|
2786
|
+
opacity: 0.65,
|
|
2787
|
+
fontSize: 10,
|
|
2788
|
+
whiteSpace: "normal",
|
|
2789
|
+
lineHeight: 1.4,
|
|
2790
|
+
}}
|
|
2791
|
+
>
|
|
2792
|
+
Turns where the whole prompt-cache prefix had to be rewritten.
|
|
2793
|
+
Either the cache expired during a long idle gap, or a /compact
|
|
2794
|
+
(manual or auto) summarized the conversation. Both cost tokens
|
|
2795
|
+
at 1.25× base input price.
|
|
2796
|
+
</div>
|
|
2797
|
+
</AnchoredTooltip>
|
|
2798
|
+
)}
|
|
2799
|
+
</span>
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
function ColdResumeChip({
|
|
2804
|
+
info,
|
|
2805
|
+
model,
|
|
2806
|
+
}: {
|
|
2807
|
+
info: NonNullable<SessionEvent["coldResume"]>;
|
|
2808
|
+
model?: string;
|
|
2809
|
+
}) {
|
|
2810
|
+
const { writeTokens } = info;
|
|
2811
|
+
const estUsd = estimateCost(
|
|
2812
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: writeTokens },
|
|
2813
|
+
model,
|
|
2814
|
+
);
|
|
2815
|
+
return (
|
|
2816
|
+
<span
|
|
2817
|
+
title={`Prompt cache expired during idle; this turn rewrote ${writeTokens.toLocaleString()} tokens into cache${estUsd >= 0.005 ? ` (est. ${formatCost(estUsd)})` : ""}.`}
|
|
2818
|
+
style={{
|
|
2819
|
+
display: "inline-flex",
|
|
2820
|
+
alignItems: "center",
|
|
2821
|
+
gap: 4,
|
|
2822
|
+
fontSize: 10.5,
|
|
2823
|
+
fontWeight: 600,
|
|
2824
|
+
padding: "2px 7px",
|
|
2825
|
+
borderRadius: 3,
|
|
2826
|
+
background: "rgba(217, 119, 6, 0.14)",
|
|
2827
|
+
color: "#78350F",
|
|
2828
|
+
letterSpacing: "0.01em",
|
|
2829
|
+
cursor: "default",
|
|
2830
|
+
}}
|
|
2831
|
+
>
|
|
2832
|
+
⚡ cold resume · {formatTokens(writeTokens)} rewritten
|
|
2833
|
+
</span>
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2433
2837
|
/* ------------------------------------------------------------------ */
|
|
2434
2838
|
/* Collapsed turn row */
|
|
2435
2839
|
/* */
|
|
@@ -2446,6 +2850,8 @@ function CollapsedTurnRow({
|
|
|
2446
2850
|
stickyOffset,
|
|
2447
2851
|
inProgress,
|
|
2448
2852
|
team,
|
|
2853
|
+
coldResume,
|
|
2854
|
+
model,
|
|
2449
2855
|
}: {
|
|
2450
2856
|
turn: TurnMegaRow;
|
|
2451
2857
|
/** Fires when the user wants to fully expand this turn into the
|
|
@@ -2456,6 +2862,8 @@ function CollapsedTurnRow({
|
|
|
2456
2862
|
stickyOffset: number;
|
|
2457
2863
|
inProgress?: boolean;
|
|
2458
2864
|
team?: (TimelineData & { teamName: string }) | null;
|
|
2865
|
+
coldResume?: NonNullable<SessionEvent["coldResume"]>;
|
|
2866
|
+
model?: string;
|
|
2459
2867
|
}) {
|
|
2460
2868
|
const theme = ROLE_THEMES.agent;
|
|
2461
2869
|
const s = turn.summary;
|
|
@@ -2526,7 +2934,7 @@ function CollapsedTurnRow({
|
|
|
2526
2934
|
Agent
|
|
2527
2935
|
</span>
|
|
2528
2936
|
|
|
2529
|
-
{/* Col 3 — content (first · stats · steps · last · bottom bar) */}
|
|
2937
|
+
{/* Col 3 — content (cold-resume notice · first · stats · steps · last · bottom bar) */}
|
|
2530
2938
|
<div
|
|
2531
2939
|
style={{
|
|
2532
2940
|
minWidth: 0,
|
|
@@ -2536,6 +2944,12 @@ function CollapsedTurnRow({
|
|
|
2536
2944
|
paddingBottom: 10,
|
|
2537
2945
|
}}
|
|
2538
2946
|
>
|
|
2947
|
+
{coldResume && (
|
|
2948
|
+
<div onClick={stop}>
|
|
2949
|
+
<ColdResumeNotice info={coldResume} model={model} />
|
|
2950
|
+
</div>
|
|
2951
|
+
)}
|
|
2952
|
+
|
|
2539
2953
|
{/* 1. First agent message — clickable to expand into full markdown */}
|
|
2540
2954
|
{firstAgentRow && (
|
|
2541
2955
|
<ExpandableMessage
|
|
@@ -3027,7 +3441,17 @@ function TurnStatsLine({
|
|
|
3027
3441
|
);
|
|
3028
3442
|
}
|
|
3029
3443
|
|
|
3030
|
-
function ExpandedTurnHeader({
|
|
3444
|
+
function ExpandedTurnHeader({
|
|
3445
|
+
turn,
|
|
3446
|
+
onClick,
|
|
3447
|
+
coldResume,
|
|
3448
|
+
model,
|
|
3449
|
+
}: {
|
|
3450
|
+
turn: TurnMegaRow;
|
|
3451
|
+
onClick: () => void;
|
|
3452
|
+
coldResume?: NonNullable<SessionEvent["coldResume"]>;
|
|
3453
|
+
model?: string;
|
|
3454
|
+
}) {
|
|
3031
3455
|
const s = turn.summary;
|
|
3032
3456
|
return (
|
|
3033
3457
|
<div
|
|
@@ -3060,6 +3484,11 @@ function ExpandedTurnHeader({ turn, onClick }: { turn: TurnMegaRow; onClick: ()
|
|
|
3060
3484
|
{s.toolCalls === 1 ? "" : "s"}
|
|
3061
3485
|
{turn.durationMs !== undefined ? ` · ${formatGap(turn.durationMs)}` : ""}
|
|
3062
3486
|
</span>
|
|
3487
|
+
{coldResume && (
|
|
3488
|
+
<span style={{ marginLeft: "auto" }}>
|
|
3489
|
+
<ColdResumeChip info={coldResume} model={model} />
|
|
3490
|
+
</span>
|
|
3491
|
+
)}
|
|
3063
3492
|
</div>
|
|
3064
3493
|
);
|
|
3065
3494
|
}
|