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.
Files changed (42) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +3 -3
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  13. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  14. package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +1 -1
  15. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  16. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  17. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  18. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  19. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  20. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  21. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  22. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +1 -1
  23. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +1 -1
  24. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +1 -1
  25. package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
  26. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
  27. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  28. package/app/apps/web/.next/server/pages/500.html +1 -1
  29. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  30. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  31. package/app/apps/web/.next/static/chunks/0u-n8fz-ms.ee.js +6 -0
  32. package/app/apps/web/.next/static/chunks/{063oimt30jeo9.js → 15w5_bct86ut6.js} +2 -2
  33. package/app/apps/web/app/sessions/[id]/session-view.tsx +450 -21
  34. package/app/apps/web/package.json +1 -1
  35. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  36. package/dist/daemon-worker.js +87 -0
  37. package/dist/index.js +92 -5
  38. package/package.json +1 -1
  39. package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +0 -6
  40. /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 95-RytHk7ZQtfpZj5BLST}/_buildManifest.js +0 -0
  41. /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 95-RytHk7ZQtfpZj5BLST}/_clientMiddlewareManifest.js +0 -0
  42. /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 [hover, setHover] = useState(false);
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={() => setHover(true)}
1053
- onMouseLeave={() => setHover(false)}
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
- {hover && (
1079
- <Tooltip
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
- </Tooltip>
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({ turn, onClick }: { turn: TurnMegaRow; onClick: () => void }) {
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-lens/web",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": true,
5
5
  "description": "Local-only dashboard for Claude Code sessions. Reads ~/.claude/projects/*.jsonl and visualizes agent activity.",
6
6
  "license": "MIT",