fleetlens 0.6.4 → 0.7.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 (226) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +1 -0
  3. package/app/apps/web/.next/build-manifest.json +5 -5
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +6 -0
  6. package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +2 -2
  7. package/app/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +2 -2
  17. package/app/apps/web/.next/server/app/_not-found/page.js +3 -3
  18. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/app/apps/web/.next/server/app/api/ask/route.js +2 -2
  21. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +3 -3
  23. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +3 -3
  25. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +1 -1
  27. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +3 -3
  29. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +1 -1
  31. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/events/route.js +2 -2
  33. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  35. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  36. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  37. package/app/apps/web/.next/server/app/api/settings/route.js +3 -3
  38. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  39. package/app/apps/web/.next/server/app/changelog/page/app-paths-manifest.json +3 -0
  40. package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +17 -0
  41. package/app/apps/web/.next/server/app/changelog/page/next-font-manifest.json +6 -0
  42. package/app/apps/web/.next/server/app/changelog/page/react-loadable-manifest.json +1 -0
  43. package/app/apps/web/.next/server/app/changelog/page/server-reference-manifest.json +4 -0
  44. package/app/apps/web/.next/server/app/changelog/page.js +17 -0
  45. package/app/apps/web/.next/server/app/changelog/page.js.map +5 -0
  46. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -0
  47. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +3 -0
  48. package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +2 -2
  49. package/app/apps/web/.next/server/app/day/[date]/page.js +4 -4
  50. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  51. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  52. package/app/apps/web/.next/server/app/day/page/build-manifest.json +2 -2
  53. package/app/apps/web/.next/server/app/day/page.js +3 -3
  54. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  55. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  56. package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +2 -2
  57. package/app/apps/web/.next/server/app/digest/[date]/page.js +3 -3
  58. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  59. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  60. package/app/apps/web/.next/server/app/digest/page/build-manifest.json +2 -2
  61. package/app/apps/web/.next/server/app/digest/page.js +3 -3
  62. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  63. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  64. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +2 -2
  65. package/app/apps/web/.next/server/app/insights/[key]/page.js +3 -3
  66. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  67. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  68. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +2 -2
  69. package/app/apps/web/.next/server/app/insights/page.js +3 -3
  70. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  71. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  72. package/app/apps/web/.next/server/app/page/build-manifest.json +2 -2
  73. package/app/apps/web/.next/server/app/page.js +4 -4
  74. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  75. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  76. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +2 -2
  77. package/app/apps/web/.next/server/app/parallelism/page.js +3 -3
  78. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  79. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  80. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +2 -2
  81. package/app/apps/web/.next/server/app/projects/[slug]/page.js +4 -4
  82. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  83. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  84. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +2 -2
  85. package/app/apps/web/.next/server/app/projects/page.js +4 -4
  86. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  87. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  88. package/app/apps/web/.next/server/app/runs/page/build-manifest.json +2 -2
  89. package/app/apps/web/.next/server/app/runs/page.js +3 -3
  90. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  91. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  92. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +2 -2
  93. package/app/apps/web/.next/server/app/sessions/[id]/page.js +4 -4
  94. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  95. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  96. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +2 -2
  97. package/app/apps/web/.next/server/app/sessions/page.js +4 -4
  98. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  99. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  100. package/app/apps/web/.next/server/app/settings/page/build-manifest.json +2 -2
  101. package/app/apps/web/.next/server/app/settings/page.js +3 -3
  102. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  103. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  104. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +2 -2
  105. package/app/apps/web/.next/server/app/usage/page.js +3 -3
  106. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  107. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  108. package/app/apps/web/.next/server/app-paths-manifest.json +1 -0
  109. package/app/apps/web/.next/server/chunks/{[externals]__0o43ejh._.js → [externals]__0fpbr8-._.js} +2 -2
  110. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0-vgrh1._.js +1 -1
  111. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.z48ue._.js +3 -0
  112. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0dctov1._.js +5 -0
  113. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e8n_x4._.js +1 -1
  114. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +2 -2
  115. package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +1 -1
  116. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +1 -1
  117. package/app/apps/web/.next/server/chunks/packages_09hmrqx._.js +3 -0
  118. package/app/apps/web/.next/server/chunks/packages_118y1lp._.js +3 -0
  119. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +26 -17
  120. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_122oe0-.js +3 -0
  121. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0vvlth5._.js → [root-of-the-server]__02~v50d._.js} +2 -2
  122. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__09rjz7_._.js +3 -0
  124. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0e.gxk3._.js +3 -0
  125. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0p1x6v2._.js +3 -0
  126. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
  127. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
  128. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0sp6.jt._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +3 -0
  130. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
  131. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wwa6gh._.js +1 -1
  132. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ytdqxt._.js +3 -0
  133. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv-a8z._.js +12 -0
  134. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10~b4bf._.js +12 -0
  135. package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +1 -1
  136. package/app/apps/web/.next/server/chunks/ssr/_0woap-3._.js +1 -1
  137. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  138. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_changelog_page_actions_12_04gy.js +3 -0
  139. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  140. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_settings_ai-features-form_tsx_01p699p._.js +1 -1
  141. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +1 -1
  142. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
  143. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_changelog-mark-read_tsx_0hm41sv._.js +3 -0
  144. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0m67-xi._.js +3 -0
  145. package/app/apps/web/.next/server/chunks/ssr/{node_modules_0i2xw~e._.js → node_modules_0fx6-.x._.js} +2 -2
  146. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uwo3v3.js +4 -0
  147. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  148. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_10_oegc._.js +3 -0
  149. package/app/apps/web/.next/server/middleware-build-manifest.js +5 -5
  150. package/app/apps/web/.next/server/pages/500.html +1 -1
  151. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  152. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  153. package/app/apps/web/.next/static/chunks/0.3mqbj24kwtc.js +6 -0
  154. package/app/apps/web/.next/static/chunks/05b689hq3x49w.js +1 -0
  155. package/app/apps/web/.next/static/chunks/07-fq4im3pmjp.js +4 -0
  156. package/app/apps/web/.next/static/chunks/{0mykrrv810ra-.js → 0kjot8367asxe.js} +1 -1
  157. package/app/apps/web/.next/static/chunks/0n3-prt61k7pr.js +1 -0
  158. package/app/apps/web/.next/static/chunks/{0_7303jr0sf0w.js → 0q2vd~aj.1hpz.js} +3 -3
  159. package/app/apps/web/.next/static/chunks/0qmh5da6gfm56.js +1 -0
  160. package/app/apps/web/.next/static/chunks/{0k9pksi_i.odf.js → 111-bgx08j2nq.js} +1 -1
  161. package/app/apps/web/.next/static/chunks/179_t_igg8ghh.js +14 -0
  162. package/app/apps/web/.next/static/chunks/{turbopack-0_p36yduv.waj.js → turbopack-00w.w1u3c~d-6.js} +1 -1
  163. package/app/apps/web/app/api/ask/route.ts +1 -1
  164. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +2 -1
  165. package/app/apps/web/app/api/settings/route.ts +1 -0
  166. package/app/apps/web/app/changelog/page.tsx +238 -0
  167. package/app/apps/web/app/insights/[key]/page.tsx +8 -2
  168. package/app/apps/web/app/layout.tsx +12 -7
  169. package/app/apps/web/app/page.tsx +19 -15
  170. package/app/apps/web/app/parallelism/gantt-chart.tsx +7 -18
  171. package/app/apps/web/app/projects/projects-view.tsx +3 -0
  172. package/app/apps/web/app/sessions/[id]/session-view.tsx +168 -41
  173. package/app/apps/web/app/sessions/page.tsx +22 -12
  174. package/app/apps/web/app/sessions/sessions-grid.tsx +82 -9
  175. package/app/apps/web/app/settings/ai-features-form.tsx +20 -1
  176. package/app/apps/web/app/settings/page.tsx +1 -0
  177. package/app/apps/web/app/usage/page.tsx +161 -52
  178. package/app/apps/web/components/agent-badge.tsx +43 -0
  179. package/app/apps/web/components/agent-icon.tsx +38 -0
  180. package/app/apps/web/components/agent-mix-chip.tsx +42 -0
  181. package/app/apps/web/components/{ask-claude.tsx → ask.tsx} +13 -12
  182. package/app/apps/web/components/changelog-icon.tsx +62 -0
  183. package/app/apps/web/components/changelog-mark-read.tsx +12 -0
  184. package/app/apps/web/components/day-digest.tsx +1 -1
  185. package/app/apps/web/components/live-sessions-widget.tsx +5 -1
  186. package/app/apps/web/components/outcome-pill.tsx +7 -2
  187. package/app/apps/web/components/sidebar.tsx +8 -1
  188. package/app/apps/web/components/week-digest.tsx +2 -0
  189. package/app/apps/web/lib/ai/session-summary.ts +1 -1
  190. package/app/apps/web/lib/calibration-data.ts +4 -64
  191. package/app/apps/web/lib/changelog-data.generated.ts +3 -0
  192. package/app/apps/web/lib/changelog.ts +67 -0
  193. package/app/apps/web/lib/cycle-peaks.test.ts +185 -0
  194. package/app/apps/web/lib/cycle-peaks.ts +104 -0
  195. package/app/apps/web/lib/data.ts +7 -8
  196. package/app/apps/web/lib/date-range.ts +9 -2
  197. package/app/apps/web/lib/usage-data.ts +24 -1
  198. package/app/apps/web/lib/validate-settings.ts +1 -0
  199. package/app/apps/web/package.json +1 -1
  200. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  201. package/dist/daemon-worker.js +3162 -2354
  202. package/dist/index.js +4419 -3742
  203. package/package.json +1 -1
  204. package/app/apps/web/.next/server/chunks/[externals]__0_xztrf._.js +0 -3
  205. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +0 -5
  206. package/app/apps/web/.next/server/chunks/packages_0e7q9u_._.js +0 -3
  207. package/app/apps/web/.next/server/chunks/packages_0n6p2ot._.js +0 -3
  208. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +0 -3
  209. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-94ji~._.js +0 -3
  210. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_6.inc._.js +0 -3
  211. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +0 -3
  212. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0jdyefm._.js +0 -12
  213. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ln9jsw._.js +0 -3
  214. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u5d7de._.js +0 -12
  215. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~_8vff._.js +0 -3
  216. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11v.eqw._.js +0 -3
  217. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_usage-data_ts_0xtv99n._.js +0 -3
  218. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +0 -3
  219. package/app/apps/web/.next/static/chunks/04m3slg_5ph02.js +0 -1
  220. package/app/apps/web/.next/static/chunks/0coa1800xqgfj.js +0 -1
  221. package/app/apps/web/.next/static/chunks/0k1psab.bybh9.js +0 -14
  222. package/app/apps/web/.next/static/chunks/0xfu09fn_358k.js +0 -4
  223. package/app/apps/web/.next/static/chunks/17u2ww8ya5dv1.js +0 -6
  224. /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → tiK9aMHXkgnKLNmxj9J1P}/_buildManifest.js +0 -0
  225. /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → tiK9aMHXkgnKLNmxj9J1P}/_clientMiddlewareManifest.js +0 -0
  226. /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → tiK9aMHXkgnKLNmxj9J1P}/_ssgManifest.js +0 -0
@@ -1,17 +1,20 @@
1
1
  /**
2
- * Claude Code usage page — historical utilization analytics.
2
+ * Usage page — historical utilization analytics, per agent.
3
3
  *
4
- * Current utilization lives in the sidebar (always visible). This page
5
- * is dedicated to historical views, trend analysis, and leadership
6
- * reporting metrics derived from the daemon's snapshot log.
4
+ * One tab per agent that has snapshots on disk. Each tab renders the
5
+ * same dashboard layout (burndown charts + previous-cycles trend) so
6
+ * Claude Code and Codex are peers, not primary/secondary.
7
7
  *
8
- * Reads from ~/.cclens/usage.jsonl — no API endpoint needed.
8
+ * Reads from ~/.cclens/usage.jsonl — the daemon writes both agents'
9
+ * snapshots there, tagged with `agent`. Filtering happens at read time.
9
10
  */
10
11
 
11
12
  import { Activity } from "lucide-react";
13
+ import Link from "next/link";
12
14
  import {
13
15
  readUsageSnapshots,
14
- latestUsageSnapshot,
16
+ readUsageSnapshotsByAgent,
17
+ latestUsageSnapshotByAgent,
15
18
  readCachedPlanTier,
16
19
  PLAN_TIER_LABELS,
17
20
  } from "@/lib/usage-data";
@@ -22,16 +25,53 @@ import {
22
25
  } from "@/lib/calibration-data";
23
26
  import { UsageChartsDashboard } from "@/components/usage-charts-dashboard";
24
27
  import { PreviousCyclesTrend } from "@/components/previous-cycles-trend";
28
+ import {
29
+ type AgentKind,
30
+ agentMetadata,
31
+ getAgentMetadata,
32
+ isAgentKind,
33
+ } from "@claude-lens/parser";
25
34
 
26
35
  export const dynamic = "force-dynamic";
27
36
 
28
- export default async function UsagePage() {
29
- const snapshots = readUsageSnapshots();
30
- const latest = latestUsageSnapshot();
31
- const tier = readCachedPlanTier();
32
- const calibration = await readCalibrationDump();
33
- const predicted = predictedSeriesFor(calibration);
34
- const cycles7d = previousCyclesTrend(calibration, "7d");
37
+ function agentLabel(kind: AgentKind): string {
38
+ return getAgentMetadata(kind)?.displayName ?? kind;
39
+ }
40
+
41
+ function agentAccent(kind: AgentKind): string {
42
+ return getAgentMetadata(kind)?.accentColor ?? "var(--af-text-tertiary)";
43
+ }
44
+
45
+ export default async function UsagePage({
46
+ searchParams,
47
+ }: {
48
+ searchParams: Promise<{ agent?: string }>;
49
+ }) {
50
+ const params = await searchParams;
51
+ const allSnapshots = readUsageSnapshots();
52
+ const agentsWithData = new Set<AgentKind>();
53
+ for (const s of allSnapshots) {
54
+ const k = (s.agent ?? "claude-code") as AgentKind;
55
+ agentsWithData.add(k);
56
+ }
57
+ // Always show Claude in the tab strip even if empty — it's the canonical
58
+ // source. Codex appears only once it has at least one snapshot.
59
+ agentsWithData.add("claude-code");
60
+
61
+ const requested: AgentKind | undefined = isAgentKind(params.agent) ? params.agent : undefined;
62
+ const selected: AgentKind =
63
+ requested && agentsWithData.has(requested)
64
+ ? requested
65
+ : agentsWithData.has("claude-code")
66
+ ? "claude-code"
67
+ : (Array.from(agentsWithData)[0] as AgentKind);
68
+
69
+ const snapshots = readUsageSnapshotsByAgent(selected);
70
+ const latest = latestUsageSnapshotByAgent(selected);
71
+ const tier = selected === "claude-code" ? readCachedPlanTier() : null;
72
+ const calibration = selected === "claude-code" ? await readCalibrationDump() : null;
73
+ const predicted = calibration ? predictedSeriesFor(calibration) : null;
74
+ const cycles7d = calibration ? previousCyclesTrend(calibration, "7d") : [];
35
75
 
36
76
  return (
37
77
  <div
@@ -43,7 +83,6 @@ export default async function UsagePage() {
43
83
  padding: "20px 32px",
44
84
  }}
45
85
  >
46
- {/* Header */}
47
86
  <header
48
87
  style={{ display: "flex", alignItems: "baseline", gap: 14, flexWrap: "wrap" }}
49
88
  >
@@ -64,7 +103,7 @@ export default async function UsagePage() {
64
103
  <span style={{ fontSize: 11, color: "var(--af-text-tertiary)" }}>
65
104
  historical plan utilization · current usage in sidebar
66
105
  </span>
67
- {tier && (
106
+ {tier && selected === "claude-code" && (
68
107
  <span
69
108
  style={{
70
109
  marginLeft: "auto",
@@ -90,14 +129,39 @@ export default async function UsagePage() {
90
129
  )}
91
130
  </span>
92
131
  )}
132
+ {selected === "codex" && latest?.plan_type && (
133
+ <span
134
+ style={{
135
+ marginLeft: "auto",
136
+ display: "inline-flex",
137
+ alignItems: "center",
138
+ gap: 8,
139
+ fontSize: 12,
140
+ padding: "4px 10px",
141
+ border: "1px solid var(--af-border-subtle)",
142
+ borderRadius: 999,
143
+ fontFamily: "var(--font-mono)",
144
+ }}
145
+ >
146
+ <span style={{ color: "var(--af-text-tertiary)" }}>plan</span>
147
+ <span style={{ color: "var(--af-text)", fontWeight: 600 }}>
148
+ Codex {latest.plan_type}
149
+ </span>
150
+ </span>
151
+ )}
93
152
  </header>
94
153
 
154
+ {/* Agent tab strip — one Link per agent that has snapshots. */}
155
+ <AgentTabs agents={Array.from(agentsWithData)} selected={selected} />
156
+
95
157
  {!latest ? (
96
- <EmptyState />
158
+ <EmptyState agent={selected} />
97
159
  ) : (
98
160
  <>
99
- <PreviousCyclesTrend windowLabel="7d" cycles={cycles7d} />
100
- <UsageChartsDashboard snapshots={snapshots} predicted={predicted} />
161
+ {cycles7d.length > 0 && (
162
+ <PreviousCyclesTrend windowLabel="7d" cycles={cycles7d} />
163
+ )}
164
+ <UsageChartsDashboard snapshots={snapshots} predicted={predicted ?? undefined} />
101
165
  <div
102
166
  style={{
103
167
  fontSize: 11,
@@ -106,7 +170,7 @@ export default async function UsagePage() {
106
170
  }}
107
171
  suppressHydrationWarning
108
172
  >
109
- Last daemon poll: {new Date(latest.captured_at).toLocaleString()} ·{" "}
173
+ Last {agentLabel(selected)} poll: {new Date(latest.captured_at).toLocaleString()} ·{" "}
110
174
  {snapshots.length} snapshot{snapshots.length === 1 ? "" : "s"} on disk
111
175
  </div>
112
176
  </>
@@ -115,7 +179,68 @@ export default async function UsagePage() {
115
179
  );
116
180
  }
117
181
 
118
- function EmptyState() {
182
+ function AgentTabs({ agents, selected }: { agents: AgentKind[]; selected: AgentKind }) {
183
+ // Claude first, then everything else alphabetically.
184
+ const sorted = [...agents].sort((a, b) => {
185
+ if (a === "claude-code") return -1;
186
+ if (b === "claude-code") return 1;
187
+ return a.localeCompare(b);
188
+ });
189
+ return (
190
+ <div
191
+ role="tablist"
192
+ style={{
193
+ display: "inline-flex",
194
+ gap: 4,
195
+ padding: 4,
196
+ background: "var(--af-border-subtle)",
197
+ borderRadius: 8,
198
+ alignSelf: "flex-start",
199
+ }}
200
+ >
201
+ {sorted.map((kind) => {
202
+ const isActive = kind === selected;
203
+ return (
204
+ <Link
205
+ key={kind}
206
+ role="tab"
207
+ aria-selected={isActive}
208
+ href={`/usage?agent=${kind}`}
209
+ style={{
210
+ display: "inline-flex",
211
+ alignItems: "center",
212
+ gap: 6,
213
+ padding: "6px 14px",
214
+ borderRadius: 6,
215
+ fontSize: 12,
216
+ fontWeight: 600,
217
+ letterSpacing: "0.01em",
218
+ color: isActive ? "var(--af-text)" : "var(--af-text-tertiary)",
219
+ background: isActive ? "var(--af-surface)" : "transparent",
220
+ boxShadow: isActive ? "0 1px 2px rgba(0,0,0,0.06)" : "none",
221
+ textDecoration: "none",
222
+ transition: "all 0.12s ease",
223
+ }}
224
+ >
225
+ <span
226
+ style={{
227
+ display: "inline-block",
228
+ width: 8,
229
+ height: 8,
230
+ borderRadius: 999,
231
+ background: agentAccent(kind),
232
+ opacity: isActive ? 1 : 0.6,
233
+ }}
234
+ />
235
+ {agentLabel(kind)}
236
+ </Link>
237
+ );
238
+ })}
239
+ </div>
240
+ );
241
+ }
242
+
243
+ function EmptyState({ agent }: { agent: AgentKind }) {
119
244
  return (
120
245
  <div
121
246
  className="af-card"
@@ -131,7 +256,7 @@ function EmptyState() {
131
256
  color: "var(--af-text)",
132
257
  }}
133
258
  >
134
- No usage data yet
259
+ No {agentLabel(agent)} usage data yet
135
260
  </div>
136
261
  <p
137
262
  style={{
@@ -140,43 +265,27 @@ function EmptyState() {
140
265
  marginTop: 8,
141
266
  }}
142
267
  >
143
- Start the polling daemon to begin collecting metrics every 5 minutes:
268
+ {agent === "claude-code"
269
+ ? "Start the polling daemon to begin collecting metrics every 5 minutes:"
270
+ : "The daemon picks up Codex usage from disk on every poll cycle. Run a Codex session and the data will appear here within minutes."}
144
271
  </p>
145
- <pre
146
- style={{
147
- display: "inline-block",
148
- marginTop: 14,
149
- background: "var(--background)",
150
- border: "1px solid var(--af-border-subtle)",
151
- padding: "8px 16px",
152
- borderRadius: 6,
153
- fontSize: 13,
154
- fontFamily: "var(--font-mono)",
155
- color: "var(--af-text)",
156
- }}
157
- >
158
- cclens daemon start
159
- </pre>
160
- <p
161
- style={{
162
- fontSize: 11,
163
- color: "var(--af-text-tertiary)",
164
- marginTop: 16,
165
- }}
166
- >
167
- For a one-shot snapshot without the daemon, run{" "}
168
- <code
272
+ {agent === "claude-code" && (
273
+ <pre
169
274
  style={{
275
+ display: "inline-block",
276
+ marginTop: 14,
170
277
  background: "var(--background)",
171
- padding: "1px 6px",
172
- borderRadius: 4,
278
+ border: "1px solid var(--af-border-subtle)",
279
+ padding: "8px 16px",
280
+ borderRadius: 6,
281
+ fontSize: 13,
173
282
  fontFamily: "var(--font-mono)",
283
+ color: "var(--af-text)",
174
284
  }}
175
285
  >
176
- cclens usage
177
- </code>
178
- .
179
- </p>
286
+ fleetlens daemon start
287
+ </pre>
288
+ )}
180
289
  </div>
181
290
  );
182
291
  }
@@ -0,0 +1,43 @@
1
+ import type { AgentKind } from "@claude-lens/parser";
2
+ import { getAgentMetadata } from "@claude-lens/parser";
3
+
4
+ type AgentBadgeProps = {
5
+ /** Source agent. Undefined is treated as legacy claude-code (no badge). */
6
+ agent?: AgentKind;
7
+ };
8
+
9
+ /**
10
+ * Inline agent pill. Registry-driven: pulls accentColor + shortLabel
11
+ * directly off the AgentSource. Adding a new agent makes a new badge
12
+ * appear automatically with the source's declared color — no edits
13
+ * needed here.
14
+ *
15
+ * Returns null for "claude-code" (legacy default-hide so existing
16
+ * Claude rows render unchanged) and for unknown kinds.
17
+ */
18
+ export function AgentBadge({ agent }: AgentBadgeProps) {
19
+ if (!agent || agent === "claude-code") return null;
20
+ const meta = getAgentMetadata(agent);
21
+ if (!meta) return null;
22
+ return (
23
+ <span
24
+ style={{
25
+ display: "inline-flex",
26
+ alignItems: "center",
27
+ borderRadius: 4,
28
+ padding: "2px 6px",
29
+ fontSize: 10,
30
+ letterSpacing: "0.04em",
31
+ textTransform: "uppercase",
32
+ whiteSpace: "nowrap",
33
+ fontWeight: 600,
34
+ color: meta.accentColor,
35
+ background: `color-mix(in srgb, ${meta.accentColor} 12%, transparent)`,
36
+ border: `1px solid color-mix(in srgb, ${meta.accentColor} 30%, transparent)`,
37
+ }}
38
+ title={`Source: ${meta.displayName}`}
39
+ >
40
+ {meta.shortLabel}
41
+ </span>
42
+ );
43
+ }
@@ -0,0 +1,38 @@
1
+ import type { AgentKind } from "@claude-lens/parser";
2
+ import { getAgentMetadata } from "@claude-lens/parser";
3
+
4
+ type Props = {
5
+ agent?: AgentKind;
6
+ /** Glyph size in px. Defaults to inherit-from-context (12 typical). */
7
+ size?: number;
8
+ /** Spoken label override; defaults to the agent's displayName. */
9
+ title?: string;
10
+ };
11
+
12
+ /** Single-glyph agent indicator: ✦ for Claude, ◆ for Codex (or whatever
13
+ * the registered metadata declares), painted in the agent's accent color.
14
+ * Returns null for unknown / undefined agents. */
15
+ export function AgentIcon({ agent, size = 11, title }: Props) {
16
+ if (!agent) return null;
17
+ const meta = getAgentMetadata(agent);
18
+ if (!meta) return null;
19
+ return (
20
+ <span
21
+ aria-hidden
22
+ title={title ?? meta.displayName}
23
+ style={{
24
+ display: "inline-flex",
25
+ alignItems: "center",
26
+ justifyContent: "center",
27
+ flexShrink: 0,
28
+ width: size + 2,
29
+ height: size + 2,
30
+ fontSize: size,
31
+ lineHeight: 1,
32
+ color: meta.accentColor,
33
+ }}
34
+ >
35
+ {meta.iconChar}
36
+ </span>
37
+ );
38
+ }
@@ -0,0 +1,42 @@
1
+ import type { AgentBreakdown } from "@claude-lens/parser";
2
+ import { getAgentMetadata } from "@claude-lens/parser";
3
+
4
+ /**
5
+ * Inline chip that surfaces "12 Claude · 3 Codex" on a project row when
6
+ * the project has activity from more than one source. Registry-driven:
7
+ * shortLabel + accentColor come from each AgentSource, so adding a new
8
+ * agent auto-appears here without edits.
9
+ *
10
+ * Returns null when there's only one agent — keeps single-agent rows
11
+ * visually unchanged.
12
+ */
13
+ export function AgentMixChip({ perAgent }: { perAgent: AgentBreakdown[] }) {
14
+ if (perAgent.length < 2) return null;
15
+ return (
16
+ <span
17
+ style={{
18
+ display: "inline-flex",
19
+ alignItems: "center",
20
+ gap: 4,
21
+ fontSize: 10,
22
+ fontFamily: "var(--font-mono)",
23
+ color: "var(--af-text-tertiary)",
24
+ whiteSpace: "nowrap",
25
+ }}
26
+ title="Sessions split by source agent"
27
+ >
28
+ {perAgent.map((row, i) => {
29
+ const meta = getAgentMetadata(row.agent);
30
+ const label = meta?.shortLabel ?? row.agent;
31
+ const color = meta?.accentColor ?? "inherit";
32
+ return (
33
+ <span key={row.agent}>
34
+ {i > 0 && <span style={{ margin: "0 4px", opacity: 0.5 }}>·</span>}
35
+ <span style={{ color, fontWeight: 600 }}>{row.sessions}</span>{" "}
36
+ {label}
37
+ </span>
38
+ );
39
+ })}
40
+ </span>
41
+ );
42
+ }
@@ -1,15 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * "Ask Claude" drawer for session analysis.
4
+ * Ask drawer for session analysis.
5
5
  *
6
6
  * Renders as a right-side panel with:
7
7
  * - Quick-action buttons (Identify errors, Analyze performance, etc.)
8
8
  * - Free-form textarea for custom questions
9
9
  * - Streaming markdown response
10
10
  *
11
- * Calls POST /api/ask which spawns `claude -p` under the hood, using
12
- * the user's local keychain auth. No API key management required.
11
+ * Calls POST /api/ask which spawns `claude -p` under the hood today;
12
+ * the analyzer becomes pluggable when the LLM runner abstraction lands.
13
+ * The subject session can come from any registered agent.
13
14
  */
14
15
 
15
16
  import React, { useCallback, useRef, useState } from "react";
@@ -19,9 +20,9 @@ import {
19
20
  AlertTriangle,
20
21
  BarChart3,
21
22
  Lightbulb,
22
- MessageCircle,
23
23
  Route,
24
24
  Send,
25
+ Sparkles,
25
26
  X,
26
27
  Loader2,
27
28
  } from "lucide-react";
@@ -58,7 +59,7 @@ const QUICK_ACTIONS = [
58
59
  },
59
60
  ];
60
61
 
61
- export function AskClaudeButton({ onClick }: { onClick: () => void }) {
62
+ export function AskButton({ onClick }: { onClick: () => void }) {
62
63
  return (
63
64
  <button
64
65
  type="button"
@@ -86,13 +87,13 @@ export function AskClaudeButton({ onClick }: { onClick: () => void }) {
86
87
  e.currentTarget.style.color = "var(--af-accent)";
87
88
  }}
88
89
  >
89
- <MessageCircle size={13} />
90
- Ask Claude
90
+ <Sparkles size={13} />
91
+ Ask
91
92
  </button>
92
93
  );
93
94
  }
94
95
 
95
- export function AskClaudeDrawer({
96
+ export function AskDrawer({
96
97
  sessionId,
97
98
  onClose,
98
99
  }: {
@@ -232,7 +233,7 @@ export function AskClaudeDrawer({
232
233
  flexShrink: 0,
233
234
  }}
234
235
  >
235
- <MessageCircle size={16} color="var(--af-accent)" />
236
+ <Sparkles size={16} color="var(--af-accent)" />
236
237
  <span
237
238
  style={{
238
239
  fontSize: 14,
@@ -241,7 +242,7 @@ export function AskClaudeDrawer({
241
242
  flex: 1,
242
243
  }}
243
244
  >
244
- Ask Claude
245
+ Ask about this session
245
246
  </span>
246
247
  <button
247
248
  onClick={onClose}
@@ -280,8 +281,8 @@ export function AskClaudeDrawer({
280
281
  lineHeight: 1.5,
281
282
  }}
282
283
  >
283
- Ask Claude to analyze this session. Pick a quick action or type
284
- your own question.
284
+ Run an AI analysis on this session. Pick a quick action or
285
+ type your own question.
285
286
  </div>
286
287
  <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
287
288
  {QUICK_ACTIONS.map((action) => (
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useEffect, useState } from "react";
5
+ import { Megaphone } from "lucide-react";
6
+ import { STORAGE_KEY } from "@/lib/changelog";
7
+
8
+ export function ChangelogIcon({ latestVersion, active }: { latestVersion: string | null; active: boolean }) {
9
+ // Server can't read localStorage, so render no dot until after mount to
10
+ // avoid a hydration mismatch flash.
11
+ const [unread, setUnread] = useState(false);
12
+
13
+ useEffect(() => {
14
+ if (!latestVersion) {
15
+ setUnread(false);
16
+ return;
17
+ }
18
+ try {
19
+ const seen = window.localStorage.getItem(STORAGE_KEY);
20
+ setUnread(seen !== latestVersion);
21
+ } catch {
22
+ setUnread(false);
23
+ }
24
+ }, [latestVersion]);
25
+
26
+ return (
27
+ <Link
28
+ href="/changelog"
29
+ aria-label={unread ? "Changelog (new entries)" : "Changelog"}
30
+ title={unread ? `What's new in v${latestVersion}` : "Changelog"}
31
+ style={{
32
+ position: "relative",
33
+ display: "inline-flex",
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ width: 24,
37
+ height: 24,
38
+ borderRadius: 6,
39
+ color: active ? "var(--af-text)" : "var(--af-text-tertiary)",
40
+ background: active ? "var(--af-surface-hover)" : "transparent",
41
+ textDecoration: "none",
42
+ }}
43
+ >
44
+ <Megaphone size={14} />
45
+ {unread && (
46
+ <span
47
+ aria-hidden
48
+ style={{
49
+ position: "absolute",
50
+ top: 3,
51
+ right: 3,
52
+ width: 7,
53
+ height: 7,
54
+ borderRadius: "50%",
55
+ background: "#ef4444",
56
+ border: "1.5px solid var(--af-surface)",
57
+ }}
58
+ />
59
+ )}
60
+ </Link>
61
+ );
62
+ }
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { STORAGE_KEY } from "@/lib/changelog";
5
+
6
+ export function ChangelogMarkRead({ version }: { version: string | null }) {
7
+ useEffect(() => {
8
+ if (!version) return;
9
+ window.localStorage.setItem(STORAGE_KEY, version);
10
+ }, [version]);
11
+ return null;
12
+ }
@@ -473,7 +473,7 @@ function SessionRow({ entry }: { entry: Entry }) {
473
473
  <span style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--af-text-tertiary)", width: 40, textAlign: "right" }}>
474
474
  {durMin}m
475
475
  </span>
476
- {outcome && <OutcomePill outcome={outcome} size="sm" label="text" />}
476
+ {outcome && <OutcomePill outcome={outcome} size="sm" label="text" agent={entry.agent} />}
477
477
  </div>
478
478
  {summary && (
479
479
  <div style={{
@@ -17,6 +17,7 @@ import { usePathname } from "next/navigation";
17
17
  import Link from "next/link";
18
18
  import { prettyProjectName } from "@/lib/format";
19
19
  import { TeamBadge } from "@/components/team-badge";
20
+ import { AgentBadge } from "@/components/agent-badge";
20
21
  import { OutcomePill } from "@/components/outcome-pill";
21
22
  import type { DayOutcome, EntryEnrichmentStatus } from "@claude-lens/entries";
22
23
 
@@ -34,6 +35,7 @@ export type LiveSessionPick = {
34
35
  lastTimestamp?: string;
35
36
  teamName?: string;
36
37
  agentName?: string;
38
+ agent?: import("@claude-lens/parser").AgentKind;
37
39
  };
38
40
 
39
41
  export type LiveEntrySummary = {
@@ -212,7 +214,7 @@ export function LiveSessionsWidget({
212
214
  style={{ marginBottom: 4 }}
213
215
  >
214
216
  {sum.outcome ? (
215
- <OutcomePill outcome={sum.outcome} size="sm" label="text" />
217
+ <OutcomePill outcome={sum.outcome} size="sm" label="text" agent={s.agent} />
216
218
  ) : sum.enrichmentStatus !== "skipped_trivial" ? (
217
219
  <OutcomePill
218
220
  outcome={null}
@@ -220,6 +222,7 @@ export function LiveSessionsWidget({
220
222
  sessionId={s.id}
221
223
  localDay={sum.localDay}
222
224
  size="sm"
225
+ agent={s.agent}
223
226
  />
224
227
  ) : null}
225
228
  </div>
@@ -248,6 +251,7 @@ export function LiveSessionsWidget({
248
251
  )}
249
252
  </span>
250
253
  <TeamBadge session={s} linkable={false} />
254
+ <AgentBadge agent={s.agent} />
251
255
  </div>
252
256
  {/* Subtitle: what the agent is saying in response. */}
253
257
  <div
@@ -1,5 +1,7 @@
1
1
  import Link from "next/link";
2
2
  import type { DayOutcome } from "@claude-lens/entries";
3
+ import type { AgentKind } from "@claude-lens/parser";
4
+ import { AgentIcon } from "@/components/agent-icon";
3
5
 
4
6
  export type EntryOutcome = Exclude<DayOutcome, "idle">;
5
7
 
@@ -31,12 +33,13 @@ type Size = keyof typeof SIZES;
31
33
  type Label = "icon" | "text" | "both";
32
34
 
33
35
  type Props =
34
- | { outcome: DayOutcome | EntryOutcome; size?: Size; label?: Label; pending?: never }
35
- | { outcome: null; pending: true; sessionId?: string; localDay: string; size?: Size };
36
+ | { outcome: DayOutcome | EntryOutcome; size?: Size; label?: Label; agent?: AgentKind; pending?: never }
37
+ | { outcome: null; pending: true; sessionId?: string; localDay: string; size?: Size; agent?: AgentKind };
36
38
 
37
39
  export function OutcomePill(props: Props) {
38
40
  const size = props.size ?? "md";
39
41
  const dim = SIZES[size];
42
+ const agentIconSize = dim.fontSize + 1;
40
43
 
41
44
  if ("pending" in props && props.pending) {
42
45
  const style: React.CSSProperties = {
@@ -59,6 +62,7 @@ export function OutcomePill(props: Props) {
59
62
  style={style}
60
63
  title={`Generate ${props.localDay} digest →`}
61
64
  >
65
+ {props.agent && <AgentIcon agent={props.agent} size={agentIconSize} />}
62
66
  <span aria-hidden style={{ lineHeight: 1 }}>{PENDING_STYLE.icon}</span>
63
67
  <span>{PENDING_STYLE.label}</span>
64
68
  </Link>
@@ -85,6 +89,7 @@ export function OutcomePill(props: Props) {
85
89
 
86
90
  return (
87
91
  <span style={style} title={variant.label}>
92
+ {props.agent && <AgentIcon agent={props.agent} size={agentIconSize} />}
88
93
  {showIcon && <span aria-hidden style={{ lineHeight: 1 }}>{variant.icon}</span>}
89
94
  {showText && <span>{variant.label}</span>}
90
95
  </span>