fleetlens 0.13.0 → 0.13.2

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 (211) 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.nft.json +1 -1
  27. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  29. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  33. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
  35. package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +5 -5
  36. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  37. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  38. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  39. package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +5 -5
  40. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  41. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  42. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  43. package/app/apps/web/.next/server/app/day/page/build-manifest.json +5 -5
  44. package/app/apps/web/.next/server/app/day/page.js +1 -1
  45. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  46. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  47. package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +5 -5
  48. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  49. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  50. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  51. package/app/apps/web/.next/server/app/digest/page/build-manifest.json +5 -5
  52. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  53. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  54. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  55. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +5 -5
  56. package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
  57. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  58. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  59. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +5 -5
  60. package/app/apps/web/.next/server/app/insights/page.js +3 -2
  61. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  62. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  63. package/app/apps/web/.next/server/app/page/build-manifest.json +5 -5
  64. package/app/apps/web/.next/server/app/page.js +1 -1
  65. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  66. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  67. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +5 -5
  68. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  69. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  70. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  71. package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +5 -5
  72. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  73. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  74. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  75. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +5 -5
  76. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
  77. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  78. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  79. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +5 -5
  80. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  81. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  82. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  83. package/app/apps/web/.next/server/app/runs/page/build-manifest.json +5 -5
  84. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  85. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  86. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  87. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +5 -5
  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/build-manifest.json +5 -5
  92. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  93. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  94. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  95. package/app/apps/web/.next/server/app/settings/page/build-manifest.json +5 -5
  96. package/app/apps/web/.next/server/app/settings/page.js +1 -1
  97. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  98. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  99. package/app/apps/web/.next/server/app/team/page/build-manifest.json +5 -5
  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/build-manifest.json +5 -5
  104. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  105. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  106. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  107. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gril0u._.js +9 -0
  108. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0n79hpm._.js +9 -0
  109. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~utv~v._.js +9 -0
  110. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  111. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  112. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
  113. 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
  114. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +8 -0
  115. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +1 -1
  116. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
  117. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
  118. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
  119. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  120. package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
  121. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  122. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
  124. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
  125. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  127. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  129. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  130. package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
  131. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  132. package/app/apps/web/.next/server/pages/500.html +1 -1
  133. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  134. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  135. package/app/apps/web/.next/static/chunks/02xxpexzk3rf-.js +1 -0
  136. package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
  137. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
  138. package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
  139. package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
  140. package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
  141. package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
  142. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
  143. package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
  144. package/app/apps/web/.next/static/chunks/{0gblp_v.kiu-..js → 0qv-gw888_5nu.js} +1 -1
  145. package/app/apps/web/.next/static/chunks/{14bjditmmrn7l.js → 0tirv_vo6q8uc.js} +1 -1
  146. package/app/apps/web/.next/static/chunks/0uyctl-m8iydb.js +1 -0
  147. package/app/apps/web/.next/static/chunks/0~qdw1b2uz._5.js +10 -0
  148. package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
  149. package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
  150. package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
  151. package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
  152. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
  153. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
  154. package/app/apps/web/app/insights/page.tsx +20 -5
  155. package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
  156. package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
  157. package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
  158. package/app/apps/web/app/parallelism/gantt-chart.tsx +24 -903
  159. package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
  160. package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
  161. package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
  162. package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
  163. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
  164. package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
  165. package/app/apps/web/app/sessions/[id]/session-view/minimap.tsx +1587 -0
  166. package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
  167. package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
  168. package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
  169. package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
  170. package/app/apps/web/app/sessions/[id]/session-view/workflows-panel.tsx +944 -0
  171. package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
  172. package/app/apps/web/app/sessions/[id]/session-view.tsx +1513 -5018
  173. package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
  174. package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
  175. package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
  176. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
  177. package/app/apps/web/app/sessions/sessions-grid.tsx +10 -10
  178. package/app/apps/web/components/digest-actions.tsx +103 -0
  179. package/app/apps/web/components/job-queue-widget.tsx +1 -1
  180. package/app/apps/web/components/month-digest-view.tsx +19 -64
  181. package/app/apps/web/components/searchable-select.tsx +215 -0
  182. package/app/apps/web/components/tail-mode.tsx +46 -9
  183. package/app/apps/web/components/week-digest-view.tsx +16 -71
  184. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  185. package/app/apps/web/lib/entries-index.ts +7 -8
  186. package/app/apps/web/lib/entries.ts +0 -9
  187. package/app/apps/web/lib/format.test.ts +221 -0
  188. package/app/apps/web/lib/format.ts +17 -0
  189. package/app/apps/web/lib/utilization-tone.test.ts +56 -0
  190. package/app/apps/web/package.json +1 -1
  191. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  192. package/dist/daemon-worker.js +35 -43
  193. package/dist/index.js +37 -49
  194. package/package.json +1 -1
  195. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
  196. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
  197. package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
  198. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
  199. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
  200. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
  201. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
  202. package/app/apps/web/.next/static/chunks/05k84gif3ucxm.js +0 -6
  203. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
  204. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
  205. package/app/apps/web/.next/static/chunks/0pzt9vemtzct8.js +0 -10
  206. package/app/apps/web/.next/static/chunks/0qmh5da6gfm56.js +0 -1
  207. package/app/apps/web/.next/static/chunks/0rnay-a.-jr8-.js +0 -1
  208. package/app/apps/web/.next/static/chunks/0snv3-2ux2xvv.js +0 -6
  209. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_buildManifest.js +0 -0
  210. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_clientMiddlewareManifest.js +0 -0
  211. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_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 (
@@ -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>
@@ -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,14 +34,25 @@ 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(() => {
44
+ // An explicit day-jump (gotoDay, e.g. opening a live session via
45
+ // ?day=<earlier day>) stamps navLockRef and scrolls to that past day —
46
+ // don't yank the view back to the live tail on top of it.
47
+ if (navLockRef && Date.now() - navLockRef.current < 1500) return;
29
48
  userScrolledRef.current = false;
30
- mainRef.current?.scrollTo({ top: mainRef.current.scrollHeight, behavior: "smooth" });
49
+ mainRef.current?.scrollTo({ top: mainRef.current.scrollHeight, behavior: "auto" });
31
50
  }, 300);
32
51
  return () => clearTimeout(t);
33
52
  }
34
- }, [isLive]);
53
+ // navLockRef is a stable ref; listed to satisfy exhaustive-deps without
54
+ // causing re-runs (mirrors the observer effect below).
55
+ }, [isLive, navLockRef]);
35
56
 
36
57
  // Track whether we're at the bottom.
37
58
  useEffect(() => {
@@ -67,18 +88,34 @@ export function TailMode({ isLive }: { isLive: boolean }) {
67
88
  // Use a MutationObserver to detect when new content is added
68
89
  // (the RSC re-render appends new transcript rows to the DOM).
69
90
  const observer = new MutationObserver(() => {
91
+ // Bail while a day-jump is in flight: SessionView just scrolled the view
92
+ // to an earlier day, and a streaming live event must NOT yank it back to
93
+ // the bottom (that fight was the day-nav "flicker" on live sessions). A
94
+ // moment later check() sees the scroll is away from the bottom and turns
95
+ // tailing off for good. NOTE: do not gate on distance-from-bottom instead
96
+ // — on a heavy session the content streams in while we're still far from
97
+ // the bottom, and a distance gate would abandon the initial catch-up.
98
+ if (navLockRef && Date.now() - navLockRef.current < 1500) return;
70
99
  userScrolledRef.current = false; // suppress the scroll-up detection
71
- main.scrollTo({ top: main.scrollHeight, behavior: "smooth" });
100
+ // Instant, not smooth: a smooth animation emits scroll events across many
101
+ // frames and check() reads a mid-animation one (still away from the bottom)
102
+ // as a user scroll-up, killing tailing after the first sizeable streamed
103
+ // append. Same reason as the immediate scroll below and the mount scroll.
104
+ main.scrollTo({ top: main.scrollHeight, behavior: "auto" });
72
105
  });
73
106
 
74
107
  observer.observe(main, { childList: true, subtree: true });
75
108
 
76
- // Also scroll immediately when tail mode is first enabled.
109
+ // Also scroll immediately when tail mode is first enabled. Instant (not
110
+ // smooth) for the same reason as the mount scroll above — a smooth
111
+ // animation's scroll events trip check() into disabling tailing mid-flight.
77
112
  userScrolledRef.current = false;
78
- main.scrollTo({ top: main.scrollHeight, behavior: "smooth" });
113
+ main.scrollTo({ top: main.scrollHeight, behavior: "auto" });
79
114
 
80
115
  return () => observer.disconnect();
81
- }, [tailing]);
116
+ // navLockRef is a stable ref; listed to satisfy exhaustive-deps without
117
+ // causing re-runs.
118
+ }, [tailing, navLockRef]);
82
119
 
83
120
  const jumpToLatest = useCallback(() => {
84
121
  const main = mainRef.current;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { useState, useCallback, useEffect, useRef, type ReactNode } from "react";
2
+ import { useState, useCallback, useEffect, useRef } from "react";
3
3
  import { WeekDigest as WeekDigestRender } from "./week-digest";
4
- import { ExportPdfButton } from "./export-pdf-button";
4
+ import { DigestActions } from "./digest-actions";
5
5
  import type { WeekDigest as WeekDigestType } from "@claude-lens/entries";
6
6
 
7
7
  type Status = "idle" | "streaming" | "done" | "error";
@@ -77,50 +77,21 @@ export function WeekDigestView({
77
77
 
78
78
  const isStreaming = status === "streaming";
79
79
  const narrativeFresh = !!digest?.headline;
80
- const pdfAction = digest ? <ExportPdfButton digestKey={`week-${monday}`} /> : null;
81
80
 
82
- let actions: ReactNode = null;
83
- if (!narrativeFresh && aiEnabled) {
84
- actions = (
85
- <button
86
- onClick={() => generate(false)}
87
- disabled={isStreaming}
88
- style={btnPrimary(isStreaming)}
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
- title="Re-roll the narrative for this week"
101
- style={btnSecondary(isStreaming)}
102
- >
103
- {isStreaming ? "..." : "Re-roll"}
104
- </button>
105
- </span>
106
- );
107
- }
108
- if (progress) {
109
- actions = (
110
- <>
111
- {actions}
112
- <span style={{ fontSize: 10, color: "var(--af-text-tertiary)" }}>{progress}</span>
113
- </>
114
- );
115
- }
116
- if (pdfAction) {
117
- actions = (
118
- <>
119
- {actions}
120
- {pdfAction}
121
- </>
122
- );
123
- }
81
+ const actions = (
82
+ <DigestActions
83
+ digestKey={monday}
84
+ period="week"
85
+ hasDigest={!!digest}
86
+ narrativeFresh={narrativeFresh}
87
+ isStreaming={isStreaming}
88
+ aiEnabled={aiEnabled}
89
+ progress={progress}
90
+ onGenerate={() => generate(false)}
91
+ onReroll={() => generate(true)}
92
+ rerollTitle="Re-roll the narrative for this week"
93
+ />
94
+ );
124
95
 
125
96
  if (digest) {
126
97
  return <WeekDigestRender digest={digest} aiEnabled={aiEnabled} actions={actions} priorDigest={prior ?? null} />;
@@ -208,29 +179,3 @@ function EmptyWeekState({ monday, isStreaming, aiEnabled, progress, onGenerate }
208
179
  );
209
180
  }
210
181
 
211
- function btnPrimary(disabled: boolean): React.CSSProperties {
212
- return {
213
- padding: "4px 10px",
214
- background: "var(--af-accent)",
215
- color: "white",
216
- border: "none",
217
- borderRadius: 5,
218
- cursor: disabled ? "default" : "pointer",
219
- fontSize: 11,
220
- fontWeight: 600,
221
- opacity: disabled ? 0.6 : 1,
222
- };
223
- }
224
-
225
- function btnSecondary(disabled: boolean): React.CSSProperties {
226
- return {
227
- padding: "2px 7px",
228
- background: "transparent",
229
- border: "1px solid var(--af-border-subtle)",
230
- borderRadius: 4,
231
- fontSize: 10,
232
- color: "var(--af-text-secondary)",
233
- cursor: disabled ? "default" : "pointer",
234
- opacity: disabled ? 0.6 : 1,
235
- };
236
- }