fleetlens 0.12.3 → 0.13.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 (228) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +8 -8
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +5 -5
  5. package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +7 -7
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  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/build-manifest.json +5 -5
  14. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +1 -1
  19. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +1 -1
  21. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  23. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +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 -1
  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/pdf/[key]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  33. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
  35. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
  36. package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +5 -5
  37. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  38. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  39. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  40. package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +5 -5
  41. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  42. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  43. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  44. package/app/apps/web/.next/server/app/day/page/build-manifest.json +5 -5
  45. package/app/apps/web/.next/server/app/day/page.js +1 -1
  46. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  47. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  48. package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +5 -5
  49. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  50. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  51. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  52. package/app/apps/web/.next/server/app/digest/page/build-manifest.json +5 -5
  53. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  54. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  55. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  56. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +5 -5
  57. package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
  58. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  59. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  60. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +5 -5
  61. package/app/apps/web/.next/server/app/insights/page.js +3 -2
  62. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  63. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  64. package/app/apps/web/.next/server/app/page/build-manifest.json +5 -5
  65. package/app/apps/web/.next/server/app/page.js +1 -1
  66. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  67. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  68. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +5 -5
  69. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  70. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  71. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  72. package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +5 -5
  73. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  74. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  75. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  76. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +5 -5
  77. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
  78. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  79. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  80. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +5 -5
  81. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  82. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  83. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  84. package/app/apps/web/.next/server/app/runs/page/build-manifest.json +5 -5
  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/build-manifest.json +5 -5
  89. package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
  90. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  91. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  92. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +5 -5
  93. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  94. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  95. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  96. package/app/apps/web/.next/server/app/settings/page/build-manifest.json +5 -5
  97. package/app/apps/web/.next/server/app/settings/page.js +1 -1
  98. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  99. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  100. package/app/apps/web/.next/server/app/team/page/build-manifest.json +5 -5
  101. package/app/apps/web/.next/server/app/team/page.js +1 -1
  102. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  103. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  104. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +5 -5
  105. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  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/chunks/[externals]__0rv9ct5._.js +3 -0
  109. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gril0u._.js +9 -0
  110. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0n79hpm._.js +9 -0
  111. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~utv~v._.js +9 -0
  112. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_02bw8.n.js +5 -0
  113. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  114. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  115. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
  116. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0-1_njm._.js → [root-of-the-server]__0k1x-c9._.js} +2 -2
  117. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +8 -0
  118. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +1 -1
  119. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o03ymy._.js +1 -1
  120. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
  121. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
  122. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
  123. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  124. package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
  125. package/app/apps/web/.next/server/chunks/ssr/apps_web_0.mu7c7._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  127. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0b-4-4j._.js +1 -1
  130. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0c-tpft._.js +1 -1
  131. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
  132. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
  133. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  134. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  135. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
  136. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  137. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  138. package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
  139. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  140. package/app/apps/web/.next/server/pages/500.html +1 -1
  141. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  142. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  143. package/app/apps/web/.next/static/chunks/02bv_ejzhhzvp.js +10 -0
  144. package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
  145. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
  146. package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
  147. package/app/apps/web/.next/static/chunks/{12jvvc_w.cb45.css → 083~hzkv577.5.css} +1 -1
  148. package/app/apps/web/.next/static/chunks/{0osdgl2.9cl1q.js → 0d2x-wfu.m0-z.js} +1 -1
  149. package/app/apps/web/.next/static/chunks/{0lziv8y_6yp5w.js → 0e_fzy0n8ij-o.js} +1 -1
  150. package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
  151. package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
  152. package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
  153. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
  154. package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
  155. package/app/apps/web/.next/static/chunks/0q8dg4txlzno0.js +1 -0
  156. package/app/apps/web/.next/static/chunks/{0gblp_v.kiu-..js → 0qv-gw888_5nu.js} +1 -1
  157. package/app/apps/web/.next/static/chunks/{14bjditmmrn7l.js → 0tirv_vo6q8uc.js} +1 -1
  158. package/app/apps/web/.next/static/chunks/{0qmh5da6gfm56.js → 0vz4~a~~hq399.js} +1 -1
  159. package/app/apps/web/.next/static/chunks/{13po66tt.68f3.js → 0wwcx42ng8i2e.js} +3 -3
  160. package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
  161. package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
  162. package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
  163. package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
  164. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
  165. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
  166. package/app/apps/web/app/api/events/route.ts +47 -4
  167. package/app/apps/web/app/globals.css +23 -0
  168. package/app/apps/web/app/insights/page.tsx +20 -5
  169. package/app/apps/web/app/layout.tsx +1 -0
  170. package/app/apps/web/app/page.tsx +1 -1
  171. package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
  172. package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
  173. package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
  174. package/app/apps/web/app/parallelism/gantt-chart.tsx +16 -902
  175. package/app/apps/web/app/projects/[slug]/page.tsx +1 -1
  176. package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
  177. package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
  178. package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
  179. package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
  180. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
  181. package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
  182. package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
  183. package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
  184. package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
  185. package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
  186. package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
  187. package/app/apps/web/app/sessions/[id]/session-view.tsx +701 -1577
  188. package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
  189. package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
  190. package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
  191. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
  192. package/app/apps/web/app/sessions/sessions-grid.tsx +43 -19
  193. package/app/apps/web/components/digest-actions.tsx +103 -0
  194. package/app/apps/web/components/job-queue-widget.tsx +1 -1
  195. package/app/apps/web/components/live-badge.tsx +11 -3
  196. package/app/apps/web/components/live-sessions-widget.tsx +9 -3
  197. package/app/apps/web/components/month-digest-view.tsx +19 -64
  198. package/app/apps/web/components/outcome-pill.tsx +24 -4
  199. package/app/apps/web/components/searchable-select.tsx +215 -0
  200. package/app/apps/web/components/tail-mode.tsx +39 -8
  201. package/app/apps/web/components/week-digest-view.tsx +16 -71
  202. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  203. package/app/apps/web/lib/entries-index.ts +7 -8
  204. package/app/apps/web/lib/entries.ts +0 -9
  205. package/app/apps/web/lib/format.test.ts +221 -0
  206. package/app/apps/web/lib/utilization-tone.test.ts +56 -0
  207. package/app/apps/web/package.json +1 -1
  208. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  209. package/dist/daemon-worker.js +83 -45
  210. package/dist/index.js +452 -198
  211. package/package.json +1 -1
  212. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +0 -5
  213. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
  214. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
  215. package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
  216. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
  217. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
  218. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
  219. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
  220. package/app/apps/web/.next/static/chunks/00xkqnvk4-d1-.js +0 -10
  221. package/app/apps/web/.next/static/chunks/0htgc_hr1bw67.js +0 -1
  222. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
  223. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
  224. package/app/apps/web/.next/static/chunks/0vf-3rauafbek.js +0 -6
  225. package/app/apps/web/.next/static/chunks/0vl27-vjob2_9.js +0 -6
  226. /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_buildManifest.js +0 -0
  227. /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_clientMiddlewareManifest.js +0 -0
  228. /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_ssgManifest.js +0 -0
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { type ReactNode } from "react";
3
+ import { ExportPdfButton } from "./export-pdf-button";
4
+
5
+ type Period = "week" | "month";
6
+
7
+ export function DigestActions({
8
+ digestKey,
9
+ period,
10
+ hasDigest,
11
+ narrativeFresh,
12
+ isStreaming,
13
+ aiEnabled,
14
+ progress,
15
+ onGenerate,
16
+ onReroll,
17
+ rerollTitle,
18
+ }: {
19
+ digestKey: string;
20
+ period: Period;
21
+ hasDigest: boolean;
22
+ narrativeFresh: boolean;
23
+ isStreaming: boolean;
24
+ aiEnabled: boolean;
25
+ progress: string;
26
+ onGenerate: () => void;
27
+ onReroll: () => void;
28
+ rerollTitle?: string;
29
+ }): ReactNode {
30
+ const defaultRerollTitle = `Re-roll the narrative for this ${period}`;
31
+ const pdfAction = hasDigest ? <ExportPdfButton digestKey={`${period}-${digestKey}`} /> : null;
32
+
33
+ let actions: ReactNode = null;
34
+ if (!narrativeFresh && aiEnabled) {
35
+ actions = (
36
+ <button
37
+ onClick={onGenerate}
38
+ disabled={isStreaming}
39
+ style={btnPrimary(isStreaming)}
40
+ >
41
+ {isStreaming ? "Generating..." : hasDigest ? "Generate" : "Generate digest"}
42
+ </button>
43
+ );
44
+ } else if (aiEnabled) {
45
+ actions = (
46
+ <span style={{ fontSize: 10, color: "var(--af-text-tertiary)", display: "inline-flex", gap: 6, alignItems: "center" }}>
47
+ ✓ Up to date
48
+ <button
49
+ onClick={onReroll}
50
+ disabled={isStreaming}
51
+ title={rerollTitle ?? defaultRerollTitle}
52
+ style={btnSecondary(isStreaming)}
53
+ >
54
+ {isStreaming ? "..." : "Re-roll"}
55
+ </button>
56
+ </span>
57
+ );
58
+ }
59
+ if (progress) {
60
+ actions = (
61
+ <>
62
+ {actions}
63
+ <span style={{ fontSize: 10, color: "var(--af-text-tertiary)" }}>{progress}</span>
64
+ </>
65
+ );
66
+ }
67
+ if (pdfAction) {
68
+ actions = (
69
+ <>
70
+ {actions}
71
+ {pdfAction}
72
+ </>
73
+ );
74
+ }
75
+ return actions;
76
+ }
77
+
78
+ function btnPrimary(disabled: boolean): React.CSSProperties {
79
+ return {
80
+ padding: "4px 10px",
81
+ background: "var(--af-accent)",
82
+ color: "white",
83
+ border: "none",
84
+ borderRadius: 5,
85
+ cursor: disabled ? "default" : "pointer",
86
+ fontSize: 11,
87
+ fontWeight: 600,
88
+ opacity: disabled ? 0.6 : 1,
89
+ };
90
+ }
91
+
92
+ function btnSecondary(disabled: boolean): React.CSSProperties {
93
+ return {
94
+ padding: "2px 7px",
95
+ background: "transparent",
96
+ border: "1px solid var(--af-border-subtle)",
97
+ borderRadius: 4,
98
+ fontSize: 10,
99
+ color: "var(--af-text-secondary)",
100
+ cursor: disabled ? "default" : "pointer",
101
+ opacity: disabled ? 0.6 : 1,
102
+ };
103
+ }
@@ -199,7 +199,7 @@ function JobRow({ job }: { job: Job }) {
199
199
  job.status === "error" ? "#f56565" :
200
200
  "#48bb78";
201
201
  const isLink = job.resultUrl && (job.status === "done" || job.status === "running");
202
- const Container: any = isLink ? Link : "div";
202
+ const Container: React.ElementType = isLink ? Link : "div";
203
203
  const containerProps = isLink ? { href: job.resultUrl! } : {};
204
204
 
205
205
  return (
@@ -15,9 +15,14 @@ const LIVE_WINDOW_MS = 45_000;
15
15
 
16
16
  export function LiveBadge({
17
17
  mtimeIso,
18
+ activityMs,
18
19
  size = "sm",
19
20
  }: {
20
21
  mtimeIso?: string;
22
+ /** Nested-aware last-activity (epoch ms) — `SessionMeta.lastActivityMs`.
23
+ * When present it wins over `mtimeIso` so a session stays LIVE while a
24
+ * background agent/workflow churns and the main transcript is quiet. */
25
+ activityMs?: number;
21
26
  size?: "sm" | "md";
22
27
  }) {
23
28
  const [now, setNow] = useState(() => Date.now());
@@ -26,9 +31,12 @@ export function LiveBadge({
26
31
  return () => clearInterval(id);
27
32
  }, []);
28
33
 
29
- if (!mtimeIso) return null;
30
- const ms = Date.parse(mtimeIso);
31
- if (Number.isNaN(ms) || now - ms > LIVE_WINDOW_MS) return null;
34
+ const isoMs = mtimeIso ? Date.parse(mtimeIso) : NaN;
35
+ const ms = Math.max(
36
+ Number.isNaN(isoMs) ? 0 : isoMs,
37
+ activityMs && Number.isFinite(activityMs) ? activityMs : 0,
38
+ );
39
+ if (ms <= 0 || now - ms > LIVE_WINDOW_MS) return null;
32
40
 
33
41
  const dotSize = size === "md" ? 8 : 6;
34
42
  const padV = size === "md" ? "3px 9px" : "2px 7px";
@@ -33,6 +33,7 @@ export type LiveSessionPick = {
33
33
  lastAgentPreview?: string;
34
34
  firstTimestamp?: string;
35
35
  lastTimestamp?: string;
36
+ lastActivityMs?: number;
36
37
  teamName?: string;
37
38
  agentName?: string;
38
39
  agent?: import("@claude-lens/parser").AgentKind;
@@ -95,9 +96,14 @@ export function LiveSessionsWidget({
95
96
  // order is stable even as agents keep emitting new messages.
96
97
  const live = sessions
97
98
  .filter((s) => {
98
- if (!s.lastTimestamp) return false;
99
- const ms = Date.parse(s.lastTimestamp);
100
- if (Number.isNaN(ms)) return false;
99
+ // Nested-aware: a session with a quiet main transcript but an active
100
+ // background agent/workflow still counts as live via lastActivityMs.
101
+ const isoMs = s.lastTimestamp ? Date.parse(s.lastTimestamp) : NaN;
102
+ const ms = Math.max(
103
+ Number.isNaN(isoMs) ? 0 : isoMs,
104
+ s.lastActivityMs && Number.isFinite(s.lastActivityMs) ? s.lastActivityMs : 0,
105
+ );
106
+ if (ms <= 0) return false;
101
107
  return now - ms <= LIVE_WINDOW_MS;
102
108
  })
103
109
  .slice()
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { useState, useCallback, type ReactNode } from "react";
2
+ import { useState, useCallback } from "react";
3
3
  import { MonthDigest as MonthDigestRender } from "./month-digest";
4
- import { ExportPdfButton } from "./export-pdf-button";
4
+ import { DigestActions } from "./digest-actions";
5
5
  import type { MonthDigest as MonthDigestType } from "@claude-lens/entries";
6
6
 
7
7
  type Status = "idle" | "streaming" | "done" | "error";
@@ -67,75 +67,30 @@ export function MonthDigestView({
67
67
 
68
68
  const isStreaming = status === "streaming";
69
69
  const narrativeFresh = !!digest?.headline;
70
- const pdfAction = digest ? <ExportPdfButton digestKey={`month-${yearMonth}`} /> : null;
71
70
 
72
- let actions: ReactNode = null;
73
- if (!narrativeFresh && aiEnabled) {
74
- actions = (
75
- <button
76
- onClick={() => generate(false)}
77
- disabled={isStreaming}
78
- style={{
79
- padding: "4px 10px",
80
- background: "var(--af-accent)",
81
- color: "white",
82
- border: "none",
83
- borderRadius: 5,
84
- cursor: isStreaming ? "default" : "pointer",
85
- fontSize: 11,
86
- fontWeight: 600,
87
- opacity: isStreaming ? 0.6 : 1,
88
- }}
89
- >
90
- {isStreaming ? "Generating..." : digest ? "Generate" : "Generate digest"}
91
- </button>
92
- );
93
- } else if (aiEnabled) {
94
- actions = (
95
- <span style={{ fontSize: 10, color: "var(--af-text-tertiary)", display: "inline-flex", gap: 6, alignItems: "center" }}>
96
- ✓ Up to date
97
- <button
98
- onClick={() => generate(true)}
99
- disabled={isStreaming}
100
- style={{
101
- padding: "2px 7px",
102
- background: "transparent",
103
- border: "1px solid var(--af-border-subtle)",
104
- borderRadius: 4,
105
- fontSize: 10,
106
- color: "var(--af-text-secondary)",
107
- cursor: isStreaming ? "default" : "pointer",
108
- opacity: isStreaming ? 0.6 : 1,
109
- }}
110
- >
111
- {isStreaming ? "..." : "Re-roll"}
112
- </button>
113
- </span>
114
- );
115
- }
116
- if (progress) {
117
- actions = (
118
- <>
119
- {actions}
120
- <span style={{ fontSize: 10, color: "var(--af-text-tertiary)" }}>{progress}</span>
121
- </>
122
- );
123
- }
124
- if (pdfAction) {
125
- actions = (
126
- <>
127
- {actions}
128
- {pdfAction}
129
- </>
130
- );
131
- }
71
+ const actions = (
72
+ <DigestActions
73
+ digestKey={yearMonth}
74
+ period="month"
75
+ hasDigest={!!digest}
76
+ narrativeFresh={narrativeFresh}
77
+ isStreaming={isStreaming}
78
+ aiEnabled={aiEnabled}
79
+ progress={progress}
80
+ onGenerate={() => generate(false)}
81
+ onReroll={() => generate(true)}
82
+ />
83
+ );
132
84
 
133
85
  if (digest) {
134
86
  return <MonthDigestRender digest={digest} aiEnabled={aiEnabled} actions={actions} />;
135
87
  }
88
+ // Hide the action bar when DigestActions would render nothing (no AI, no progress,
89
+ // no digest → no PDF). Otherwise the empty wrapper leaves visible padding.
90
+ const hasActionContent = aiEnabled || !!progress;
136
91
  return (
137
92
  <>
138
- {actions && (
93
+ {hasActionContent && (
139
94
  <div style={{ display: "flex", gap: 10, padding: "14px 40px 0", alignItems: "center", flexWrap: "wrap" }}>
140
95
  {actions}
141
96
  </div>
@@ -34,7 +34,19 @@ type Label = "icon" | "text" | "both";
34
34
 
35
35
  type Props =
36
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 };
37
+ | {
38
+ outcome: null;
39
+ pending: true;
40
+ sessionId?: string;
41
+ localDay: string;
42
+ size?: Size;
43
+ agent?: AgentKind;
44
+ // When the pill is rendered inside another anchor (e.g. a SessionCard
45
+ // whose whole body is a <Link>), render a plain <span> instead of a
46
+ // nested <Link> — nested <a> is invalid HTML and trips hydration. The
47
+ // host is then responsible for the click → /digest navigation.
48
+ noLink?: boolean;
49
+ };
38
50
 
39
51
  export function OutcomePill(props: Props) {
40
52
  const size = props.size ?? "md";
@@ -56,15 +68,23 @@ export function OutcomePill(props: Props) {
56
68
  textDecoration: "none",
57
69
  whiteSpace: "nowrap",
58
70
  };
71
+ const inner = (
72
+ <>
73
+ {props.agent && <AgentIcon agent={props.agent} size={agentIconSize} />}
74
+ <span aria-hidden style={{ lineHeight: 1 }}>{PENDING_STYLE.icon}</span>
75
+ <span>{PENDING_STYLE.label}</span>
76
+ </>
77
+ );
78
+ if (props.noLink) {
79
+ return <span style={style}>{inner}</span>;
80
+ }
59
81
  return (
60
82
  <Link
61
83
  href={`/digest/${props.localDay}`}
62
84
  style={style}
63
85
  title={`Generate ${props.localDay} digest →`}
64
86
  >
65
- {props.agent && <AgentIcon agent={props.agent} size={agentIconSize} />}
66
- <span aria-hidden style={{ lineHeight: 1 }}>{PENDING_STYLE.icon}</span>
67
- <span>{PENDING_STYLE.label}</span>
87
+ {inner}
68
88
  </Link>
69
89
  );
70
90
  }
@@ -0,0 +1,215 @@
1
+ "use client";
2
+
3
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { ChevronDown, Search } from "lucide-react";
5
+
6
+ export type SelectOption = { value: string; label: string };
7
+
8
+ /** A native <select> can't be filtered, which is painful once there are dozens
9
+ * of options (e.g. the project list across many folders). This is a drop-in
10
+ * combobox: click to open, type to filter by label, arrow/Enter/Escape to
11
+ * navigate. Selection is still a single string value via onChange. */
12
+ export function SearchableSelect({
13
+ value,
14
+ onChange,
15
+ options,
16
+ placeholder = "Select…",
17
+ searchPlaceholder = "Type to filter…",
18
+ maxWidth = 260,
19
+ title,
20
+ }: {
21
+ value: string;
22
+ onChange: (v: string) => void;
23
+ options: SelectOption[];
24
+ placeholder?: string;
25
+ searchPlaceholder?: string;
26
+ maxWidth?: number;
27
+ title?: string;
28
+ }) {
29
+ const [open, setOpen] = useState(false);
30
+ const [q, setQ] = useState("");
31
+ const [active, setActive] = useState(0);
32
+ const buttonRef = useRef<HTMLButtonElement>(null);
33
+ const inputRef = useRef<HTMLInputElement>(null);
34
+ const listRef = useRef<HTMLDivElement>(null);
35
+ const baseId = useId();
36
+ const listboxId = `${baseId}-listbox`;
37
+ const optionId = (i: number) => `${baseId}-opt-${i}`;
38
+
39
+ const selected = options.find((o) => o.value === value);
40
+ const filtered = useMemo(() => {
41
+ const s = q.trim().toLowerCase();
42
+ if (!s) return options;
43
+ return options.filter((o) => o.label.toLowerCase().includes(s));
44
+ }, [q, options]);
45
+
46
+ // On open only: clear the filter and highlight the current value (a native
47
+ // select opens on its selection), then focus the search box. Deps are [open]
48
+ // alone on purpose — `options` is a fresh array each parent render, so
49
+ // including it would reset the filter mid-typing.
50
+ useEffect(() => {
51
+ if (!open) return;
52
+ setQ("");
53
+ setActive(Math.max(0, options.findIndex((o) => o.value === value)));
54
+ const id = requestAnimationFrame(() => inputRef.current?.focus());
55
+ return () => cancelAnimationFrame(id);
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [open]);
58
+
59
+ // Keep the active option scrolled into view as the user arrows through.
60
+ useEffect(() => {
61
+ const el = listRef.current?.children[active] as HTMLElement | undefined;
62
+ el?.scrollIntoView({ block: "nearest" });
63
+ }, [active]);
64
+
65
+ // Close and return focus to the trigger (a native select refocuses itself).
66
+ const close = () => {
67
+ setOpen(false);
68
+ buttonRef.current?.focus();
69
+ };
70
+ const choose = (v: string) => {
71
+ onChange(v);
72
+ close();
73
+ };
74
+
75
+ const activeId = filtered[active] ? optionId(active) : undefined;
76
+
77
+ return (
78
+ <div style={{ position: "relative", maxWidth }}>
79
+ <button
80
+ ref={buttonRef}
81
+ type="button"
82
+ onClick={() => setOpen((o) => !o)}
83
+ title={title}
84
+ aria-haspopup="listbox"
85
+ aria-expanded={open}
86
+ style={{
87
+ display: "flex",
88
+ alignItems: "center",
89
+ gap: 6,
90
+ width: "100%",
91
+ background: "var(--background)",
92
+ border: `1px solid ${open ? "var(--af-accent)" : "var(--af-border-subtle)"}`,
93
+ borderRadius: 8,
94
+ padding: "9px 12px",
95
+ color: "var(--af-text)",
96
+ fontSize: 13,
97
+ cursor: "pointer",
98
+ textAlign: "left",
99
+ }}
100
+ >
101
+ <span
102
+ style={{
103
+ overflow: "hidden",
104
+ textOverflow: "ellipsis",
105
+ whiteSpace: "nowrap",
106
+ color: selected ? "var(--af-text)" : "var(--af-text-tertiary)",
107
+ }}
108
+ >
109
+ {selected?.label ?? placeholder}
110
+ </span>
111
+ <ChevronDown size={13} style={{ marginLeft: "auto", flexShrink: 0, color: "var(--af-text-tertiary)" }} />
112
+ </button>
113
+
114
+ {open && (
115
+ <>
116
+ {/* Click-away backdrop. */}
117
+ <div onClick={() => setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 40 }} />
118
+ <div
119
+ style={{
120
+ position: "absolute",
121
+ top: "calc(100% + 4px)",
122
+ left: 0,
123
+ minWidth: "100%",
124
+ width: "max-content",
125
+ maxWidth: 380,
126
+ zIndex: 41,
127
+ background: "var(--af-surface)",
128
+ border: "1px solid var(--af-border-subtle)",
129
+ borderRadius: 8,
130
+ boxShadow: "0 8px 24px rgba(0,0,0,0.28)",
131
+ overflow: "hidden",
132
+ }}
133
+ >
134
+ <div style={{ position: "relative", padding: 6, borderBottom: "1px solid var(--af-border-subtle)" }}>
135
+ <Search
136
+ size={12}
137
+ style={{ position: "absolute", left: 15, top: "50%", transform: "translateY(-50%)", color: "var(--af-text-tertiary)" }}
138
+ />
139
+ <input
140
+ ref={inputRef}
141
+ type="text"
142
+ role="combobox"
143
+ aria-expanded={open}
144
+ aria-controls={listboxId}
145
+ aria-activedescendant={activeId}
146
+ aria-autocomplete="list"
147
+ value={q}
148
+ onChange={(e) => {
149
+ setQ(e.target.value);
150
+ setActive(0); // typing narrows the list; keep the highlight on a real row
151
+ }}
152
+ placeholder={searchPlaceholder}
153
+ onKeyDown={(e) => {
154
+ if (e.key === "ArrowDown") {
155
+ e.preventDefault();
156
+ setActive((a) => Math.min(a + 1, Math.max(0, filtered.length - 1)));
157
+ } else if (e.key === "ArrowUp") {
158
+ e.preventDefault();
159
+ setActive((a) => Math.max(a - 1, 0));
160
+ } else if (e.key === "Enter") {
161
+ e.preventDefault();
162
+ const opt = filtered[active];
163
+ if (opt) choose(opt.value);
164
+ } else if (e.key === "Escape") {
165
+ e.preventDefault();
166
+ close();
167
+ } else if (e.key === "Tab") {
168
+ // Tab moves browser focus out naturally; close the dropdown
169
+ // so it doesn't linger visible until a click-away.
170
+ setOpen(false);
171
+ }
172
+ }}
173
+ style={{ width: "100%", paddingLeft: 30, fontSize: 12 }}
174
+ />
175
+ </div>
176
+ <div ref={listRef} role="listbox" id={listboxId} style={{ maxHeight: 300, overflowY: "auto", padding: 4 }}>
177
+ {filtered.length === 0 ? (
178
+ <div style={{ padding: "8px 10px", fontSize: 12, color: "var(--af-text-tertiary)" }}>No matches</div>
179
+ ) : (
180
+ filtered.map((o, i) => (
181
+ <button
182
+ key={o.value}
183
+ id={optionId(i)}
184
+ type="button"
185
+ role="option"
186
+ aria-selected={o.value === value}
187
+ onClick={() => choose(o.value)}
188
+ onMouseEnter={() => setActive(i)}
189
+ style={{
190
+ display: "block",
191
+ width: "100%",
192
+ textAlign: "left",
193
+ padding: "6px 10px",
194
+ fontSize: 12,
195
+ border: "none",
196
+ borderRadius: 4,
197
+ cursor: "pointer",
198
+ background: i === active ? "var(--af-surface-hover)" : "transparent",
199
+ color: o.value === value ? "var(--af-accent)" : "var(--af-text)",
200
+ whiteSpace: "nowrap",
201
+ overflow: "hidden",
202
+ textOverflow: "ellipsis",
203
+ }}
204
+ >
205
+ {o.label}
206
+ </button>
207
+ ))
208
+ )}
209
+ </div>
210
+ </div>
211
+ </>
212
+ )}
213
+ </div>
214
+ );
215
+ }
@@ -11,10 +11,20 @@
11
11
  * - Shows "Following live" indicator when tailing
12
12
  */
13
13
 
14
- import { useEffect, useRef, useState, useCallback } from "react";
14
+ import { useEffect, useRef, useState, useCallback, type MutableRefObject } from "react";
15
15
  import { ArrowDown, Radio } from "lucide-react";
16
16
 
17
- export function TailMode({ isLive }: { isLive: boolean }) {
17
+ export function TailMode({
18
+ isLive,
19
+ navLockRef,
20
+ }: {
21
+ isLive: boolean;
22
+ /** SessionView stamps Date.now() here on every explicit day-jump. While the
23
+ * stamp is fresh the live-follow observer bails, so a streaming event can't
24
+ * yank the view back to the bottom mid-navigation. Read synchronously — a
25
+ * setState couldn't disconnect the observer before its callback fires. */
26
+ navLockRef?: MutableRefObject<number>;
27
+ }) {
18
28
  const [tailing, setTailing] = useState(isLive);
19
29
  const [atBottom, setAtBottom] = useState(false);
20
30
  const mainRef = useRef<HTMLElement | null>(null);
@@ -24,10 +34,15 @@ export function TailMode({ isLive }: { isLive: boolean }) {
24
34
  useEffect(() => {
25
35
  mainRef.current = document.querySelector("main");
26
36
  if (isLive && mainRef.current) {
27
- // Small delay so the initial render settles before scrolling.
37
+ // Small delay so the initial render settles before scrolling. Use an
38
+ // INSTANT scroll, not smooth: a smooth animation emits a stream of scroll
39
+ // events, and check() reads the second one (still mid-animation, not yet
40
+ // at the bottom) as a user scroll-up and kills tailing — so on a heavy
41
+ // session the view never reaches the live tail. Instant lands in one
42
+ // event, which check() correctly sees as "at bottom".
28
43
  const t = setTimeout(() => {
29
44
  userScrolledRef.current = false;
30
- mainRef.current?.scrollTo({ top: mainRef.current.scrollHeight, behavior: "smooth" });
45
+ mainRef.current?.scrollTo({ top: mainRef.current.scrollHeight, behavior: "auto" });
31
46
  }, 300);
32
47
  return () => clearTimeout(t);
33
48
  }
@@ -67,18 +82,34 @@ export function TailMode({ isLive }: { isLive: boolean }) {
67
82
  // Use a MutationObserver to detect when new content is added
68
83
  // (the RSC re-render appends new transcript rows to the DOM).
69
84
  const observer = new MutationObserver(() => {
85
+ // Bail while a day-jump is in flight: SessionView just scrolled the view
86
+ // to an earlier day, and a streaming live event must NOT yank it back to
87
+ // the bottom (that fight was the day-nav "flicker" on live sessions). A
88
+ // moment later check() sees the scroll is away from the bottom and turns
89
+ // tailing off for good. NOTE: do not gate on distance-from-bottom instead
90
+ // — on a heavy session the content streams in while we're still far from
91
+ // the bottom, and a distance gate would abandon the initial catch-up.
92
+ if (navLockRef && Date.now() - navLockRef.current < 1500) return;
70
93
  userScrolledRef.current = false; // suppress the scroll-up detection
71
- main.scrollTo({ top: main.scrollHeight, behavior: "smooth" });
94
+ // Instant, not smooth: a smooth animation emits scroll events across many
95
+ // frames and check() reads a mid-animation one (still away from the bottom)
96
+ // as a user scroll-up, killing tailing after the first sizeable streamed
97
+ // append. Same reason as the immediate scroll below and the mount scroll.
98
+ main.scrollTo({ top: main.scrollHeight, behavior: "auto" });
72
99
  });
73
100
 
74
101
  observer.observe(main, { childList: true, subtree: true });
75
102
 
76
- // Also scroll immediately when tail mode is first enabled.
103
+ // Also scroll immediately when tail mode is first enabled. Instant (not
104
+ // smooth) for the same reason as the mount scroll above — a smooth
105
+ // animation's scroll events trip check() into disabling tailing mid-flight.
77
106
  userScrolledRef.current = false;
78
- main.scrollTo({ top: main.scrollHeight, behavior: "smooth" });
107
+ main.scrollTo({ top: main.scrollHeight, behavior: "auto" });
79
108
 
80
109
  return () => observer.disconnect();
81
- }, [tailing]);
110
+ // navLockRef is a stable ref; listed to satisfy exhaustive-deps without
111
+ // causing re-runs.
112
+ }, [tailing, navLockRef]);
82
113
 
83
114
  const jumpToLatest = useCallback(() => {
84
115
  const main = mainRef.current;