fleetlens 0.15.2 → 0.15.4

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 (222) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +3 -0
  3. package/app/apps/web/.next/build-manifest.json +3 -3
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +18 -0
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  14. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  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.nft.json +1 -1
  19. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  21. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/menubar/route/app-paths-manifest.json +3 -0
  23. package/app/apps/web/.next/server/app/api/menubar/route/build-manifest.json +9 -0
  24. package/app/apps/web/.next/server/app/api/menubar/route/server-reference-manifest.json +4 -0
  25. package/app/apps/web/.next/server/app/api/menubar/route.js +6 -0
  26. package/app/apps/web/.next/server/app/api/menubar/route.js.map +5 -0
  27. package/app/apps/web/.next/server/app/api/menubar/route.js.nft.json +1 -0
  28. package/app/apps/web/.next/server/app/api/menubar/route_client-reference-manifest.js +3 -0
  29. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +1 -1
  30. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/projects/open-folder/route/app-paths-manifest.json +3 -0
  32. package/app/apps/web/.next/server/app/api/projects/open-folder/route/build-manifest.json +9 -0
  33. package/app/apps/web/.next/server/app/api/projects/open-folder/route/server-reference-manifest.json +4 -0
  34. package/app/apps/web/.next/server/app/api/projects/open-folder/route.js +7 -0
  35. package/app/apps/web/.next/server/app/api/projects/open-folder/route.js.map +5 -0
  36. package/app/apps/web/.next/server/app/api/projects/open-folder/route.js.nft.json +1 -0
  37. package/app/apps/web/.next/server/app/api/projects/open-folder/route_client-reference-manifest.js +3 -0
  38. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  39. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  40. package/app/apps/web/.next/server/app/api/settings/credentials/route/app-paths-manifest.json +3 -0
  41. package/app/apps/web/.next/server/app/api/settings/credentials/route/build-manifest.json +9 -0
  42. package/app/apps/web/.next/server/app/api/settings/credentials/route/server-reference-manifest.json +4 -0
  43. package/app/apps/web/.next/server/app/api/settings/credentials/route.js +10 -0
  44. package/app/apps/web/.next/server/app/api/settings/credentials/route.js.map +5 -0
  45. package/app/apps/web/.next/server/app/api/settings/credentials/route.js.nft.json +1 -0
  46. package/app/apps/web/.next/server/app/api/settings/credentials/route_client-reference-manifest.js +3 -0
  47. package/app/apps/web/.next/server/app/api/settings/route.js +4 -4
  48. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  49. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  50. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  51. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  52. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  53. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  54. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  55. package/app/apps/web/.next/server/app/day/page.js +1 -1
  56. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  57. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  58. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  59. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  60. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  61. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  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.js +1 -1
  65. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  66. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  67. package/app/apps/web/.next/server/app/insights/page.js +1 -1
  68. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  69. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  70. package/app/apps/web/.next/server/app/page.js +1 -1
  71. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  72. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  74. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  75. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  76. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  77. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  78. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  79. package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -2
  80. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  81. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  82. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  83. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  84. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  85. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  86. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  87. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  88. package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
  89. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  90. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  91. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  92. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  93. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  94. package/app/apps/web/.next/server/app/settings/page.js +2 -2
  95. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  96. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  97. package/app/apps/web/.next/server/app/team/onboarding/page.js +1 -1
  98. package/app/apps/web/.next/server/app/team/onboarding/page.js.nft.json +1 -1
  99. package/app/apps/web/.next/server/app/team/onboarding/page_client-reference-manifest.js +1 -1
  100. package/app/apps/web/.next/server/app/team/page.js +1 -1
  101. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  102. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  103. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  104. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  105. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  106. package/app/apps/web/.next/server/app-paths-manifest.json +3 -0
  107. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_projects_open-folder_route_actions_0m~419c.js +3 -0
  108. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_settings_credentials_route_actions_04an_i2.js +3 -0
  109. package/app/apps/web/.next/server/chunks/[root-of-the-server]__05z0pcw._.js +3 -0
  110. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_nle98._.js +3 -0
  111. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0b5r~-1._.js +3 -0
  112. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0a0~7d6._.js → [root-of-the-server]__0t7i-hy._.js} +2 -2
  113. package/app/apps/web/.next/server/chunks/[root-of-the-server]__12iid50._.js +3 -0
  114. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_menubar_route_actions_0xep8_i.js +3 -0
  115. package/app/apps/web/.next/server/chunks/{node_modules_next_036my~r._.js → node_modules_next_04~_e52._.js} +5 -5
  116. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +14 -14
  117. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  118. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  119. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0.zquoa._.js +1 -1
  120. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__03svi~4._.js +8 -0
  121. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +1 -1
  122. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04il6m2._.js +1 -1
  123. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08l86e-._.js +1 -1
  124. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__09rjz7_._.js +1 -1
  125. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ar_8wr._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__10~b4bf._.js → [root-of-the-server]__0blm9gz._.js} +3 -3
  127. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0e.gxk3._.js +1 -1
  128. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0g-tomj._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0i.q-1h._.js +1 -1
  130. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0k1x-c9._.js +1 -1
  131. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lz4a~g._.js +1 -1
  132. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0nr2~y1._.js +3 -0
  133. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o1_g.~._.js +1 -1
  134. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0p1x6v2._.js +1 -1
  135. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
  136. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
  137. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0stu~~r._.js +1 -1
  138. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +1 -1
  139. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u39afl._.js +1 -1
  140. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ufi3.w._.js +1 -1
  141. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0xy1qim._.js +1 -1
  142. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
  143. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10srpu0._.js +1 -1
  144. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10xgshr._.js +1 -1
  145. package/app/apps/web/.next/server/chunks/ssr/_02r-a1a._.js +8 -0
  146. package/app/apps/web/.next/server/chunks/ssr/_0k62ejd._.js +8 -0
  147. package/app/apps/web/.next/server/chunks/ssr/{_0xu2pvc._.js → _0k8yioi._.js} +2 -2
  148. package/app/apps/web/.next/server/chunks/ssr/{_0wj750k._.js → _0t~mx7w._.js} +2 -2
  149. package/app/apps/web/.next/server/chunks/ssr/{_0kajk31._.js → _0whg~r-._.js} +2 -2
  150. package/app/apps/web/.next/server/chunks/ssr/_13jdm1g._.js +3 -0
  151. package/app/apps/web/.next/server/chunks/ssr/apps_web_0~aj0~u._.js +3 -0
  152. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +1 -1
  153. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0x014hn._.js +16 -0
  154. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  155. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  156. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_12vc-v-._.js +3 -0
  157. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  158. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  159. package/app/apps/web/.next/server/functions-config-manifest.json +3 -0
  160. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  161. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  162. package/app/apps/web/.next/server/pages/500.html +1 -1
  163. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  164. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  165. package/app/apps/web/.next/static/chunks/0-z49mtd-zwgv.js +1 -0
  166. package/app/apps/web/.next/static/chunks/043slkg3juwck.js +1 -0
  167. package/app/apps/web/.next/static/chunks/{0~qdw1b2uz._5.js → 044mtrzh7d7va.js} +3 -3
  168. package/app/apps/web/.next/static/chunks/05b0dn.r00hq1.js +1 -0
  169. package/app/apps/web/.next/static/chunks/{14vx702c32kn6.js → 0_0~~ipv90vm3.js} +1 -1
  170. package/app/apps/web/.next/static/chunks/0awkd5df28gvj.js +6 -0
  171. package/app/apps/web/.next/static/chunks/{0m7dd-2zd9vl2.js → 0eoc4yy0s0gpe.js} +1 -1
  172. package/app/apps/web/.next/static/chunks/0g0hv.dt-de9g.js +14 -0
  173. package/app/apps/web/.next/static/chunks/0rsp61c8wb.sx.js +6 -0
  174. package/app/apps/web/.next/static/chunks/{03rd.3cc_t~yf.js → 0s~0-8l~_kqnf.js} +1 -1
  175. package/app/apps/web/.next/static/chunks/0t2on1dhydyb2.css +1 -0
  176. package/app/apps/web/.next/static/chunks/0y3448zrint0k.js +6 -0
  177. package/app/apps/web/app/api/menubar/route.ts +66 -0
  178. package/app/apps/web/app/api/projects/open-folder/route.ts +60 -0
  179. package/app/apps/web/app/api/settings/credentials/route.ts +45 -0
  180. package/app/apps/web/app/layout.tsx +2 -0
  181. package/app/apps/web/app/projects/[slug]/local-folders-button.tsx +406 -0
  182. package/app/apps/web/app/projects/[slug]/page.tsx +155 -20
  183. package/app/apps/web/app/projects/page.tsx +2 -2
  184. package/app/apps/web/app/sessions/[id]/page.tsx +4 -1
  185. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +81 -0
  186. package/app/apps/web/app/sessions/[id]/session-view.tsx +11 -0
  187. package/app/apps/web/app/sessions/sessions-grid.tsx +56 -25
  188. package/app/apps/web/app/settings/page.tsx +26 -0
  189. package/app/apps/web/app/usage/page.tsx +80 -0
  190. package/app/apps/web/components/menubar-install-banner.tsx +117 -0
  191. package/app/apps/web/components/menubar-install.tsx +107 -0
  192. package/app/apps/web/components/zai-credentials-form.tsx +116 -0
  193. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  194. package/app/apps/web/lib/entries-index.ts +4 -4
  195. package/app/apps/web/lib/menubar-data.ts +50 -0
  196. package/app/apps/web/lib/usage-data.ts +4 -0
  197. package/app/apps/web/package.json +1 -1
  198. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  199. package/dist/daemon-worker.js +640 -240
  200. package/dist/index.js +938 -283
  201. package/package.json +3 -2
  202. package/app/apps/web/.next/server/chunks/[root-of-the-server]__065lib8._.js +0 -3
  203. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0a7n44-._.js +0 -3
  204. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0c00i76._.js +0 -3
  205. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +0 -8
  206. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +0 -8
  207. package/app/apps/web/.next/server/chunks/ssr/_12mbgs5._.js +0 -3
  208. package/app/apps/web/.next/server/chunks/ssr/apps_web_0q2x4a~._.js +0 -3
  209. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0b-4-4j._.js +0 -8
  210. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +0 -16
  211. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +0 -3
  212. package/app/apps/web/.next/static/chunks/00a9oyw~h5i1p.js +0 -1
  213. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +0 -6
  214. package/app/apps/web/.next/static/chunks/040s42ypidk7s.css +0 -1
  215. package/app/apps/web/.next/static/chunks/0d2x-wfu.m0-z.js +0 -6
  216. package/app/apps/web/.next/static/chunks/0i77er85v.rj..js +0 -1
  217. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +0 -6
  218. package/app/apps/web/.next/static/chunks/0tirv_vo6q8uc.js +0 -1
  219. package/app/apps/web/.next/static/chunks/0u6gxpqhl0gc7.js +0 -14
  220. /package/app/apps/web/.next/static/{mTj8nKiazosAKJec15dLe → nmAGY0LiBgXuibWTKZ1RD}/_buildManifest.js +0 -0
  221. /package/app/apps/web/.next/static/{mTj8nKiazosAKJec15dLe → nmAGY0LiBgXuibWTKZ1RD}/_clientMiddlewareManifest.js +0 -0
  222. /package/app/apps/web/.next/static/{mTj8nKiazosAKJec15dLe → nmAGY0LiBgXuibWTKZ1RD}/_ssgManifest.js +0 -0
@@ -0,0 +1,406 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { FolderOpen, Github, X } from "lucide-react";
5
+ import { formatRelative } from "@/lib/format";
6
+
7
+ export type ProjectLocalFolder = {
8
+ path: string;
9
+ canonicalProject: string;
10
+ reasons: string[];
11
+ worktreeNames: string[];
12
+ agents: string[];
13
+ projectDirs: string[];
14
+ sessionCount: number;
15
+ lastTimestamp?: string;
16
+ canOpen: boolean;
17
+ exists: boolean;
18
+ isWorktree: boolean;
19
+ branch?: string;
20
+ defaultBranch?: string;
21
+ remote?: { host: string; owner: string; name: string; url: string; webUrl: string };
22
+ };
23
+
24
+ const chip = {
25
+ border: "1px solid var(--af-border-subtle)",
26
+ borderRadius: 999,
27
+ padding: "1px 7px",
28
+ fontSize: 10,
29
+ whiteSpace: "nowrap" as const,
30
+ };
31
+
32
+ type OpenState =
33
+ | { kind: "idle" }
34
+ | { kind: "opening"; path: string }
35
+ | { kind: "opened"; path: string }
36
+ | { kind: "error"; path: string; message: string };
37
+
38
+ function shortPath(path: string): string {
39
+ const parts = path.split("/").filter(Boolean);
40
+ return parts.length >= 2 ? `${parts.at(-2)}/${parts.at(-1)}` : path;
41
+ }
42
+
43
+ export function ProjectLocalFoldersButton({
44
+ projectKey,
45
+ folders,
46
+ }: {
47
+ projectKey: string;
48
+ folders: ProjectLocalFolder[];
49
+ }) {
50
+ const [open, setOpen] = useState(false);
51
+ const [state, setState] = useState<OpenState>({ kind: "idle" });
52
+
53
+ useEffect(() => {
54
+ if (!open) return;
55
+ const onKey = (e: KeyboardEvent) => {
56
+ if (e.key === "Escape") setOpen(false);
57
+ };
58
+ window.addEventListener("keydown", onKey);
59
+ return () => window.removeEventListener("keydown", onKey);
60
+ }, [open]);
61
+
62
+ async function openFolder(path: string) {
63
+ setState({ kind: "opening", path });
64
+ const res = await fetch("/api/projects/open-folder", {
65
+ method: "POST",
66
+ headers: { "content-type": "application/json" },
67
+ body: JSON.stringify({ projectKey, path }),
68
+ });
69
+ const body = (await res.json().catch(() => null)) as { error?: string } | null;
70
+ if (!res.ok) {
71
+ setState({
72
+ kind: "error",
73
+ path,
74
+ message: body?.error ?? "Could not open this folder.",
75
+ });
76
+ return;
77
+ }
78
+ setState({ kind: "opened", path });
79
+ }
80
+
81
+ return (
82
+ <>
83
+ <button
84
+ type="button"
85
+ className="af-btn"
86
+ onClick={() => setOpen(true)}
87
+ style={{
88
+ display: "inline-flex",
89
+ alignItems: "center",
90
+ gap: 7,
91
+ whiteSpace: "nowrap",
92
+ }}
93
+ >
94
+ <FolderOpen size={14} />
95
+ Local folders
96
+ <span
97
+ style={{
98
+ fontFamily: "var(--font-mono)",
99
+ fontSize: 11,
100
+ color: "var(--af-text-tertiary)",
101
+ }}
102
+ >
103
+ {folders.length}
104
+ </span>
105
+ </button>
106
+
107
+ {open && (
108
+ <div
109
+ role="dialog"
110
+ aria-modal="true"
111
+ aria-labelledby="local-folders-title"
112
+ onClick={() => setOpen(false)}
113
+ style={{
114
+ position: "fixed",
115
+ inset: 0,
116
+ zIndex: 220,
117
+ background: "rgba(0, 0, 0, 0.5)",
118
+ backdropFilter: "blur(4px)",
119
+ WebkitBackdropFilter: "blur(4px)",
120
+ display: "flex",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+ padding: 20,
124
+ }}
125
+ >
126
+ <div
127
+ onClick={(e) => e.stopPropagation()}
128
+ style={{
129
+ background: "var(--af-surface-elevated)",
130
+ border: "1px solid var(--af-border-subtle)",
131
+ borderRadius: 10,
132
+ width: "min(860px, 100%)",
133
+ maxHeight: "86vh",
134
+ display: "flex",
135
+ flexDirection: "column",
136
+ boxShadow: "0 18px 54px rgba(0, 0, 0, 0.35)",
137
+ }}
138
+ >
139
+ <div
140
+ style={{
141
+ display: "flex",
142
+ alignItems: "center",
143
+ justifyContent: "space-between",
144
+ gap: 16,
145
+ padding: "16px 20px",
146
+ borderBottom: "1px solid var(--af-border-subtle)",
147
+ }}
148
+ >
149
+ <div style={{ minWidth: 0 }}>
150
+ <h2
151
+ id="local-folders-title"
152
+ style={{
153
+ margin: 0,
154
+ fontSize: 16,
155
+ fontWeight: 700,
156
+ letterSpacing: "-0.01em",
157
+ }}
158
+ >
159
+ Aggregated local folders
160
+ </h2>
161
+ <p
162
+ style={{
163
+ margin: "4px 0 0",
164
+ fontSize: 12,
165
+ color: "var(--af-text-tertiary)",
166
+ }}
167
+ >
168
+ Folders from sessions that currently roll up into this project.
169
+ </p>
170
+ </div>
171
+ <button
172
+ type="button"
173
+ onClick={() => setOpen(false)}
174
+ aria-label="Close"
175
+ style={{
176
+ background: "transparent",
177
+ border: "none",
178
+ color: "var(--af-text-tertiary)",
179
+ cursor: "pointer",
180
+ padding: 4,
181
+ lineHeight: 0,
182
+ }}
183
+ >
184
+ <X size={18} />
185
+ </button>
186
+ </div>
187
+
188
+ <div style={{ overflow: "auto", padding: 14 }}>
189
+ {folders.length === 0 ? (
190
+ <div className="af-empty">No local folders were found for this project.</div>
191
+ ) : (
192
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
193
+ {folders.map((folder) => {
194
+ const isBusy = state.kind === "opening" && state.path === folder.path;
195
+ const isOpened = state.kind === "opened" && state.path === folder.path;
196
+ const isError = state.kind === "error" && state.path === folder.path;
197
+ return (
198
+ <div
199
+ key={folder.path}
200
+ style={{
201
+ border: "1px solid var(--af-border-subtle)",
202
+ borderRadius: 8,
203
+ background: "var(--af-surface)",
204
+ padding: 12,
205
+ display: "grid",
206
+ gridTemplateColumns: "minmax(0, 1fr) auto",
207
+ gap: 12,
208
+ alignItems: "start",
209
+ }}
210
+ >
211
+ <div style={{ minWidth: 0 }}>
212
+ <div
213
+ style={{
214
+ display: "flex",
215
+ alignItems: "center",
216
+ gap: 8,
217
+ marginBottom: 6,
218
+ }}
219
+ >
220
+ <span
221
+ style={{
222
+ fontSize: 12,
223
+ fontWeight: 700,
224
+ color: "var(--af-text)",
225
+ }}
226
+ >
227
+ {shortPath(folder.path)}
228
+ </span>
229
+ {folder.worktreeNames.map((name) => (
230
+ <span
231
+ key={name}
232
+ style={{ ...chip, color: "var(--af-text-secondary)" }}
233
+ >
234
+ worktree: {name}
235
+ </span>
236
+ ))}
237
+ {folder.branch && (
238
+ <span
239
+ title={
240
+ folder.branch === folder.defaultBranch
241
+ ? "on the default branch"
242
+ : `default branch is ${folder.defaultBranch ?? "unknown"}`
243
+ }
244
+ style={{
245
+ ...chip,
246
+ fontFamily: "var(--font-mono)",
247
+ color:
248
+ folder.branch === folder.defaultBranch
249
+ ? "var(--af-text-tertiary)"
250
+ : "var(--af-text)",
251
+ }}
252
+ >
253
+ {folder.branch}
254
+ </span>
255
+ )}
256
+ {!folder.exists && (
257
+ <span
258
+ title="This folder is gone from disk. Its sessions still count toward the project."
259
+ style={{ ...chip, color: "var(--af-danger)", borderColor: "var(--af-danger)" }}
260
+ >
261
+ gone
262
+ </span>
263
+ )}
264
+ </div>
265
+ <div
266
+ title={folder.path}
267
+ style={{
268
+ fontFamily: "var(--font-mono)",
269
+ fontSize: 11,
270
+ color: "var(--af-text-secondary)",
271
+ overflow: "hidden",
272
+ textOverflow: "ellipsis",
273
+ whiteSpace: "nowrap",
274
+ marginBottom: 8,
275
+ }}
276
+ >
277
+ {folder.path}
278
+ </div>
279
+ <div
280
+ style={{
281
+ display: "grid",
282
+ gridTemplateColumns: "92px minmax(0, 1fr)",
283
+ rowGap: 5,
284
+ columnGap: 10,
285
+ fontSize: 11,
286
+ color: "var(--af-text-tertiary)",
287
+ }}
288
+ >
289
+ <span>Grouped via</span>
290
+ <span style={{ color: "var(--af-text-secondary)" }}>
291
+ {folder.reasons.join(", ")}
292
+ </span>
293
+ <span>Into</span>
294
+ <span
295
+ title={folder.canonicalProject}
296
+ style={{
297
+ fontFamily: "var(--font-mono)",
298
+ color: "var(--af-text-secondary)",
299
+ overflow: "hidden",
300
+ textOverflow: "ellipsis",
301
+ whiteSpace: "nowrap",
302
+ }}
303
+ >
304
+ {folder.canonicalProject}
305
+ </span>
306
+ <span>Sessions</span>
307
+ <span style={{ color: "var(--af-text-secondary)" }}>
308
+ {folder.sessionCount} session{folder.sessionCount === 1 ? "" : "s"}
309
+ {" | "}
310
+ {folder.agents.join(", ")}
311
+ {folder.lastTimestamp ? ` | ${formatRelative(folder.lastTimestamp)}` : ""}
312
+ </span>
313
+ {folder.remote && (
314
+ <>
315
+ <span>Remote</span>
316
+ <span
317
+ style={{
318
+ fontFamily: "var(--font-mono)",
319
+ overflow: "hidden",
320
+ textOverflow: "ellipsis",
321
+ whiteSpace: "nowrap",
322
+ }}
323
+ >
324
+ <a
325
+ href={folder.remote.webUrl}
326
+ target="_blank"
327
+ rel="noreferrer"
328
+ title={folder.remote.url}
329
+ style={{
330
+ color: "var(--af-text-secondary)",
331
+ display: "inline-flex",
332
+ alignItems: "center",
333
+ gap: 5,
334
+ }}
335
+ >
336
+ {folder.remote.host === "github.com" && (
337
+ <Github size={11} aria-label="GitHub" />
338
+ )}
339
+ {folder.remote.owner}/{folder.remote.name}
340
+ </a>
341
+ {folder.remote.host !== "github.com" && (
342
+ <span style={{ color: "var(--af-text-tertiary)" }}>
343
+ {" \u00b7 "}{folder.remote.host}
344
+ </span>
345
+ )}
346
+ </span>
347
+ </>
348
+ )}
349
+ <span>Transcript dir</span>
350
+ <span
351
+ title={folder.projectDirs.join(", ")}
352
+ style={{
353
+ fontFamily: "var(--font-mono)",
354
+ color: "var(--af-text-secondary)",
355
+ overflow: "hidden",
356
+ textOverflow: "ellipsis",
357
+ whiteSpace: "nowrap",
358
+ }}
359
+ >
360
+ {folder.projectDirs.join(", ")}
361
+ </span>
362
+ </div>
363
+ {isError && (
364
+ <div
365
+ style={{
366
+ marginTop: 8,
367
+ fontSize: 11,
368
+ color: "var(--af-danger)",
369
+ }}
370
+ >
371
+ {state.message}
372
+ </div>
373
+ )}
374
+ {isOpened && (
375
+ <div
376
+ style={{
377
+ marginTop: 8,
378
+ fontSize: 11,
379
+ color: "var(--af-accent)",
380
+ }}
381
+ >
382
+ Folder opened.
383
+ </div>
384
+ )}
385
+ </div>
386
+ <button
387
+ type="button"
388
+ className="af-btn"
389
+ disabled={!folder.canOpen || isBusy}
390
+ onClick={() => openFolder(folder.path)}
391
+ style={{ whiteSpace: "nowrap" }}
392
+ >
393
+ {isBusy ? "Opening..." : "Open folder"}
394
+ </button>
395
+ </div>
396
+ );
397
+ })}
398
+ </div>
399
+ )}
400
+ </div>
401
+ </div>
402
+ </div>
403
+ )}
404
+ </>
405
+ );
406
+ }
@@ -1,11 +1,14 @@
1
1
  import { notFound } from "next/navigation";
2
2
  import Link from "next/link";
3
+ import { isAbsolute } from "node:path";
3
4
  import { listSessions, getSession } from "@/lib/data";
5
+ import { readGitFolder } from "@claude-lens/parser/fs";
4
6
  import {
5
- projectRepoName,
7
+ buildProjectKeyResolver,
6
8
  detectPrMarkers,
7
9
  sessionAirTimeMs,
8
10
  } from "@claude-lens/parser";
11
+ import type { SessionMeta } from "@claude-lens/parser";
9
12
  import type { DayOutcome, DayHelpfulness } from "@claude-lens/entries";
10
13
  import { DashboardView } from "@/components/dashboard-view";
11
14
  import { LiveBadge } from "@/components/live-badge";
@@ -14,7 +17,8 @@ import { OutcomePill, OUTCOME_STYLES } from "@/components/outcome-pill";
14
17
  import { HelpfulnessSparkline } from "@/components/helpfulness-sparkline";
15
18
  import { buildEntriesIndex, listCachedDayDigests } from "@/lib/entries-index";
16
19
  import { formatDuration, formatRelative, prettyProjectName } from "@/lib/format";
17
- import { ArrowLeft } from "lucide-react";
20
+ import { ArrowLeft, Github } from "lucide-react";
21
+ import { ProjectLocalFoldersButton, type ProjectLocalFolder } from "./local-folders-button";
18
22
 
19
23
  function lastNDays(n: number): string[] {
20
24
  const out: string[] = [];
@@ -34,17 +38,110 @@ const OUTCOME_PRI: Record<DayOutcome, number> = {
34
38
  shipped: 6, partial: 5, blocked: 4, exploratory: 3, trivial: 2, idle: 1,
35
39
  };
36
40
 
41
+ function agentLabel(agent: SessionMeta["agent"]): string {
42
+ return agent ?? "claude-code";
43
+ }
44
+
45
+ function folderPathForSession(s: SessionMeta): string | undefined {
46
+ if (s.cwd) return s.cwd;
47
+ return isAbsolute(s.projectName) ? s.projectName : undefined;
48
+ }
49
+
50
+ function aggregationReason(s: SessionMeta, folderPath: string): string {
51
+ if (s.worktreeName) return `worktree "${s.worktreeName}" -> project root`;
52
+ if (folderPath === s.projectName) return "project root";
53
+ return "cwd inside git project -> project root";
54
+ }
55
+
56
+ function buildProjectLocalFolders(sessions: SessionMeta[]): ProjectLocalFolder[] {
57
+ const rows = new Map<string, {
58
+ path: string;
59
+ canonicalProject: string;
60
+ reasons: Set<string>;
61
+ worktreeNames: Set<string>;
62
+ agents: Set<string>;
63
+ projectDirs: Set<string>;
64
+ sessionCount: number;
65
+ lastActivityMs: number;
66
+ lastTimestamp?: string;
67
+ canOpen: boolean;
68
+ }>();
69
+
70
+ for (const s of sessions) {
71
+ const folderPath = folderPathForSession(s);
72
+ if (!folderPath) continue;
73
+ const timestampMs = s.lastTimestamp ? Date.parse(s.lastTimestamp) : 0;
74
+ const lastActivityMs = s.lastActivityMs
75
+ ?? (Number.isFinite(timestampMs) ? timestampMs : 0);
76
+ const existing = rows.get(folderPath);
77
+ if (existing) {
78
+ existing.reasons.add(aggregationReason(s, folderPath));
79
+ if (s.worktreeName) existing.worktreeNames.add(s.worktreeName);
80
+ existing.agents.add(agentLabel(s.agent));
81
+ existing.projectDirs.add(s.projectDir);
82
+ existing.sessionCount += 1;
83
+ if (lastActivityMs > existing.lastActivityMs) {
84
+ existing.lastActivityMs = lastActivityMs;
85
+ existing.lastTimestamp = s.lastTimestamp;
86
+ }
87
+ continue;
88
+ }
89
+ rows.set(folderPath, {
90
+ path: folderPath,
91
+ canonicalProject: s.projectName,
92
+ reasons: new Set([aggregationReason(s, folderPath)]),
93
+ worktreeNames: new Set(s.worktreeName ? [s.worktreeName] : []),
94
+ agents: new Set([agentLabel(s.agent)]),
95
+ projectDirs: new Set([s.projectDir]),
96
+ sessionCount: 1,
97
+ lastActivityMs,
98
+ lastTimestamp: s.lastTimestamp,
99
+ canOpen: isAbsolute(folderPath),
100
+ });
101
+ }
102
+
103
+ return [...rows.values()]
104
+ .map((row) => {
105
+ const git = readGitFolder(row.path);
106
+ return {
107
+ path: row.path,
108
+ canonicalProject: row.canonicalProject,
109
+ reasons: [...row.reasons].sort(),
110
+ worktreeNames: [...row.worktreeNames].sort(),
111
+ agents: [...row.agents].sort(),
112
+ projectDirs: [...row.projectDirs].sort(),
113
+ sessionCount: row.sessionCount,
114
+ lastTimestamp: row.lastTimestamp,
115
+ exists: git.exists,
116
+ isWorktree: git.isWorktree,
117
+ branch: git.branch,
118
+ defaultBranch: git.defaultBranch,
119
+ remote: git.remote,
120
+ // A pruned worktree still owns history; it just can't be opened.
121
+ canOpen: git.exists,
122
+ };
123
+ })
124
+ .sort((a, b) => {
125
+ const aIsRoot = a.path === a.canonicalProject ? 0 : 1;
126
+ const bIsRoot = b.path === b.canonicalProject ? 0 : 1;
127
+ if (aIsRoot !== bIsRoot) return aIsRoot - bIsRoot;
128
+ const aLast = a.lastTimestamp ? Date.parse(a.lastTimestamp) : 0;
129
+ const bLast = b.lastTimestamp ? Date.parse(b.lastTimestamp) : 0;
130
+ return bLast - aLast || a.path.localeCompare(b.path);
131
+ });
132
+ }
133
+
37
134
  export const dynamic = "force-dynamic";
38
135
 
39
136
  export default async function ProjectDetail({ params }: { params: Promise<{ slug: string }> }) {
40
137
  const { slug } = await params;
41
- // Slugs are repo names (e.g. `claude-lens`) — see projectRepoName. Match any
42
- // session in that repo, which folds the parent repo, every `/.worktrees/<name>`
43
- // subdir, and the same repo reached via Conductor / Superset into one view.
138
+ // Slugs are Fleetlens project keys (e.g. `claude-lens`). Parser adapters
139
+ // resolve each session to one canonical project before this page filters.
44
140
  const decodedCanonical = decodeURIComponent(slug);
45
141
  const all = await listSessions();
142
+ const keyOf = buildProjectKeyResolver(all);
46
143
  const projectSessions = all.filter(
47
- (s) => projectRepoName(s.projectName) === decodedCanonical,
144
+ (s) => keyOf(s) === decodedCanonical,
48
145
  );
49
146
  if (projectSessions.length === 0) return notFound();
50
147
 
@@ -67,6 +164,8 @@ export default async function ProjectDetail({ params }: { params: Promise<{ slug
67
164
  .reduce((a, d) => a + sessionAirTimeMs(d.events), 0);
68
165
 
69
166
  const recentSessions = projectSessions.slice(0, 12);
167
+ const localFolders = buildProjectLocalFolders(projectSessions);
168
+ const projectRemote = localFolders.find((f) => f.remote)?.remote;
70
169
  const hasPrs = prMarkers.length > 0;
71
170
 
72
171
  // Build the recent-7-days strip + helpfulness sparkline.
@@ -139,20 +238,56 @@ export default async function ProjectDetail({ params }: { params: Promise<{ slug
139
238
  </div>
140
239
 
141
240
  {/* Header */}
142
- <header>
143
- <h1 style={{ fontSize: 26, fontWeight: 700, letterSpacing: "-0.02em", margin: 0 }}>
144
- {prettyProjectName(projectName)}
145
- </h1>
146
- <p
147
- style={{
148
- fontSize: 12,
149
- color: "var(--af-text-tertiary)",
150
- marginTop: 4,
151
- fontFamily: "var(--font-mono)",
152
- }}
153
- >
154
- {projectName}
155
- </p>
241
+ <header
242
+ style={{
243
+ display: "flex",
244
+ justifyContent: "space-between",
245
+ alignItems: "flex-start",
246
+ flexWrap: "wrap",
247
+ gap: 16,
248
+ }}
249
+ >
250
+ <div style={{ minWidth: 0 }}>
251
+ <h1 style={{ fontSize: 26, fontWeight: 700, letterSpacing: "-0.02em", margin: 0 }}>
252
+ {prettyProjectName(projectName)}
253
+ </h1>
254
+ <p
255
+ style={{
256
+ fontSize: 12,
257
+ color: "var(--af-text-tertiary)",
258
+ marginTop: 4,
259
+ fontFamily: "var(--font-mono)",
260
+ overflow: "hidden",
261
+ textOverflow: "ellipsis",
262
+ whiteSpace: "nowrap",
263
+ }}
264
+ >
265
+ {projectRemote ? (
266
+ <a
267
+ href={projectRemote.webUrl}
268
+ target="_blank"
269
+ rel="noreferrer"
270
+ title={projectRemote.url}
271
+ style={{
272
+ color: "var(--af-text-secondary)",
273
+ textDecoration: "none",
274
+ display: "inline-flex",
275
+ alignItems: "center",
276
+ gap: 5,
277
+ }}
278
+ >
279
+ {projectRemote.host === "github.com" && <Github size={12} aria-label="GitHub" />}
280
+ {projectRemote.host === "github.com"
281
+ ? `${projectRemote.owner}/${projectRemote.name}`
282
+ : `${projectRemote.host}/${projectRemote.owner}/${projectRemote.name}`}
283
+ {" \u2197"}
284
+ </a>
285
+ ) : (
286
+ projectName
287
+ )}
288
+ </p>
289
+ </div>
290
+ <ProjectLocalFoldersButton projectKey={decodedCanonical} folders={localFolders} />
156
291
  </header>
157
292
 
158
293
  <DashboardView
@@ -1,5 +1,5 @@
1
1
  import { listSessions } from "@/lib/data";
2
- import { projectRepoName, groupByProject } from "@claude-lens/parser";
2
+ import { projectKey, groupByProject } from "@claude-lens/parser";
3
3
  import type { DayOutcome } from "@claude-lens/entries";
4
4
  import { ProjectsView, type ProjectRow } from "./projects-view";
5
5
  import { buildEntriesIndex } from "@/lib/entries-index";
@@ -26,7 +26,7 @@ export default async function ProjectsPage() {
26
26
  const days = lastNDays(7);
27
27
 
28
28
  const rows: ProjectRow[] = projects.map((p) => {
29
- const projectEntries = index.byProject.get(projectRepoName(p.projectName)) ?? [];
29
+ const projectEntries = index.byProject.get(projectKey(p.projectName)) ?? [];
30
30
  const byDay = new Map<string, typeof projectEntries>();
31
31
  for (const e of projectEntries) {
32
32
  const arr = byDay.get(e.local_day);
@@ -1,6 +1,6 @@
1
1
  import { notFound } from "next/navigation";
2
2
  import { getSession } from "@/lib/data";
3
- import { loadTeamForSession, findTeamLead } from "@claude-lens/parser/fs";
3
+ import { loadTeamForSession, findTeamLead, readGitFolder } from "@claude-lens/parser/fs";
4
4
  import { listEntriesForSession } from "@claude-lens/entries/fs";
5
5
  import {
6
6
  teamViewToTimelineData,
@@ -54,9 +54,12 @@ export default async function SessionDetailPage({
54
54
  events: session.events.map((e) => ({ ...e, raw: undefined })),
55
55
  };
56
56
 
57
+ const git = session.cwd ? readGitFolder(session.cwd) : undefined;
58
+
57
59
  return (
58
60
  <SessionView
59
61
  session={stripped}
62
+ git={git}
60
63
  team={teamProps}
61
64
  teamLead={teamLeadInfo}
62
65
  entries={entries}