fleetlens 0.6.4 → 0.7.0

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 (178) 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.js +2 -2
  12. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/app/apps/web/.next/server/app/api/ask/route.js +2 -2
  15. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  16. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +3 -3
  17. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +3 -3
  19. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +1 -1
  21. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +3 -3
  23. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +1 -1
  25. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/events/route.js +2 -2
  27. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  29. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/settings/route.js +3 -3
  32. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  33. package/app/apps/web/.next/server/app/day/[date]/page.js +3 -3
  34. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  35. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  36. package/app/apps/web/.next/server/app/day/page.js +2 -2
  37. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  38. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  39. package/app/apps/web/.next/server/app/digest/[date]/page.js +2 -2
  40. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  41. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  42. package/app/apps/web/.next/server/app/digest/page.js +2 -2
  43. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  44. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  45. package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -2
  46. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  47. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  48. package/app/apps/web/.next/server/app/insights/page.js +2 -2
  49. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  50. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  51. package/app/apps/web/.next/server/app/page.js +3 -3
  52. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  53. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/app/apps/web/.next/server/app/parallelism/page.js +2 -2
  55. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  56. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  57. package/app/apps/web/.next/server/app/projects/[slug]/page.js +3 -3
  58. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  59. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  60. package/app/apps/web/.next/server/app/projects/page.js +3 -3
  61. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  62. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  63. package/app/apps/web/.next/server/app/runs/page.js +2 -2
  64. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  65. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  66. package/app/apps/web/.next/server/app/sessions/[id]/page.js +3 -3
  67. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  68. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  69. package/app/apps/web/.next/server/app/sessions/page.js +3 -3
  70. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  71. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  72. package/app/apps/web/.next/server/app/settings/page.js +2 -2
  73. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/app/apps/web/.next/server/app/usage/page.js +2 -2
  76. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  77. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  78. package/app/apps/web/.next/server/chunks/{[externals]__0o43ejh._.js → [externals]__0fpbr8-._.js} +2 -2
  79. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0-vgrh1._.js +1 -1
  80. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.z48ue._.js +3 -0
  81. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0dctov1._.js +5 -0
  82. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e8n_x4._.js +1 -1
  83. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +2 -2
  84. package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +1 -1
  85. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +1 -1
  86. package/app/apps/web/.next/server/chunks/packages_09hmrqx._.js +3 -0
  87. package/app/apps/web/.next/server/chunks/packages_118y1lp._.js +3 -0
  88. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +26 -17
  89. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_122oe0-.js +3 -0
  90. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +3 -0
  91. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0e.gxk3._.js +3 -0
  92. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0p1x6v2._.js +3 -0
  93. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
  94. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
  95. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0sp6.jt._.js +3 -0
  96. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +3 -0
  97. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0v.6dx0._.js +3 -0
  98. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
  99. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wwa6gh._.js +1 -1
  100. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv-a8z._.js +12 -0
  101. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10~b4bf._.js +12 -0
  102. package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +1 -1
  103. package/app/apps/web/.next/server/chunks/ssr/_0woap-3._.js +1 -1
  104. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  105. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  106. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_settings_ai-features-form_tsx_01p699p._.js +1 -1
  107. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +1 -1
  108. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
  109. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_usage-data_ts_0xtv99n._.js +1 -1
  110. package/app/apps/web/.next/server/chunks/ssr/{node_modules_0i2xw~e._.js → node_modules_0fx6-.x._.js} +2 -2
  111. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  112. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_10_oegc._.js +3 -0
  113. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  114. package/app/apps/web/.next/server/pages/500.html +1 -1
  115. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  116. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  117. package/app/apps/web/.next/static/chunks/0.3mqbj24kwtc.js +6 -0
  118. package/app/apps/web/.next/static/chunks/07-fq4im3pmjp.js +4 -0
  119. package/app/apps/web/.next/static/chunks/09gdxttgu~.aj.js +14 -0
  120. package/app/apps/web/.next/static/chunks/0n3-prt61k7pr.js +1 -0
  121. package/app/apps/web/.next/static/chunks/{0_7303jr0sf0w.js → 0q2vd~aj.1hpz.js} +3 -3
  122. package/app/apps/web/.next/static/chunks/0qmh5da6gfm56.js +1 -0
  123. package/app/apps/web/.next/static/chunks/{0k9pksi_i.odf.js → 111-bgx08j2nq.js} +1 -1
  124. package/app/apps/web/app/api/ask/route.ts +1 -1
  125. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +2 -1
  126. package/app/apps/web/app/api/settings/route.ts +1 -0
  127. package/app/apps/web/app/insights/[key]/page.tsx +8 -2
  128. package/app/apps/web/app/layout.tsx +10 -7
  129. package/app/apps/web/app/page.tsx +19 -15
  130. package/app/apps/web/app/parallelism/gantt-chart.tsx +7 -18
  131. package/app/apps/web/app/projects/projects-view.tsx +3 -0
  132. package/app/apps/web/app/sessions/[id]/session-view.tsx +168 -41
  133. package/app/apps/web/app/sessions/page.tsx +22 -12
  134. package/app/apps/web/app/sessions/sessions-grid.tsx +82 -9
  135. package/app/apps/web/app/settings/ai-features-form.tsx +20 -1
  136. package/app/apps/web/app/settings/page.tsx +1 -0
  137. package/app/apps/web/app/usage/page.tsx +161 -52
  138. package/app/apps/web/components/agent-badge.tsx +43 -0
  139. package/app/apps/web/components/agent-icon.tsx +38 -0
  140. package/app/apps/web/components/agent-mix-chip.tsx +42 -0
  141. package/app/apps/web/components/{ask-claude.tsx → ask.tsx} +13 -12
  142. package/app/apps/web/components/day-digest.tsx +1 -1
  143. package/app/apps/web/components/live-sessions-widget.tsx +5 -1
  144. package/app/apps/web/components/outcome-pill.tsx +7 -2
  145. package/app/apps/web/components/sidebar.tsx +1 -1
  146. package/app/apps/web/components/week-digest.tsx +2 -0
  147. package/app/apps/web/lib/ai/session-summary.ts +1 -1
  148. package/app/apps/web/lib/data.ts +7 -8
  149. package/app/apps/web/lib/date-range.ts +9 -2
  150. package/app/apps/web/lib/usage-data.ts +24 -1
  151. package/app/apps/web/lib/validate-settings.ts +1 -0
  152. package/app/apps/web/package.json +1 -1
  153. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  154. package/dist/daemon-worker.js +3152 -2355
  155. package/dist/index.js +4418 -3752
  156. package/package.json +1 -1
  157. package/app/apps/web/.next/server/chunks/[externals]__0_xztrf._.js +0 -3
  158. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +0 -5
  159. package/app/apps/web/.next/server/chunks/packages_0e7q9u_._.js +0 -3
  160. package/app/apps/web/.next/server/chunks/packages_0n6p2ot._.js +0 -3
  161. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +0 -3
  162. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-94ji~._.js +0 -3
  163. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_6.inc._.js +0 -3
  164. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +0 -3
  165. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0jdyefm._.js +0 -12
  166. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ln9jsw._.js +0 -3
  167. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u5d7de._.js +0 -12
  168. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~_8vff._.js +0 -3
  169. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11v.eqw._.js +0 -3
  170. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +0 -3
  171. package/app/apps/web/.next/static/chunks/04m3slg_5ph02.js +0 -1
  172. package/app/apps/web/.next/static/chunks/0coa1800xqgfj.js +0 -1
  173. package/app/apps/web/.next/static/chunks/0k1psab.bybh9.js +0 -14
  174. package/app/apps/web/.next/static/chunks/0xfu09fn_358k.js +0 -4
  175. package/app/apps/web/.next/static/chunks/17u2ww8ya5dv1.js +0 -6
  176. /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → qYW0u_NOmV0v7lf7xZc6x}/_buildManifest.js +0 -0
  177. /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → qYW0u_NOmV0v7lf7xZc6x}/_clientMiddlewareManifest.js +0 -0
  178. /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → qYW0u_NOmV0v7lf7xZc6x}/_ssgManifest.js +0 -0
@@ -39,7 +39,7 @@ import {
39
39
  } from "@claude-lens/parser";
40
40
  import { estimateCost, formatCost, formatGap, formatOffset, formatRelative, formatTokens, shortId } from "@/lib/format";
41
41
  import { LiveBadge } from "@/components/live-badge";
42
- import { AskClaudeButton, AskClaudeDrawer } from "@/components/ask-claude";
42
+ import { AskButton, AskDrawer } from "@/components/ask";
43
43
  import { TailMode } from "@/components/tail-mode";
44
44
  import type { TimelineData } from "./team-tab/adapter";
45
45
  import { TeamTabClient } from "./team-tab/team-tab-client";
@@ -65,7 +65,9 @@ import {
65
65
  /* Constants + theming */
66
66
  /* ------------------------------------------------------------------ */
67
67
 
68
- /** Gap > this (ms) before a user row is shown as a "Session idle" divider. */
68
+ /** Gap > this (ms) before a user row is shown as a "Session idle" divider
69
+ * in the transcript body (separate from the minimap, which now derives
70
+ * idle from raw event timestamps via rawIdleBands). */
69
71
  const IDLE_THRESHOLD_MS = 2000;
70
72
 
71
73
  /** Sticky header height (session header + tabs + mini-map).
@@ -150,7 +152,7 @@ export function SessionView({
150
152
  const [filter, setFilter] = useState<FilterMode>("turns");
151
153
  const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
152
154
  const [selectedSubagentId, setSelectedSubagentId] = useState<string | null>(null);
153
- const [askClaudeOpen, setAskClaudeOpen] = useState(false);
155
+ const [askOpen, setAskOpen] = useState(false);
154
156
  const [expandedTurns, setExpandedTurns] = useState<Set<number>>(new Set());
155
157
  /** When a timeline click sets the index we also want to scroll. Track that
156
158
  * intent separately so selection via row-click doesn't auto-scroll. */
@@ -290,6 +292,76 @@ export function SessionView({
290
292
  /** Detect PR creations in this session. */
291
293
  const prMarkers = useMemo(() => detectPrMarkers(session), [session]);
292
294
 
295
+ /** Idle bands derived from raw event timestamps, classified per gap.
296
+ *
297
+ * Two flavors:
298
+ * - In-turn idle (gap between agent activities within the same user
299
+ * turn) — multiple of these inside one turn collapse into ONE merged
300
+ * band whose `start..end` spans the whole period and `durationMs`
301
+ * sums the actual idle.
302
+ * - Between-turn idle (gap whose terminating anchor IS a user message)
303
+ * — emitted standalone. This is "user paused, walked away, came
304
+ * back" time and is conceptually distinct from agent thinking.
305
+ *
306
+ * Anchors only count user / agent / tool-call / tool-result events —
307
+ * meta + thinking events are skipped so sub-second event-to-event gaps
308
+ * don't crowd the strip.
309
+ *
310
+ * Side effect: the leading warm-up gap before the first agent response
311
+ * drops out unless it crosses MIN_GAP_MS — which the first 6s in a
312
+ * typical Codex session does not. */
313
+ const rawIdleBands = useMemo(() => {
314
+ const isAnchor = (role: string) =>
315
+ role === "user" ||
316
+ role === "agent" ||
317
+ role === "tool-call" ||
318
+ role === "tool-result";
319
+ const MIN_GAP_MS = 10_000;
320
+ const anchors: { ms: number; role: string }[] = [];
321
+ for (const e of events) {
322
+ if (!isAnchor(e.role)) continue;
323
+ if (!e.timestamp) continue;
324
+ const ms = Date.parse(e.timestamp);
325
+ if (!Number.isFinite(ms)) continue;
326
+ anchors.push({ ms, role: e.role });
327
+ }
328
+ if (anchors.length < 2) return [];
329
+ anchors.sort((a, b) => a.ms - b.ms);
330
+ const sessionStartMs = anchors[0]!.ms - (events.find((e) => e.timestamp)?.tOffsetMs ?? 0);
331
+
332
+ type Band = { start: number; end: number; durationMs: number };
333
+ const bands: Band[] = [];
334
+ let inTurn: Band | null = null;
335
+
336
+ const flushInTurn = () => {
337
+ if (inTurn && inTurn.durationMs > MIN_GAP_MS) bands.push(inTurn);
338
+ inTurn = null;
339
+ };
340
+
341
+ for (let i = 1; i < anchors.length; i++) {
342
+ const gap = anchors[i]!.ms - anchors[i - 1]!.ms;
343
+ if (gap <= MIN_GAP_MS) continue;
344
+ const gStart = anchors[i - 1]!.ms - sessionStartMs;
345
+ const gEnd = anchors[i]!.ms - sessionStartMs;
346
+
347
+ if (anchors[i]!.role === "user") {
348
+ // Between-turn idle — close any in-progress in-turn merge first,
349
+ // then push the between-turn band on its own.
350
+ flushInTurn();
351
+ bands.push({ start: gStart, end: gEnd, durationMs: gap });
352
+ } else {
353
+ // In-turn idle — accumulate into the running merged band.
354
+ if (!inTurn) inTurn = { start: gStart, end: gEnd, durationMs: gap };
355
+ else {
356
+ inTurn.end = gEnd;
357
+ inTurn.durationMs += gap;
358
+ }
359
+ }
360
+ }
361
+ flushInTurn();
362
+ return bands;
363
+ }, [events]);
364
+
293
365
  const coldResumeMarkers = useMemo(() => {
294
366
  const seen = new Set<string>();
295
367
  const out: {
@@ -607,7 +679,7 @@ export function SessionView({
607
679
  </span>
608
680
  </div>
609
681
 
610
- {/* Toolbar (filter, copy, ask-claude) */}
682
+ {/* Toolbar (filter, copy, ask) */}
611
683
  <div
612
684
  className="flex items-center"
613
685
  style={{
@@ -662,9 +734,9 @@ export function SessionView({
662
734
  >
663
735
  <Copy size={12} /> Copy all
664
736
  </button>
665
- <AskClaudeButton
737
+ <AskButton
666
738
  onClick={() => {
667
- setAskClaudeOpen((p) => !p);
739
+ setAskOpen((p) => !p);
668
740
  setSelectedIndex(null);
669
741
  setSelectedSubagentId(null);
670
742
  }}
@@ -707,6 +779,7 @@ export function SessionView({
707
779
  collapsed={collapsed}
708
780
  prMarkers={prMarkers}
709
781
  coldResumeMarkers={coldResumeMarkers}
782
+ rawIdleBands={rawIdleBands}
710
783
  model={model}
711
784
  selectedSubagentId={selectedSubagentId}
712
785
  onSelectSubagent={(id) => {
@@ -788,6 +861,7 @@ export function SessionView({
788
861
  isSessionLive={isSessionLive}
789
862
  team={team}
790
863
  model={model}
864
+ rawIdleBands={rawIdleBands}
791
865
  />
792
866
  </>
793
867
  ) : (
@@ -868,8 +942,8 @@ export function SessionView({
868
942
  );
869
943
  })()}
870
944
 
871
- {/* Ask Claude drawer */}
872
- {askClaudeOpen && (
945
+ {/* Ask drawer */}
946
+ {askOpen && (
873
947
  <aside
874
948
  style={{
875
949
  position: "fixed",
@@ -885,9 +959,9 @@ export function SessionView({
885
959
  flexDirection: "column",
886
960
  }}
887
961
  >
888
- <AskClaudeDrawer
962
+ <AskDrawer
889
963
  sessionId={session.id}
890
- onClose={() => setAskClaudeOpen(false)}
964
+ onClose={() => setAskOpen(false)}
891
965
  />
892
966
  </aside>
893
967
  )}
@@ -1285,6 +1359,7 @@ function Minimap({
1285
1359
  onSelectSubagent,
1286
1360
  prMarkers,
1287
1361
  coldResumeMarkers,
1362
+ rawIdleBands,
1288
1363
  model,
1289
1364
  }: {
1290
1365
  displayRows: DisplayRow[];
@@ -1301,6 +1376,11 @@ function Minimap({
1301
1376
  tOffsetMs: number;
1302
1377
  info: NonNullable<SessionEvent["coldResume"]>;
1303
1378
  }[];
1379
+ /** Idle bands derived directly from raw event timestamps. Used in place
1380
+ * of the legacy "gap before user row" heuristic so Codex sessions
1381
+ * (which spend most of their time in agent reasoning, not user input)
1382
+ * surface their dead-air on the minimap. */
1383
+ rawIdleBands?: { start: number; end: number; durationMs: number }[];
1304
1384
  model?: string;
1305
1385
  }) {
1306
1386
  const WIDTH = 1400;
@@ -1456,52 +1536,66 @@ function Minimap({
1456
1536
  }
1457
1537
  }
1458
1538
 
1539
+ // Build a sorted copy of idle bands so we can clip row/turn ends to
1540
+ // the start of the next idle band (or the next row, whichever comes
1541
+ // first). This stops a row's rectangle from spanning across an idle
1542
+ // gap and visually swallowing it.
1543
+ const idleBands = (rawIdleBands ?? []).slice().sort((a, b) => a.start - b.start);
1544
+ const nextIdleStartFrom = (t: number): number | undefined => {
1545
+ for (const b of idleBands) {
1546
+ if (b.start >= t) return b.start;
1547
+ }
1548
+ return undefined;
1549
+ };
1550
+
1459
1551
  for (let i = 0; i < withTime.length; i++) {
1460
1552
  const item = withTime[i];
1461
1553
  const next = withTime[i + 1];
1462
1554
  const start = item.start;
1463
-
1464
- // Idle gap before user rows (only in atomic view — turns already
1465
- // contain their idle time inside their duration).
1466
- if (
1467
- item.kind === "row" &&
1468
- item.row.kind === "user" &&
1469
- item.gapMs > IDLE_THRESHOLD_MS &&
1470
- out.length > 0
1471
- ) {
1472
- out.push({
1473
- kind: "idle",
1474
- start: Math.max(0, start - item.gapMs),
1475
- end: start,
1476
- durationMs: item.gapMs,
1477
- });
1478
- }
1555
+ const nextRowStart = next?.start;
1556
+ const nextIdleStart = nextIdleStartFrom(start + 1);
1557
+ // Cap end to whichever comes first: next visible row, next idle band.
1558
+ const cap = (raw: number) =>
1559
+ Math.min(
1560
+ raw,
1561
+ nextRowStart ?? Number.POSITIVE_INFINITY,
1562
+ nextIdleStart ?? Number.POSITIVE_INFINITY,
1563
+ );
1479
1564
 
1480
1565
  if (item.kind === "turn") {
1481
- // Turn's end = start + duration, clamped against the next
1482
- // segment's start so adjacent turns don't overlap.
1483
1566
  const turnEnd = start + (item.turn.durationMs ?? 0);
1484
- const nextStart = next?.start;
1485
- const end = nextStart !== undefined ? Math.min(turnEnd, nextStart) : turnEnd;
1486
1567
  out.push({
1487
1568
  kind: "turn",
1488
1569
  turn: item.turn,
1489
1570
  primaryIndex: item.turn.firstPrimaryIndex,
1490
1571
  start,
1491
- end: Math.max(end, start + 1),
1572
+ end: Math.max(cap(turnEnd), start + 1),
1492
1573
  });
1493
1574
  } else {
1494
- const end = next?.start ?? Math.min(start + 800, safeDur);
1575
+ const end = cap(nextRowStart ?? start + 800);
1495
1576
  out.push({
1496
1577
  kind: "row",
1497
1578
  row: item.row,
1498
1579
  primaryIndex: rowPrimaryIndex(item.row),
1499
1580
  start,
1500
- end,
1581
+ end: Math.max(end, start + 1),
1501
1582
  });
1502
1583
  }
1503
1584
  }
1504
1585
 
1586
+ // Idle bands are computed from raw event timestamps (5s threshold) up
1587
+ // in the parent — append them and let the sort below interleave by
1588
+ // start time so they slot between rows correctly.
1589
+ for (const band of idleBands) {
1590
+ out.push({
1591
+ kind: "idle",
1592
+ start: band.start,
1593
+ end: band.end,
1594
+ durationMs: band.durationMs,
1595
+ });
1596
+ }
1597
+ out.sort((a, b) => a.start - b.start);
1598
+
1505
1599
  // Cap the number of rendered segments. Each <rect> carries two
1506
1600
  // event handlers (onMouseEnter + onClick), so 2000 raw-events in
1507
1601
  // "All events" mode becomes 4000 DOM event listeners — enough to
@@ -1546,7 +1640,7 @@ function Minimap({
1546
1640
  }
1547
1641
  }
1548
1642
  return bucketed;
1549
- }, [displayRows, safeDur]);
1643
+ }, [displayRows, safeDur, rawIdleBands]);
1550
1644
 
1551
1645
  /* Sequential layout with a minimum displayed width per segment.
1552
1646
  - Raw proportional width = (seg.end - seg.start) / safeDur * WIDTH
@@ -2514,6 +2608,7 @@ function TranscriptList({
2514
2608
  isSessionLive,
2515
2609
  team,
2516
2610
  model,
2611
+ rawIdleBands,
2517
2612
  }: {
2518
2613
  displayRows: DisplayRow[];
2519
2614
  rowRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
@@ -2524,6 +2619,10 @@ function TranscriptList({
2524
2619
  isSessionLive?: boolean;
2525
2620
  team?: (TimelineData & { teamName: string }) | null;
2526
2621
  model?: string;
2622
+ /** Anchor-to-anchor idle bands. The body emits IdleDivider before any
2623
+ * row whose start matches a band's end, so Codex sessions get the same
2624
+ * "Session idle X minutes" markers Claude has always had. */
2625
+ rawIdleBands?: { start: number; end: number; durationMs: number }[];
2527
2626
  }) {
2528
2627
  // Find the last collapsed turn index so we can mark it as in-progress
2529
2628
  // when the session is live.
@@ -2537,9 +2636,38 @@ function TranscriptList({
2537
2636
  }
2538
2637
  }
2539
2638
 
2639
+ const idleBandList = (rawIdleBands ?? []).slice().sort((a, b) => a.start - b.start);
2640
+ /** Returns the durationMs of the idle band that ends at (or just before)
2641
+ * the given row's tOffset, or null if no band matches. Tolerance is 1s
2642
+ * to absorb Date.parse rounding differences between anchor.ms and the
2643
+ * presentation row's tOffsetMs. */
2644
+ const idleBeforeOffset = (tOff: number | undefined): number | null => {
2645
+ if (tOff === undefined) return null;
2646
+ for (const b of idleBandList) {
2647
+ if (Math.abs(b.end - tOff) < 1000) return b.durationMs;
2648
+ }
2649
+ return null;
2650
+ };
2651
+ /** Pull a t-offset off whichever display-row variant we have. */
2652
+ const rowTOffset = (d: DisplayRow): number | undefined => {
2653
+ if (d.kind === "turn-collapsed" || d.kind === "turn-expanded-header") return d.turn.tOffsetMs;
2654
+ if (d.kind === "turn-expanded-footer") return undefined;
2655
+ return d.row.tOffsetMs;
2656
+ };
2657
+
2540
2658
  const out: React.ReactNode[] = [];
2541
2659
  for (let i = 0; i < displayRows.length; i++) {
2542
2660
  const d = displayRows[i];
2661
+ // Emit a "Session idle" divider before any row that starts right after
2662
+ // an anchor-to-anchor idle band. Skips indented (child) rows so an
2663
+ // expanded turn doesn't render dividers for its inner steps.
2664
+ const indented = d.kind === "presentation" ? d.indented : false;
2665
+ if (!indented) {
2666
+ const idleMs = idleBeforeOffset(rowTOffset(d));
2667
+ if (idleMs !== null && idleMs > IDLE_THRESHOLD_MS) {
2668
+ out.push(<IdleDivider key={`idle-before-${i}`} gapMs={idleMs} />);
2669
+ }
2670
+ }
2543
2671
 
2544
2672
  // Collapsed turn summary row
2545
2673
  if (d.kind === "turn-collapsed") {
@@ -2592,12 +2720,10 @@ function TranscriptList({
2592
2720
  continue;
2593
2721
  }
2594
2722
 
2595
- // Normal presentation row (possibly indented as a child of an expanded turn)
2723
+ // Normal presentation row (possibly indented as a child of an expanded
2724
+ // turn). Idle dividers are now emitted at the top of the loop from the
2725
+ // shared rawIdleBands list — no separate gap-before-user check needed.
2596
2726
  const r = d.row;
2597
- const gap = r.gapMs ?? 0;
2598
- if (r.kind === "user" && gap > IDLE_THRESHOLD_MS && !d.indented) {
2599
- out.push(<IdleDivider key={`idle-${rowPrimaryIndex(r)}`} gapMs={gap} />);
2600
- }
2601
2727
  const idx = rowPrimaryIndex(r);
2602
2728
  out.push(
2603
2729
  <TranscriptRow
@@ -4699,7 +4825,7 @@ function EntryDayStrip({ entry }: { entry: Entry }) {
4699
4825
  📅 {fmtDate}
4700
4826
  </span>
4701
4827
  {isTrivial ? (
4702
- <OutcomePill outcome="trivial" size="md" />
4828
+ <OutcomePill outcome="trivial" size="md" agent={entry.agent} />
4703
4829
  ) : isPending ? (
4704
4830
  <OutcomePill
4705
4831
  outcome={null}
@@ -4707,9 +4833,10 @@ function EntryDayStrip({ entry }: { entry: Entry }) {
4707
4833
  sessionId={entry.session_id}
4708
4834
  localDay={entry.local_day}
4709
4835
  size="md"
4836
+ agent={entry.agent}
4710
4837
  />
4711
4838
  ) : enr.outcome ? (
4712
- <OutcomePill outcome={enr.outcome} size="md" />
4839
+ <OutcomePill outcome={enr.outcome} size="md" agent={entry.agent} />
4713
4840
  ) : null}
4714
4841
  <span
4715
4842
  style={{
@@ -1,9 +1,20 @@
1
+ import { homedir } from "node:os";
1
2
  import { listSessions } from "@/lib/data";
2
3
  import { buildEntriesIndex } from "@/lib/entries-index";
4
+ import { agentSources } from "@claude-lens/parser/fs";
3
5
  import { SessionsGrid, type SessionRow } from "./sessions-grid";
4
6
 
5
7
  export const dynamic = "force-dynamic";
6
8
 
9
+ const HOME = homedir();
10
+ const codeStyle = {
11
+ fontFamily: "var(--font-mono)",
12
+ fontSize: 11,
13
+ background: "var(--af-border-subtle)",
14
+ padding: "1px 6px",
15
+ borderRadius: 4,
16
+ } as const;
17
+
7
18
  export default async function SessionsPage() {
8
19
  const [sessions, index] = await Promise.all([listSessions(), buildEntriesIndex()]);
9
20
 
@@ -29,18 +40,17 @@ export default async function SessionsPage() {
29
40
  All sessions
30
41
  </h1>
31
42
  <p style={{ fontSize: 13, color: "var(--af-text-secondary)", marginTop: 4 }}>
32
- {sessions.length} session{sessions.length === 1 ? "" : "s"} in{" "}
33
- <code
34
- style={{
35
- fontFamily: "var(--font-mono)",
36
- fontSize: 11,
37
- background: "var(--af-border-subtle)",
38
- padding: "1px 6px",
39
- borderRadius: 4,
40
- }}
41
- >
42
- ~/.claude/projects
43
- </code>
43
+ {sessions.length} session{sessions.length === 1 ? "" : "s"} across{" "}
44
+ {agentSources.map((source, i) => (
45
+ <span key={source.kind}>
46
+ {i > 0 && " + "}
47
+ <code style={codeStyle}>
48
+ {source.defaultRoot.startsWith(HOME)
49
+ ? "~" + source.defaultRoot.slice(HOME.length)
50
+ : source.defaultRoot}
51
+ </code>
52
+ </span>
53
+ ))}
44
54
  </p>
45
55
  </header>
46
56
  <SessionsGrid rows={rows} />
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import { useMemo, useState } from "react";
5
- import { useRouter } from "next/navigation";
6
- import type { SessionMeta } from "@claude-lens/parser";
4
+ import { useCallback, useMemo } from "react";
5
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
6
+ import type { AgentKind, SessionMeta } from "@claude-lens/parser";
7
+ import { getAgentMetadata, isAgentKind } from "@claude-lens/parser";
7
8
  import type { DayOutcome, EntryEnrichmentStatus } from "@claude-lens/entries";
8
9
  import {
9
10
  formatDuration,
@@ -15,6 +16,7 @@ import {
15
16
  import { Search, Wrench, MessagesSquare, Clock } from "lucide-react";
16
17
  import { LiveBadge } from "@/components/live-badge";
17
18
  import { TeamBadge } from "@/components/team-badge";
19
+ import { AgentBadge } from "@/components/agent-badge";
18
20
  import { DataTable, type Column } from "@/components/data-table";
19
21
  import { useViewToggle } from "@/components/view-toggle";
20
22
  import { OutcomePill, outcomePriority } from "@/components/outcome-pill";
@@ -27,21 +29,66 @@ export type SessionRow = {
27
29
  latestLocalDay: string | null;
28
30
  };
29
31
 
32
+ type SortBy = "newest" | "longest" | "most-tokens" | "outcome";
33
+
34
+ const SORT_VALUES: readonly SortBy[] = ["newest", "longest", "most-tokens", "outcome"] as const;
35
+
36
+ function isSortBy(s: string | null | undefined): s is SortBy {
37
+ return s !== null && s !== undefined && (SORT_VALUES as readonly string[]).includes(s);
38
+ }
39
+
30
40
  export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
31
41
  const router = useRouter();
32
- const [query, setQuery] = useState("");
33
- const [project, setProject] = useState("all");
34
- const [sortBy, setSortBy] = useState<"newest" | "longest" | "most-tokens" | "outcome">("newest");
42
+ const pathname = usePathname();
43
+ const searchParams = useSearchParams();
44
+
45
+ // Filter state lives in the URL — back/forward replays the same view, and
46
+ // returning from a session detail page restores what the user had picked.
47
+ const query = searchParams.get("q") ?? "";
48
+ const project = searchParams.get("project") ?? "all";
49
+ const agentParam = searchParams.get("agent");
50
+ const agent: AgentKind | "all" = isAgentKind(agentParam) ? agentParam : "all";
51
+ const sortBy: SortBy = isSortBy(searchParams.get("sort"))
52
+ ? (searchParams.get("sort") as SortBy)
53
+ : "newest";
35
54
  const { mode: viewMode, toggle: viewToggle } = useViewToggle("cclens:sessions:view");
36
55
 
56
+ const updateParam = useCallback(
57
+ (key: string, value: string, fallback: string) => {
58
+ const next = new URLSearchParams(searchParams.toString());
59
+ if (!value || value === fallback) next.delete(key);
60
+ else next.set(key, value);
61
+ const qs = next.toString();
62
+ // replace, not push — we don't want a history entry per keystroke.
63
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
64
+ },
65
+ [pathname, router, searchParams],
66
+ );
67
+
68
+ const setQuery = (v: string) => updateParam("q", v, "");
69
+ const setProject = (v: string) => updateParam("project", v, "all");
70
+ const setAgent = (v: AgentKind | "all") => updateParam("agent", v, "all");
71
+ const setSortBy = (v: SortBy) => updateParam("sort", v, "newest");
72
+
37
73
  const projects = useMemo(() => {
38
74
  const s = new Set(rows.map((r) => r.session.projectName));
39
75
  return ["all", ...Array.from(s).sort()];
40
76
  }, [rows]);
41
77
 
78
+ const agentOptions = useMemo(() => {
79
+ const counts = new Map<AgentKind, number>();
80
+ for (const r of rows) {
81
+ const k = (r.session.agent ?? "claude-code") as AgentKind;
82
+ counts.set(k, (counts.get(k) ?? 0) + 1);
83
+ }
84
+ return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);
85
+ }, [rows]);
86
+
42
87
  const filtered = useMemo(() => {
43
88
  let items = rows.slice();
44
89
  if (project !== "all") items = items.filter((r) => r.session.projectName === project);
90
+ if (agent !== "all")
91
+ items = items.filter((r) => (r.session.agent ?? "claude-code") === agent);
45
92
  if (query) {
46
93
  const q = query.toLowerCase();
47
94
  items = items.filter(
@@ -78,7 +125,7 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
78
125
  else if (sortBy === "outcome")
79
126
  items.sort((a, b) => outcomePriority(b.outcome) - outcomePriority(a.outcome));
80
127
  return items;
81
- }, [rows, project, query, sortBy]);
128
+ }, [rows, project, agent, query, sortBy]);
82
129
 
83
130
  return (
84
131
  <div>
@@ -116,6 +163,20 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
116
163
  </option>
117
164
  ))}
118
165
  </select>
166
+ {agentOptions.length > 1 && (
167
+ <select
168
+ value={agent}
169
+ onChange={(e) => setAgent(e.target.value as typeof agent)}
170
+ title="Filter by source agent"
171
+ >
172
+ <option value="all">All agents</option>
173
+ {agentOptions.map(([k, count]) => (
174
+ <option key={k} value={k}>
175
+ {agentLabel(k)} ({count})
176
+ </option>
177
+ ))}
178
+ </select>
179
+ )}
119
180
  <select value={sortBy} onChange={(e) => setSortBy(e.target.value as typeof sortBy)}>
120
181
  <option value="newest">Newest</option>
121
182
  <option value="longest">Longest</option>
@@ -196,6 +257,7 @@ const sessionTableColumns: Column<SessionRow>[] = [
196
257
  {prettyProjectName(r.session.projectName)}
197
258
  </span>
198
259
  <TeamBadge session={r.session} />
260
+ <AgentBadge agent={r.session.agent} />
199
261
  </div>
200
262
  <div
201
263
  style={{
@@ -319,7 +381,9 @@ const sessionTableColumns: Column<SessionRow>[] = [
319
381
  ];
320
382
 
321
383
  function OutcomeCell({ row }: { row: SessionRow }) {
322
- if (row.outcome) return <OutcomePill outcome={row.outcome} size="sm" label="text" />;
384
+ if (row.outcome) {
385
+ return <OutcomePill outcome={row.outcome} size="sm" label="text" agent={row.session.agent} />;
386
+ }
323
387
  if (row.enrichmentStatus && row.latestLocalDay && row.enrichmentStatus !== "skipped_trivial") {
324
388
  return (
325
389
  <OutcomePill
@@ -328,6 +392,7 @@ function OutcomeCell({ row }: { row: SessionRow }) {
328
392
  sessionId={row.session.id}
329
393
  localDay={row.latestLocalDay}
330
394
  size="sm"
395
+ agent={row.session.agent}
331
396
  />
332
397
  );
333
398
  }
@@ -383,6 +448,7 @@ function SessionCard({ row }: { row: SessionRow }) {
383
448
  {prettyProjectName(s.projectName)}
384
449
  </span>
385
450
  <TeamBadge session={s} linkable={false} />
451
+ <AgentBadge agent={s.agent} />
386
452
  </div>
387
453
  <div
388
454
  style={{
@@ -400,7 +466,7 @@ function SessionCard({ row }: { row: SessionRow }) {
400
466
  {(outcome || showPending) && (
401
467
  <div onClick={(e) => e.stopPropagation()}>
402
468
  {outcome ? (
403
- <OutcomePill outcome={outcome} size="md" />
469
+ <OutcomePill outcome={outcome} size="md" agent={s.agent} />
404
470
  ) : showPending ? (
405
471
  <OutcomePill
406
472
  outcome={null}
@@ -408,6 +474,7 @@ function SessionCard({ row }: { row: SessionRow }) {
408
474
  sessionId={s.id}
409
475
  localDay={latestLocalDay!}
410
476
  size="md"
477
+ agent={s.agent}
411
478
  />
412
479
  ) : null}
413
480
  </div>
@@ -481,3 +548,9 @@ function Stat({ icon, label }: { icon: React.ReactNode; label: string }) {
481
548
  </span>
482
549
  );
483
550
  }
551
+
552
+ // Registry-driven label — reads displayName off the agent metadata so
553
+ // new agents auto-show up with their declared name.
554
+ function agentLabel(kind: AgentKind): string {
555
+ return getAgentMetadata(kind)?.displayName ?? kind;
556
+ }
@@ -4,11 +4,13 @@ import { useState } from "react";
4
4
  type Initial = {
5
5
  enabled: boolean;
6
6
  autoBackfillLastWeek: boolean;
7
+ autoBackfillYesterday: boolean;
7
8
  };
8
9
 
9
10
  export function AiFeaturesForm({ initial }: { initial: Initial }) {
10
11
  const [enabled, setEnabled] = useState(initial.enabled);
11
12
  const [autoBackfillLastWeek, setAutoBackfillLastWeek] = useState(initial.autoBackfillLastWeek);
13
+ const [autoBackfillYesterday, setAutoBackfillYesterday] = useState(initial.autoBackfillYesterday);
12
14
  const [saving, setSaving] = useState(false);
13
15
  const [savedMsg, setSavedMsg] = useState<string | null>(null);
14
16
 
@@ -20,7 +22,7 @@ export function AiFeaturesForm({ initial }: { initial: Initial }) {
20
22
  method: "PUT",
21
23
  headers: { "content-type": "application/json" },
22
24
  body: JSON.stringify({
23
- ai_features: { enabled, autoBackfillLastWeek },
25
+ ai_features: { enabled, autoBackfillLastWeek, autoBackfillYesterday },
24
26
  }),
25
27
  });
26
28
  setSaving(false);
@@ -34,6 +36,23 @@ export function AiFeaturesForm({ initial }: { initial: Initial }) {
34
36
  <span>Enable AI digests and enrichment</span>
35
37
  </label>
36
38
 
39
+ <label className={`flex items-start gap-2 ${enabled ? "" : "opacity-50"}`}>
40
+ <input
41
+ type="checkbox"
42
+ checked={autoBackfillYesterday}
43
+ disabled={!enabled}
44
+ onChange={e => setAutoBackfillYesterday(e.target.checked)}
45
+ />
46
+ <span>
47
+ Auto-backfill yesterday's digest on daemon start
48
+ <span className="block text-xs text-gray-500 mt-0.5">
49
+ Generates yesterday's enriched entries and day digest. Runs once
50
+ per daemon boot. Without this, yesterday's digest only appears
51
+ after you load the homepage.
52
+ </span>
53
+ </span>
54
+ </label>
55
+
37
56
  <label className={`flex items-start gap-2 ${enabled ? "" : "opacity-50"}`}>
38
57
  <input
39
58
  type="checkbox"
@@ -16,6 +16,7 @@ export default function SettingsPage() {
16
16
  <AiFeaturesForm initial={{
17
17
  enabled: s.ai_features.enabled,
18
18
  autoBackfillLastWeek: s.ai_features.autoBackfillLastWeek,
19
+ autoBackfillYesterday: s.ai_features.autoBackfillYesterday,
19
20
  }} />
20
21
  </section>
21
22
  </main>