fleetlens 0.13.0 → 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 (205) 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]__0~.f_3z._.js +3 -3
  116. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
  117. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
  118. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  119. package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
  120. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  121. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
  122. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
  124. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  125. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
  127. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  128. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  129. package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
  130. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  131. package/app/apps/web/.next/server/pages/500.html +1 -1
  132. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  133. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  134. package/app/apps/web/.next/static/chunks/{0pzt9vemtzct8.js → 02bv_ejzhhzvp.js} +3 -3
  135. package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
  136. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
  137. package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
  138. package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
  139. package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
  140. package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
  141. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
  142. package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
  143. package/app/apps/web/.next/static/chunks/0q8dg4txlzno0.js +1 -0
  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/{0qmh5da6gfm56.js → 0vz4~a~~hq399.js} +1 -1
  147. package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
  148. package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
  149. package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
  150. package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
  151. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
  152. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
  153. package/app/apps/web/app/insights/page.tsx +20 -5
  154. package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
  155. package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
  156. package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
  157. package/app/apps/web/app/parallelism/gantt-chart.tsx +16 -902
  158. package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
  159. package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
  160. package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
  161. package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
  162. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
  163. package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
  164. package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
  165. package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
  166. package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
  167. package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
  168. package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
  169. package/app/apps/web/app/sessions/[id]/session-view.tsx +448 -1639
  170. package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
  171. package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
  172. package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
  173. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
  174. package/app/apps/web/app/sessions/sessions-grid.tsx +10 -10
  175. package/app/apps/web/components/digest-actions.tsx +103 -0
  176. package/app/apps/web/components/job-queue-widget.tsx +1 -1
  177. package/app/apps/web/components/month-digest-view.tsx +19 -64
  178. package/app/apps/web/components/searchable-select.tsx +215 -0
  179. package/app/apps/web/components/tail-mode.tsx +39 -8
  180. package/app/apps/web/components/week-digest-view.tsx +16 -71
  181. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  182. package/app/apps/web/lib/entries-index.ts +7 -8
  183. package/app/apps/web/lib/entries.ts +0 -9
  184. package/app/apps/web/lib/format.test.ts +221 -0
  185. package/app/apps/web/lib/utilization-tone.test.ts +56 -0
  186. package/app/apps/web/package.json +1 -1
  187. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  188. package/dist/daemon-worker.js +35 -43
  189. package/dist/index.js +37 -49
  190. package/package.json +1 -1
  191. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
  192. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
  193. package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
  194. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
  195. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
  196. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
  197. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
  198. package/app/apps/web/.next/static/chunks/05k84gif3ucxm.js +0 -6
  199. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
  200. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
  201. package/app/apps/web/.next/static/chunks/0rnay-a.-jr8-.js +0 -1
  202. package/app/apps/web/.next/static/chunks/0snv3-2ux2xvv.js +0 -6
  203. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_buildManifest.js +0 -0
  204. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_clientMiddlewareManifest.js +0 -0
  205. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → 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 (
@@ -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,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;
@@ -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
- }