fleetlens 0.4.0 → 0.6.0

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 (440) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +16 -7
  3. package/app/apps/web/.next/build-manifest.json +7 -7
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/required-server-files.json +2 -1
  6. package/app/apps/web/.next/routes-manifest.json +86 -24
  7. package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +4 -4
  8. package/app/apps/web/.next/server/app/_global-error/page.js +1 -1
  9. package/app/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.rsc +7 -7
  13. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  14. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  16. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  17. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +4 -4
  19. package/app/apps/web/.next/server/app/_not-found/page.js +5 -4
  20. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/day/[date]/route/app-paths-manifest.json +3 -0
  23. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +9 -0
  24. package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/day/[date]}/route.js.nft.json +1 -1
  25. package/app/apps/web/.next/server/app/api/digest/day/[date]/route_client-reference-manifest.js +3 -0
  26. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route/app-paths-manifest.json +3 -0
  27. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +9 -0
  28. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -0
  29. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route_client-reference-manifest.js +3 -0
  30. package/app/apps/web/.next/server/app/api/digest/month-index/route/app-paths-manifest.json +3 -0
  31. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +7 -0
  32. package/app/apps/web/.next/server/app/api/{insights/months-index → digest/month-index}/route.js.nft.json +1 -1
  33. package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route_client-reference-manifest.js +1 -1
  34. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route/app-paths-manifest.json +3 -0
  35. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +9 -0
  36. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -0
  37. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route_client-reference-manifest.js +3 -0
  38. package/app/apps/web/.next/server/app/api/digest/week-index/route/app-paths-manifest.json +3 -0
  39. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +7 -0
  40. package/app/apps/web/.next/server/app/api/{insights/weeks-index → digest/week-index}/route.js.nft.json +1 -1
  41. package/app/apps/web/.next/server/app/api/digest/week-index/route_client-reference-manifest.js +3 -0
  42. package/app/apps/web/.next/server/app/api/jobs/route/app-paths-manifest.json +3 -0
  43. package/app/apps/web/.next/server/app/api/jobs/route.js +6 -0
  44. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -0
  45. package/app/apps/web/.next/server/app/api/jobs/route_client-reference-manifest.js +3 -0
  46. package/app/apps/web/.next/server/app/api/runs/[runId]/route/app-paths-manifest.json +3 -0
  47. package/app/apps/web/.next/server/app/api/runs/[runId]/route/build-manifest.json +9 -0
  48. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js +6 -0
  49. package/app/apps/web/.next/server/app/api/{insights/saved → runs/[runId]}/route.js.nft.json +1 -1
  50. package/app/apps/web/.next/server/app/api/runs/[runId]/route_client-reference-manifest.js +3 -0
  51. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route/app-paths-manifest.json +3 -0
  52. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route/build-manifest.json +9 -0
  53. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route/server-reference-manifest.json +4 -0
  54. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js +6 -0
  55. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.map +5 -0
  56. package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → runs/[runId]/stream}/route.js.nft.json +1 -1
  57. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route_client-reference-manifest.js +3 -0
  58. package/app/apps/web/.next/server/app/api/runs/route/app-paths-manifest.json +3 -0
  59. package/app/apps/web/.next/server/app/api/runs/route/build-manifest.json +9 -0
  60. package/app/apps/web/.next/server/app/api/runs/route/server-reference-manifest.json +4 -0
  61. package/app/apps/web/.next/server/app/api/runs/route.js +6 -0
  62. package/app/apps/web/.next/server/app/api/runs/route.js.map +5 -0
  63. package/app/apps/web/.next/server/app/api/{insights → runs}/route.js.nft.json +1 -1
  64. package/app/apps/web/.next/server/app/api/runs/route_client-reference-manifest.js +3 -0
  65. package/app/apps/web/.next/server/app/api/settings/route/app-paths-manifest.json +3 -0
  66. package/app/apps/web/.next/server/app/api/settings/route/build-manifest.json +9 -0
  67. package/app/apps/web/.next/server/app/api/settings/route/server-reference-manifest.json +4 -0
  68. package/app/apps/web/.next/server/app/api/settings/route.js +10 -0
  69. package/app/apps/web/.next/server/app/api/settings/route.js.map +5 -0
  70. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -0
  71. package/app/apps/web/.next/server/app/api/{insights → settings}/route_client-reference-manifest.js +1 -1
  72. package/app/apps/web/.next/server/app/day/[date]/page/app-paths-manifest.json +3 -0
  73. package/app/apps/web/.next/server/app/{insights/print/[key] → day/[date]}/page/build-manifest.json +4 -4
  74. package/app/apps/web/.next/server/app/day/[date]/page/server-reference-manifest.json +4 -0
  75. package/app/apps/web/.next/server/app/day/[date]/page.js +18 -0
  76. package/app/apps/web/.next/server/app/day/[date]/page.js.map +5 -0
  77. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -0
  78. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +3 -0
  79. package/app/apps/web/.next/server/app/day/page/app-paths-manifest.json +3 -0
  80. package/app/apps/web/.next/server/app/day/page/build-manifest.json +17 -0
  81. package/app/apps/web/.next/server/app/day/page/next-font-manifest.json +6 -0
  82. package/app/apps/web/.next/server/app/day/page/react-loadable-manifest.json +1 -0
  83. package/app/apps/web/.next/server/app/day/page/server-reference-manifest.json +4 -0
  84. package/app/apps/web/.next/server/app/{insights/print/[key] → day}/page.js +11 -10
  85. package/app/apps/web/.next/server/app/day/page.js.map +5 -0
  86. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -0
  87. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +3 -0
  88. package/app/apps/web/.next/server/app/digest/[date]/page/app-paths-manifest.json +3 -0
  89. package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +17 -0
  90. package/app/apps/web/.next/server/app/digest/[date]/page/next-font-manifest.json +6 -0
  91. package/app/apps/web/.next/server/app/digest/[date]/page/react-loadable-manifest.json +1 -0
  92. package/app/apps/web/.next/server/app/digest/[date]/page/server-reference-manifest.json +4 -0
  93. package/app/apps/web/.next/server/app/digest/[date]/page.js +17 -0
  94. package/app/apps/web/.next/server/app/digest/[date]/page.js.map +5 -0
  95. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -0
  96. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +3 -0
  97. package/app/apps/web/.next/server/app/digest/page/app-paths-manifest.json +3 -0
  98. package/app/apps/web/.next/server/app/digest/page/build-manifest.json +17 -0
  99. package/app/apps/web/.next/server/app/digest/page/next-font-manifest.json +6 -0
  100. package/app/apps/web/.next/server/app/digest/page/react-loadable-manifest.json +1 -0
  101. package/app/apps/web/.next/server/app/digest/page/server-reference-manifest.json +4 -0
  102. package/app/apps/web/.next/server/app/digest/page.js +17 -0
  103. package/app/apps/web/.next/server/app/digest/page.js.map +5 -0
  104. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -0
  105. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +3 -0
  106. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +4 -4
  107. package/app/apps/web/.next/server/app/insights/[key]/page.js +7 -5
  108. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  109. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  110. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +4 -4
  111. package/app/apps/web/.next/server/app/insights/page.js +7 -5
  112. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  113. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  114. package/app/apps/web/.next/server/app/page/build-manifest.json +4 -4
  115. package/app/apps/web/.next/server/app/page.js +7 -6
  116. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  117. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  118. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +4 -4
  119. package/app/apps/web/.next/server/app/parallelism/page.js +6 -5
  120. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  121. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  122. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +4 -4
  123. package/app/apps/web/.next/server/app/projects/[slug]/page.js +6 -5
  124. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  125. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  126. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +4 -4
  127. package/app/apps/web/.next/server/app/projects/page.js +5 -4
  128. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  129. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  130. package/app/apps/web/.next/server/app/runs/page/app-paths-manifest.json +3 -0
  131. package/app/apps/web/.next/server/app/runs/page/build-manifest.json +17 -0
  132. package/app/apps/web/.next/server/app/runs/page/next-font-manifest.json +6 -0
  133. package/app/apps/web/.next/server/app/runs/page/react-loadable-manifest.json +1 -0
  134. package/app/apps/web/.next/server/app/runs/page/server-reference-manifest.json +4 -0
  135. package/app/apps/web/.next/server/app/runs/page.js +17 -0
  136. package/app/apps/web/.next/server/app/runs/page.js.map +5 -0
  137. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -0
  138. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +3 -0
  139. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +4 -4
  140. package/app/apps/web/.next/server/app/sessions/[id]/page.js +5 -4
  141. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  142. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  143. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +4 -4
  144. package/app/apps/web/.next/server/app/sessions/page.js +5 -4
  145. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  146. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  147. package/app/apps/web/.next/server/app/settings/page/app-paths-manifest.json +3 -0
  148. package/app/apps/web/.next/server/app/settings/page/build-manifest.json +17 -0
  149. package/app/apps/web/.next/server/app/settings/page/next-font-manifest.json +6 -0
  150. package/app/apps/web/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
  151. package/app/apps/web/.next/server/app/settings/page/server-reference-manifest.json +4 -0
  152. package/app/apps/web/.next/server/app/settings/page.js +18 -0
  153. package/app/apps/web/.next/server/app/settings/page.js.map +5 -0
  154. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -0
  155. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +3 -0
  156. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +4 -4
  157. package/app/apps/web/.next/server/app/usage/page.js +6 -5
  158. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  159. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  160. package/app/apps/web/.next/server/app-paths-manifest.json +16 -7
  161. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_digest_week_[startDate]_route_actions_0klqqsh.js +3 -0
  162. package/app/apps/web/.next/server/chunks/09og__next-internal_server_app_api_digest_month_[yearMonth]_route_actions_0i4ielu.js +3 -0
  163. package/app/apps/web/.next/server/chunks/[externals]__0o43ejh._.js +3 -0
  164. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0-vgrh1._.js +3 -0
  165. package/app/apps/web/.next/server/chunks/[root-of-the-server]__00n3f9y._.js +3 -0
  166. package/app/apps/web/.next/server/chunks/[root-of-the-server]__02zukjw._.js +3 -0
  167. package/app/apps/web/.next/server/chunks/[root-of-the-server]__067~im1._.js +6 -0
  168. package/app/apps/web/.next/server/chunks/[root-of-the-server]__06b4cj-._.js +3 -0
  169. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_8zh-i._.js +9 -0
  170. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e8n_x4._.js +3 -0
  171. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ej_p3s._.js +9 -0
  172. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +9 -0
  173. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_digest_day_[date]_route_actions_09sxie5.js +3 -0
  174. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_digest_month-index_route_actions_0wldpbu.js +3 -0
  175. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_digest_week-index_route_actions_12c2wj2.js +3 -0
  176. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_jobs_route_actions_08kx~8-.js +3 -0
  177. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_runs_[runId]_route_actions_0dfcghc.js +3 -0
  178. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_runs_[runId]_stream_route_actions_0.8e8-k.js +3 -0
  179. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_runs_route_actions_0xyx82f.js +3 -0
  180. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_settings_route_actions_0.xv7sx.js +3 -0
  181. package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +49 -0
  182. package/app/apps/web/.next/server/chunks/packages_0e7q9u_._.js +3 -0
  183. package/app/apps/web/.next/server/chunks/packages_0n6p2ot._.js +3 -0
  184. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +379 -0
  185. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +1 -1
  186. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-1_njm._.js +3 -0
  187. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-94ji~._.js +3 -0
  188. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__13xpdx0._.js → [root-of-the-server]__03uoeiw._.js} +2 -2
  189. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_6.inc._.js +1 -1
  190. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d~2~le._.js +8 -0
  191. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +1 -1
  192. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08.d5ia._.js → [root-of-the-server]__0jdyefm._.js} +10 -10
  193. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ln9jsw._.js +3 -0
  194. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0z45d0g._.js → [root-of-the-server]__0lz4a~g._.js} +2 -48
  195. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o1_g.~._.js +3 -0
  196. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +3 -0
  197. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +3 -0
  198. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0euu8sa._.js → [root-of-the-server]__0u39afl._.js} +2 -2
  199. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u5d7de._.js +12 -0
  200. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
  201. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wwa6gh._.js +3 -0
  202. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0xy1qim._.js +3 -0
  203. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~_8vff._.js +3 -0
  204. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10srpu0._.js +3 -0
  205. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11v.eqw._.js +1 -1
  206. package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +8 -0
  207. package/app/apps/web/.next/server/chunks/ssr/_0fjbf--._.js +3 -0
  208. package/app/apps/web/.next/server/chunks/ssr/_0jw2sm7._.js +3 -0
  209. package/app/apps/web/.next/server/chunks/ssr/_0wj750k._.js +3 -0
  210. package/app/apps/web/.next/server/chunks/ssr/_0woap-3._.js +1 -1
  211. package/app/apps/web/.next/server/chunks/ssr/{_0xies90._.js → _0xu2pvc._.js} +2 -2
  212. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_day_[date]_page_actions_0u2kxb..js +3 -0
  213. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_day_page_actions_1042maj.js +3 -0
  214. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_digest_[date]_page_actions_0olu967.js +3 -0
  215. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_digest_page_actions_0bl.eil.js +3 -0
  216. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_runs_page_actions_0485qy7.js +3 -0
  217. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_settings_page_actions_0f6epye.js +3 -0
  218. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +3 -0
  219. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_settings_ai-features-form_tsx_01p699p._.js +3 -0
  220. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0b-4-4j._.js +1 -1
  221. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +6 -0
  222. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +16 -0
  223. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0xs658s._.js +10 -0
  224. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_runs-live-board_tsx_0r6dkdq._.js +3 -0
  225. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_usage-charts-dashboard_tsx_0bgr_uv._.js +1 -1
  226. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_usage-data_ts_0xtv99n._.js +3 -0
  227. package/app/apps/web/.next/server/chunks/ssr/node_modules_0i2xw~e._.js +3 -0
  228. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0e1izl_._.js +1 -1
  229. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0d3d4wn.js +4 -0
  230. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0edz1ko.js +4 -0
  231. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0eq.n-n.js +4 -0
  232. package/app/apps/web/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0vz7_px.js → node_modules_next_dist_esm_build_templates_app-page_0hcvt-s.js} +3 -3
  233. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0nd5zu0.js +4 -0
  234. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_11j1qy0.js +4 -0
  235. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12n1lw_.js +1 -1
  236. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +3 -0
  237. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
  238. package/app/apps/web/.next/server/functions-config-manifest.json +11 -6
  239. package/app/apps/web/.next/server/middleware-build-manifest.js +7 -7
  240. package/app/apps/web/.next/server/pages/500.html +1 -1
  241. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  242. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  243. package/app/apps/web/.next/static/chunks/0-yc2_pjxwwuq.js +1 -0
  244. package/app/apps/web/.next/static/chunks/0257pdz1-imal.js +2 -0
  245. package/app/apps/web/.next/static/chunks/05m.oz3kmf1p7.js +1 -0
  246. package/app/apps/web/.next/static/chunks/{0pqt~8bl3ukh4.js → 073081qv4uyn8.js} +1 -1
  247. package/app/apps/web/.next/static/chunks/{0018dy-m-kusj.js → 07lbm-sdh9c2j.js} +1 -1
  248. package/app/apps/web/.next/static/chunks/0858bgw2i4vob.js +1 -0
  249. package/app/apps/web/.next/static/chunks/0_7303jr0sf0w.js +10 -0
  250. package/app/apps/web/.next/static/chunks/0byz8fqw.exef.js +1 -0
  251. package/app/apps/web/.next/static/chunks/0coa1800xqgfj.js +1 -0
  252. package/app/apps/web/.next/static/chunks/0fs3gxea5h0s3.css +1 -0
  253. package/app/apps/web/.next/static/chunks/0k1psab.bybh9.js +14 -0
  254. package/app/apps/web/.next/static/chunks/0k9pksi_i.odf.js +1 -0
  255. package/app/apps/web/.next/static/chunks/0m62vx92~0_0d.js +1 -0
  256. package/app/apps/web/.next/static/chunks/0mc8aa8z.0-5l.js +8 -0
  257. package/app/apps/web/.next/static/chunks/0mykrrv810ra-.js +31 -0
  258. package/app/apps/web/.next/static/chunks/0o9ayt8sknubd.js +1 -0
  259. package/app/apps/web/.next/static/chunks/0osdgl2.9cl1q.js +6 -0
  260. package/app/apps/web/.next/static/chunks/0xfu09fn_358k.js +4 -0
  261. package/app/apps/web/.next/static/chunks/171za22fv684..js +6 -0
  262. package/app/apps/web/.next/static/chunks/17u2ww8ya5dv1.js +6 -0
  263. package/app/apps/web/.next/static/chunks/{turbopack-169w-1._23ra_.js → turbopack-0_p36yduv.waj.js} +1 -1
  264. package/app/apps/web/app/api/digest/day/[date]/route.ts +152 -0
  265. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +147 -0
  266. package/app/apps/web/app/api/digest/month-index/route.ts +103 -0
  267. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +149 -0
  268. package/app/apps/web/app/api/{insights/weeks-index → digest/week-index}/route.ts +45 -33
  269. package/app/apps/web/app/api/jobs/route.ts +8 -0
  270. package/app/apps/web/app/api/runs/[runId]/route.ts +64 -0
  271. package/app/apps/web/app/api/runs/[runId]/stream/route.ts +154 -0
  272. package/app/apps/web/app/api/runs/route.ts +266 -0
  273. package/app/apps/web/app/api/settings/route.ts +35 -0
  274. package/app/apps/web/app/day/[date]/day-view.tsx +193 -0
  275. package/app/apps/web/app/day/[date]/page.tsx +175 -0
  276. package/app/apps/web/app/day/page.tsx +8 -0
  277. package/app/apps/web/app/digest/[date]/page.tsx +15 -0
  278. package/app/apps/web/app/digest/page.tsx +7 -0
  279. package/app/apps/web/app/insights/[key]/page.tsx +106 -48
  280. package/app/apps/web/app/insights/page.tsx +133 -3
  281. package/app/apps/web/app/layout.tsx +23 -3
  282. package/app/apps/web/app/page.tsx +33 -4
  283. package/app/apps/web/app/parallelism/gantt-chart.tsx +67 -1
  284. package/app/apps/web/app/parallelism/page.tsx +8 -278
  285. package/app/apps/web/app/projects/[slug]/page.tsx +153 -0
  286. package/app/apps/web/app/projects/page.tsx +49 -4
  287. package/app/apps/web/app/projects/projects-view.tsx +79 -68
  288. package/app/apps/web/app/runs/page.tsx +21 -0
  289. package/app/apps/web/app/sessions/[id]/page.tsx +12 -1
  290. package/app/apps/web/app/sessions/[id]/session-view.tsx +569 -21
  291. package/app/apps/web/app/sessions/page.tsx +19 -3
  292. package/app/apps/web/app/sessions/sessions-grid.tsx +172 -131
  293. package/app/apps/web/app/settings/ai-features-form.tsx +45 -0
  294. package/app/apps/web/app/settings/page.tsx +20 -0
  295. package/app/apps/web/app/usage/page.tsx +45 -3
  296. package/app/apps/web/components/auto-generate-yesterday.tsx +96 -0
  297. package/app/apps/web/components/backfill-drawer.tsx +309 -0
  298. package/app/apps/web/components/dashboard-view.tsx +4 -2
  299. package/app/apps/web/components/date-nav.tsx +385 -0
  300. package/app/apps/web/components/day-digest-view.tsx +180 -0
  301. package/app/apps/web/components/day-digest.tsx +489 -0
  302. package/app/apps/web/components/flag-chip.tsx +54 -0
  303. package/app/apps/web/components/goal-bar.tsx +63 -0
  304. package/app/apps/web/components/heatmap.tsx +78 -1
  305. package/app/apps/web/components/helpfulness-sparkline.tsx +82 -0
  306. package/app/apps/web/components/insights-history.tsx +211 -0
  307. package/app/apps/web/components/insights-top-bar.tsx +216 -0
  308. package/app/apps/web/components/job-queue-widget.tsx +274 -0
  309. package/app/apps/web/components/live-sessions-widget.tsx +38 -3
  310. package/app/apps/web/components/month-digest-view.tsx +138 -0
  311. package/app/apps/web/components/month-digest.tsx +216 -0
  312. package/app/apps/web/components/outcome-mix-row.tsx +37 -0
  313. package/app/apps/web/components/outcome-pill.tsx +101 -0
  314. package/app/apps/web/components/previous-cycles-trend.tsx +155 -0
  315. package/app/apps/web/components/recent-days-panel.tsx +83 -0
  316. package/app/apps/web/components/runs-live-board.tsx +513 -0
  317. package/app/apps/web/components/sidebar.tsx +38 -4
  318. package/app/apps/web/components/usage-chart-range.tsx +58 -6
  319. package/app/apps/web/components/usage-chart.tsx +94 -7
  320. package/app/apps/web/components/usage-charts-dashboard.tsx +20 -1
  321. package/app/apps/web/components/week-digest-view.tsx +226 -0
  322. package/app/apps/web/components/week-digest.tsx +2128 -0
  323. package/app/apps/web/components/yesterday-hero.tsx +110 -0
  324. package/app/apps/web/lib/auto-week-fire.ts +37 -0
  325. package/app/apps/web/lib/calibration-data.ts +171 -0
  326. package/app/apps/web/lib/entries-index.ts +174 -0
  327. package/app/apps/web/lib/entries.ts +72 -0
  328. package/app/apps/web/lib/inflight-coalesce.test.ts +43 -0
  329. package/app/apps/web/lib/inflight-coalesce.ts +19 -0
  330. package/app/apps/web/lib/jobs.ts +170 -0
  331. package/app/apps/web/lib/usage-data.ts +37 -0
  332. package/app/apps/web/lib/validate-settings.test.ts +25 -0
  333. package/app/apps/web/lib/validate-settings.ts +9 -0
  334. package/app/apps/web/next.config.ts +1 -1
  335. package/app/apps/web/package.json +4 -2
  336. package/app/apps/web/server.js +1 -1
  337. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  338. package/app/apps/web/vitest.config.ts +9 -0
  339. package/dist/daemon-worker.js +1758 -713
  340. package/dist/index.js +11308 -1888
  341. package/package.json +4 -1
  342. package/app/apps/web/.next/server/app/api/insights/months-index/route/app-paths-manifest.json +0 -3
  343. package/app/apps/web/.next/server/app/api/insights/months-index/route.js +0 -7
  344. package/app/apps/web/.next/server/app/api/insights/months-index/route_client-reference-manifest.js +0 -3
  345. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/app-paths-manifest.json +0 -3
  346. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js +0 -6
  347. package/app/apps/web/.next/server/app/api/insights/route/app-paths-manifest.json +0 -3
  348. package/app/apps/web/.next/server/app/api/insights/route.js +0 -8
  349. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/app-paths-manifest.json +0 -3
  350. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js +0 -6
  351. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route_client-reference-manifest.js +0 -3
  352. package/app/apps/web/.next/server/app/api/insights/saved/route/app-paths-manifest.json +0 -3
  353. package/app/apps/web/.next/server/app/api/insights/saved/route.js +0 -6
  354. package/app/apps/web/.next/server/app/api/insights/saved/route_client-reference-manifest.js +0 -3
  355. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/app-paths-manifest.json +0 -3
  356. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js +0 -7
  357. package/app/apps/web/.next/server/app/api/insights/weeks-index/route_client-reference-manifest.js +0 -3
  358. package/app/apps/web/.next/server/app/insights/print/[key]/page/app-paths-manifest.json +0 -3
  359. package/app/apps/web/.next/server/app/insights/print/[key]/page.js.nft.json +0 -1
  360. package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +0 -3
  361. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_months-index_route_actions_0~h~zci.js +0 -3
  362. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_saved_[key]_route_actions_0a5j9yw.js +0 -3
  363. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_weeks-index_route_actions_0hh-1c7.js +0 -3
  364. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.hmj0z._.js +0 -3
  365. package/app/apps/web/.next/server/chunks/[root-of-the-server]__01h_g-p._.js +0 -3
  366. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh-z10._.js +0 -3
  367. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ri7q~r._.js +0 -3
  368. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0rr1~~x._.js +0 -3
  369. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s0gv.0._.js +0 -3
  370. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_pdf_[key]_route_actions_0lu-lnx.js +0 -3
  371. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_route_actions_0h8l5lz.js +0 -3
  372. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_saved_route_actions_13rlblw.js +0 -3
  373. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_00t2xj6.js +0 -68
  374. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0.tsxpj._.js +0 -3
  375. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +0 -3
  376. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08pen0k._.js +0 -3
  377. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b323~k._.js +0 -8
  378. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gx-4qa._.js +0 -3
  379. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wv27wt._.js +0 -3
  380. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +0 -3
  381. package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +0 -8
  382. package/app/apps/web/.next/server/chunks/ssr/_0r~__0v._.js +0 -12
  383. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_print_[key]_page_actions_0a853fw.js +0 -3
  384. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_parallelism_08vub.0._.js +0 -3
  385. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +0 -8
  386. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0bbo_pd._.js +0 -8
  387. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insight-report_tsx_13q2a-c._.js +0 -12
  388. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insights-view_tsx_11730ta._.js +0 -6
  389. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0nqz8dw._.js +0 -3
  390. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js +0 -3
  391. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +0 -3
  392. package/app/apps/web/.next/static/chunks/028_pmk40b69_.js +0 -6
  393. package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +0 -6
  394. package/app/apps/web/.next/static/chunks/03_7xtpi9sv4g.js +0 -4
  395. package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +0 -6
  396. package/app/apps/web/.next/static/chunks/063oimt30jeo9.js +0 -10
  397. package/app/apps/web/.next/static/chunks/08c1pcrocqq5s.js +0 -6
  398. package/app/apps/web/.next/static/chunks/0e-wrhqh4h3r5.js +0 -2
  399. package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +0 -1
  400. package/app/apps/web/.next/static/chunks/0i7er7_bfab36.js +0 -10
  401. package/app/apps/web/.next/static/chunks/0ipytyi01h0ta.js +0 -1
  402. package/app/apps/web/.next/static/chunks/0lsznd4_avzhv.js +0 -6
  403. package/app/apps/web/.next/static/chunks/0lty3v_i9mde7.js +0 -1
  404. package/app/apps/web/.next/static/chunks/0vekjjj4pidlz.css +0 -1
  405. package/app/apps/web/.next/static/chunks/137uw2-qbro07.js +0 -31
  406. package/app/apps/web/app/api/insights/months-index/route.ts +0 -80
  407. package/app/apps/web/app/api/insights/pdf/[key]/route.ts +0 -45
  408. package/app/apps/web/app/api/insights/route.ts +0 -508
  409. package/app/apps/web/app/api/insights/saved/[key]/route.ts +0 -11
  410. package/app/apps/web/app/api/insights/saved/route.ts +0 -9
  411. package/app/apps/web/app/insights/print/[key]/page.tsx +0 -73
  412. package/app/apps/web/components/insight-report.tsx +0 -1031
  413. package/app/apps/web/components/insights-view.tsx +0 -647
  414. package/app/apps/web/lib/ai/insights-prompt.ts +0 -71
  415. package/app/apps/web/lib/ai/saved-reports.ts +0 -83
  416. /package/app/apps/web/.next/server/app/api/{insights/months-index → digest/day/[date]}/route/build-manifest.json +0 -0
  417. /package/app/apps/web/.next/server/app/api/{insights/months-index → digest/day/[date]}/route/server-reference-manifest.json +0 -0
  418. /package/app/apps/web/.next/server/app/api/{insights/months-index → digest/day/[date]}/route.js.map +0 -0
  419. /package/app/apps/web/.next/server/app/api/{insights → digest/month/[yearMonth]}/route/build-manifest.json +0 -0
  420. /package/app/apps/web/.next/server/app/api/{insights → digest/month/[yearMonth]}/route/server-reference-manifest.json +0 -0
  421. /package/app/apps/web/.next/server/app/api/{insights → digest/month/[yearMonth]}/route.js.map +0 -0
  422. /package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route/build-manifest.json +0 -0
  423. /package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route/server-reference-manifest.json +0 -0
  424. /package/app/apps/web/.next/server/app/api/{insights/pdf/[key] → digest/month-index}/route.js.map +0 -0
  425. /package/app/apps/web/.next/server/app/api/{insights/saved → digest/week/[startDate]}/route/build-manifest.json +0 -0
  426. /package/app/apps/web/.next/server/app/api/{insights/saved → digest/week/[startDate]}/route/server-reference-manifest.json +0 -0
  427. /package/app/apps/web/.next/server/app/api/{insights/saved → digest/week/[startDate]}/route.js.map +0 -0
  428. /package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/week-index}/route/build-manifest.json +0 -0
  429. /package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/week-index}/route/server-reference-manifest.json +0 -0
  430. /package/app/apps/web/.next/server/app/api/{insights/saved/[key] → digest/week-index}/route.js.map +0 -0
  431. /package/app/apps/web/.next/server/app/api/{insights/weeks-index → jobs}/route/build-manifest.json +0 -0
  432. /package/app/apps/web/.next/server/app/api/{insights/weeks-index → jobs}/route/server-reference-manifest.json +0 -0
  433. /package/app/apps/web/.next/server/app/api/{insights/weeks-index → jobs}/route.js.map +0 -0
  434. /package/app/apps/web/.next/server/app/{insights/print/[key]/page → api/runs/[runId]/route}/server-reference-manifest.json +0 -0
  435. /package/app/apps/web/.next/server/app/{insights/print/[key]/page.js.map → api/runs/[runId]/route.js.map} +0 -0
  436. /package/app/apps/web/.next/server/app/{insights/print/[key] → day/[date]}/page/next-font-manifest.json +0 -0
  437. /package/app/apps/web/.next/server/app/{insights/print/[key] → day/[date]}/page/react-loadable-manifest.json +0 -0
  438. /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 8tTWPvUhtatZEoocIhamh}/_buildManifest.js +0 -0
  439. /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 8tTWPvUhtatZEoocIhamh}/_clientMiddlewareManifest.js +0 -0
  440. /package/app/apps/web/.next/static/{G_4ofKI9v9ARC_lb9JILZ → 8tTWPvUhtatZEoocIhamh}/_ssgManifest.js +0 -0
@@ -0,0 +1,2128 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useState, type ReactNode } from "react";
5
+ import { Check, Copy } from "lucide-react";
6
+ import type { WeekDigest as WeekDigestType, DayHelpfulness, WeekTopSession, SessionPin } from "@claude-lens/entries";
7
+ import { renderWithFlagChips } from "./flag-chip";
8
+ import { GoalBar } from "./goal-bar";
9
+
10
+ const HELP_COLORS: Record<NonNullable<DayHelpfulness>, string> = {
11
+ essential: "#48bb78",
12
+ helpful: "#4299e1",
13
+ neutral: "#a0aec0",
14
+ unhelpful: "#f56565",
15
+ };
16
+
17
+ const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
18
+
19
+ const SHIPPED_COLLAPSE_THRESHOLD = 10;
20
+ const PROJECT_MIN_SHARE = 5;
21
+
22
+ const SHAPE_LABELS: Record<string, string> = {
23
+ "spec-review-loop": "Spec-review loop",
24
+ "chunk-implementation": "Chunk implementation",
25
+ "research-then-build": "Research-then-build",
26
+ "reviewer-triad": "Reviewer triad",
27
+ "background-coordinated": "Background-coordinated",
28
+ "solo-continuation": "Solo continuation",
29
+ "solo-design": "Solo design",
30
+ "solo-build": "Solo build",
31
+ "mixed": "Mixed",
32
+ };
33
+
34
+ const SHAPE_COLORS: Record<string, string> = {
35
+ "spec-review-loop": "#9f7aea",
36
+ "chunk-implementation": "#4299e1",
37
+ "research-then-build": "#38b2ac",
38
+ "reviewer-triad": "#ed64a6",
39
+ "background-coordinated": "#ecc94b",
40
+ "solo-continuation": "#a0aec0",
41
+ "solo-design": "#48bb78",
42
+ "solo-build": "#ed8936",
43
+ "mixed": "#a0aec0",
44
+ };
45
+
46
+ const SHAPE_DESCRIPTIONS: Record<string, string> = {
47
+ "spec-review-loop": "Write a spec → dispatch reviewer subagents → revise → re-review → implement.",
48
+ "chunk-implementation": "Numbered Chunk/Task subagent dispatches with paired reviewers per chunk.",
49
+ "research-then-build": "Explore/researcher subagents map terrain upfront, then build with a clear picture.",
50
+ "reviewer-triad": "Three reviewer dispatches with distinct lenses (reuse / quality / efficiency) on the same diff.",
51
+ "background-coordinated": "A background subagent runs in parallel with the foreground session.",
52
+ "solo-continuation": "No subagents; first input was \"continue\" picking up from a prior session.",
53
+ "solo-design": "No subagents; loaded the brainstorming/writing-plans skill — design or planning session.",
54
+ "solo-build": "No subagents; direct hands-on work.",
55
+ };
56
+
57
+ const SURPRISE_LABELS: Record<string, string> = {
58
+ "outlier": "Outlier",
59
+ "novel-use": "Novel use",
60
+ "user-built-tool": "User-built tool",
61
+ "cross-week-contrast": "Cross-week contrast",
62
+ };
63
+
64
+ const LEAN_KIND_LABELS: Record<string, string> = {
65
+ "claude-md": "CLAUDE.md addition",
66
+ "skill": "Skill to build",
67
+ "hook": "Hook to wire",
68
+ "harness": "Harness extension",
69
+ "decision": "Decision to make",
70
+ };
71
+
72
+ const FRAME_LABELS: Record<string, string> = {
73
+ "teammate": "<teammate-message>",
74
+ "task-notification": "<task-notification>",
75
+ "local-command-caveat": "<local-command-caveat>",
76
+ "slash-command": "Slash command (<command-name>)",
77
+ "image-attached": "[Image #N]",
78
+ "handoff-prose": "Handoff prose",
79
+ };
80
+
81
+ const FRAME_HELP: Record<string, string> = {
82
+ "teammate": "From Claude's agent-teams feature — coordinator dispatching to a teammate role.",
83
+ "task-notification": "From Claude's Monitor tool — the user's auto-monitor triggers a session.",
84
+ "local-command-caveat": "Claude convention wrapping local-command output to prevent prompt-injection.",
85
+ "slash-command": "A custom or stock slash-command invoked by the user.",
86
+ "image-attached": "User opened with one or more screenshots.",
87
+ "handoff-prose": "Personal habit: cross-session compaction prompt copied from a prior session.",
88
+ };
89
+
90
+ export function WeekDigest({
91
+ digest, aiEnabled, actions, priorDigest,
92
+ }: {
93
+ digest: WeekDigestType;
94
+ aiEnabled: boolean;
95
+ actions?: ReactNode;
96
+ priorDigest?: WeekDigestType | null;
97
+ }) {
98
+ const totalShipped = digest.shipped.length;
99
+ const dayCount = Object.values(digest.outcome_mix).reduce((a, b) => a + (b ?? 0), 0);
100
+ const hrs = digest.agent_min_total / 60;
101
+ const timeStr = hrs >= 1 ? `${hrs.toFixed(1)}h` : `${Math.round(digest.agent_min_total)}m`;
102
+ const delta = priorDigest ? buildDelta(digest, priorDigest) : null;
103
+
104
+ return (
105
+ <div style={{ maxWidth: 980, margin: "0 auto", padding: "24px 40px" }}>
106
+ <header style={{ marginBottom: 24 }}>
107
+ <div style={{
108
+ display: "flex", alignItems: "center", gap: 10, marginBottom: 8,
109
+ fontSize: 12, color: "var(--af-text-tertiary)", fontWeight: 500,
110
+ flexWrap: "wrap",
111
+ }}>
112
+ <span style={{ fontFamily: "var(--font-mono)", color: "var(--af-text-secondary)" }}>
113
+ {timeStr} agent time · {dayCount} active day{dayCount === 1 ? "" : "s"} · {totalShipped} PR{totalShipped === 1 ? "" : "s"}
114
+ </span>
115
+ {delta && (
116
+ <span style={{
117
+ fontFamily: "var(--font-mono)", fontSize: 11,
118
+ color: "var(--af-text-tertiary)",
119
+ padding: "1px 7px", borderRadius: 999,
120
+ border: "1px solid var(--af-border-subtle)",
121
+ }} title="vs the prior calendar week">
122
+ vs last week: {delta}
123
+ </span>
124
+ )}
125
+ {actions && (
126
+ <div style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 8 }}>
127
+ {actions}
128
+ </div>
129
+ )}
130
+ </div>
131
+ {digest.headline ? (
132
+ <h1 style={{
133
+ fontSize: 26, fontWeight: 700, lineHeight: 1.3, letterSpacing: "-0.02em",
134
+ margin: "0 0 6px", maxWidth: 820, color: "var(--af-text)",
135
+ }}>
136
+ {renderWithFlagChips(digest.headline)}
137
+ </h1>
138
+ ) : (
139
+ <h1 style={{ fontSize: 20, color: "var(--af-text-secondary)", margin: "0 0 14px" }}>
140
+ {aiEnabled
141
+ ? "No narrative yet."
142
+ : `Worked ${timeStr} across ${digest.projects.length} project${digest.projects.length === 1 ? "" : "s"}.`}
143
+ </h1>
144
+ )}
145
+ {digest.key_pattern && (
146
+ <p style={{
147
+ fontSize: 13, fontStyle: "italic", margin: 0, lineHeight: 1.5,
148
+ color: "var(--af-text-secondary)", maxWidth: 820,
149
+ }}>
150
+ {renderWithFlagChips(digest.key_pattern)}
151
+ </p>
152
+ )}
153
+ </header>
154
+
155
+ {!aiEnabled && (
156
+ <div style={{
157
+ padding: 14, marginBottom: 24, background: "var(--af-accent-subtle)",
158
+ borderRadius: 8, fontSize: 13, color: "var(--af-text)",
159
+ }}>
160
+ Enable AI features in <Link href="/settings" style={{ color: "var(--af-accent)" }}>Settings</Link> to see weekly narratives.
161
+ </div>
162
+ )}
163
+
164
+ {/* ── §1: This week at a glance — totals, time distribution, goal mix ── */}
165
+ <WeekStatsStrip digest={digest} />
166
+
167
+ <DaysActiveBars digest={digest} />
168
+
169
+ {digest.top_goal_categories.length > 0 && (
170
+ <Section title="Goal mix" anchor="goals">
171
+ <GoalBar goals={digest.top_goal_categories} total={digest.agent_min_total} />
172
+ </Section>
173
+ )}
174
+
175
+ {/* ── §2: Through the week — trajectory + standout days + project areas ── */}
176
+ <ThroughTheWeek digest={digest} />
177
+
178
+ {/* ── §3: Top 3 sessions — per-session deep-dive ── */}
179
+ {digest.top_sessions && digest.top_sessions.length > 0 && (
180
+ <TopSessionsSection sessions={digest.top_sessions} />
181
+ )}
182
+
183
+ {/* ── §4: Findings narrative — what worked / stalled / surprised / lean ── */}
184
+ <FindingsSection
185
+ title="What worked"
186
+ anchor="what-worked"
187
+ items={digest.what_worked ?? []}
188
+ tone="#48bb78"
189
+ sectionFallbackProse={null}
190
+ />
191
+
192
+ <FindingsSection
193
+ title="What stalled"
194
+ anchor="what-stalled"
195
+ items={digest.what_stalled ?? []}
196
+ tone="#ed8936"
197
+ sectionFallbackProse="No stalls stuck this week."
198
+ />
199
+
200
+ <SurprisesSection items={digest.what_surprised ?? []} />
201
+
202
+ <WhereToLeanSection items={digest.where_to_lean ?? []} />
203
+
204
+ {/* ── Bottom: shipped list (kept) + legacy fallback ──
205
+ Removed PatternRollupsFold (working_shapes + interaction_grammar
206
+ UI) and ByTheNumbersFold (interaction_modes). They were collapsed
207
+ by default and rarely expanded; the LLM's findings already cite
208
+ shape/grammar names by anchor, so the rollup tables were
209
+ redundant context. Their feeder data is still on the digest for
210
+ back-compat — only the rendering is gone. */}
211
+ <ShippedList shipped={digest.shipped} />
212
+
213
+ {!digest.working_shapes && (
214
+ <LegacyNarrativeFallback digest={digest} />
215
+ )}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function LegacyNarrativeFallback({ digest }: { digest: WeekDigestType }) {
221
+ const themes = digest.recurring_themes ?? [];
222
+ const correlations = digest.outcome_correlations ?? [];
223
+ const friction = digest.friction_categories ?? [];
224
+ const suggestions = digest.suggestions;
225
+ const horizon = digest.on_the_horizon;
226
+ const fun = digest.fun_ending;
227
+ const hasAny = themes.length > 0 || correlations.length > 0 || friction.length > 0
228
+ || (suggestions?.claude_md_additions?.length ?? 0) > 0
229
+ || horizon || fun;
230
+ if (!hasAny) return null;
231
+ return (
232
+ <div style={{
233
+ padding: "14px 16px", marginTop: 18, borderRadius: 8,
234
+ background: "color-mix(in srgb, var(--af-text-tertiary) 5%, var(--af-surface))",
235
+ border: "1px dashed var(--af-border-subtle)",
236
+ fontSize: 12, color: "var(--af-text-secondary)", lineHeight: 1.55,
237
+ }}>
238
+ <strong style={{ color: "var(--af-text)" }}>Legacy digest:</strong> this week was generated under a previous prompt.
239
+ Re-roll the digest to refresh it under the new working-shapes narrative.
240
+ </div>
241
+ );
242
+ }
243
+
244
+ // ─── Sub-components ──────────────────────────────────────────────────────
245
+
246
+ type StatCard = {
247
+ label: string;
248
+ primary: string;
249
+ detail: string;
250
+ href?: string;
251
+ title?: string;
252
+ visual?: ReactNode;
253
+ };
254
+
255
+ function WeekStatsStrip({ digest }: { digest: WeekDigestType }) {
256
+ const totalHrs = digest.agent_min_total / 60;
257
+ const totalStr = totalHrs >= 1 ? `${totalHrs.toFixed(1)}h` : `${Math.round(digest.agent_min_total)}m`;
258
+ const peakHourBucket = peakHour(digest.hours_distribution);
259
+
260
+ // Per-day agent_min for the mini-bar in Agent time / Busiest day cards
261
+ const dayMap = new Map(digest.days_active.map(d => [d.date, d]));
262
+ const weekDates = weekDateLabels(digest.window.start);
263
+ const perDayAgent = weekDates.map(d => dayMap.get(d)?.agent_min ?? 0);
264
+ const maxAgent = Math.max(...perDayAgent, 1);
265
+
266
+ // Busiest day index for highlight
267
+ const busiestIdx = digest.busiest_day
268
+ ? weekDates.indexOf(digest.busiest_day.date)
269
+ : -1;
270
+
271
+ // Longest run vs busiest day's total (for the bar visual)
272
+ const longestRunRatio = digest.longest_run && digest.busiest_day
273
+ ? Math.min(1, digest.longest_run.active_min / Math.max(1, digest.busiest_day.agent_min))
274
+ : 0;
275
+
276
+ const cards: StatCard[] = [];
277
+
278
+ cards.push({
279
+ label: "Agent time",
280
+ primary: totalStr,
281
+ detail: `across ${digest.days_active.length} day${digest.days_active.length === 1 ? "" : "s"}`,
282
+ visual: <MiniDayBars values={perDayAgent} max={maxAgent} highlight={-1} />,
283
+ });
284
+
285
+ if (digest.busiest_day) {
286
+ cards.push({
287
+ label: "Busiest day",
288
+ href: `/digest/${digest.busiest_day.date}`,
289
+ primary: `${dayName(digest.busiest_day.date)} ${digest.busiest_day.date.slice(5)}`,
290
+ detail: `${Math.round(digest.busiest_day.agent_min)}m · ${digest.busiest_day.shipped_count} PR${digest.busiest_day.shipped_count === 1 ? "" : "s"}`,
291
+ visual: <MiniDayBars values={perDayAgent} max={maxAgent} highlight={busiestIdx} />,
292
+ });
293
+ }
294
+
295
+ if (digest.longest_run) {
296
+ cards.push({
297
+ label: "Longest single run",
298
+ href: `/sessions/${digest.longest_run.session_id}`,
299
+ title: `Session ${digest.longest_run.session_id.slice(0, 8)}`,
300
+ primary: `${Math.round(digest.longest_run.active_min)}m`,
301
+ detail: `${digest.longest_run.project_display} · ${dayName(digest.longest_run.date)} ${digest.longest_run.date.slice(5)}`,
302
+ visual: <MiniRatioBar ratio={longestRunRatio} caption="vs busiest day" />,
303
+ });
304
+ }
305
+
306
+ if (peakHourBucket) {
307
+ cards.push({
308
+ label: "Peak hours",
309
+ title: `${formatHourRange(peakHourBucket.start, peakHourBucket.end)} carried ${peakHourBucket.share_pct.toFixed(0)}% of the week's agent time`,
310
+ primary: formatHourRange(peakHourBucket.start, peakHourBucket.end),
311
+ detail: `${peakHourBucket.share_pct.toFixed(0)}% of agent time`,
312
+ visual: <MiniHoursSparkline hours={digest.hours_distribution} peakStart={peakHourBucket.start} peakEnd={peakHourBucket.end} />,
313
+ });
314
+ }
315
+
316
+ if (cards.length === 0) return null;
317
+
318
+ return (
319
+ <section style={{
320
+ display: "grid",
321
+ gridTemplateColumns: `repeat(${Math.min(cards.length, 4)}, minmax(0, 1fr))`,
322
+ gap: 10,
323
+ marginBottom: 20,
324
+ }}>
325
+ {cards.map((s, i) => {
326
+ const inner = (
327
+ <div style={{
328
+ padding: "12px 14px", borderRadius: 10,
329
+ background: "var(--af-surface)",
330
+ border: "1px solid var(--af-border-subtle)",
331
+ height: "100%",
332
+ display: "flex", flexDirection: "column", justifyContent: "space-between",
333
+ minHeight: 90,
334
+ }} title={s.title}>
335
+ <div>
336
+ <div style={{
337
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
338
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
339
+ marginBottom: 6,
340
+ }}>{s.label}</div>
341
+ <div style={{
342
+ fontSize: 17, fontWeight: 600, color: "var(--af-text)",
343
+ letterSpacing: "-0.01em", lineHeight: 1.2,
344
+ }}>{s.primary}</div>
345
+ <div style={{
346
+ fontSize: 10.5, color: "var(--af-text-tertiary)",
347
+ fontFamily: "var(--font-mono)", marginTop: 3,
348
+ }}>{s.detail}</div>
349
+ </div>
350
+ {s.visual && <div style={{ marginTop: 10 }}>{s.visual}</div>}
351
+ </div>
352
+ );
353
+ return s.href ? (
354
+ <Link key={i} href={s.href} style={{ textDecoration: "none" }}>{inner}</Link>
355
+ ) : (
356
+ <div key={i}>{inner}</div>
357
+ );
358
+ })}
359
+ </section>
360
+ );
361
+ }
362
+
363
+ /** 7-day mini-bar — shows daily agent_min distribution. When highlight≥0
364
+ * that day glows in accent; others stay subtle. */
365
+ function MiniDayBars({ values, max, highlight }: { values: number[]; max: number; highlight: number }) {
366
+ return (
367
+ <div style={{ display: "flex", gap: 2, alignItems: "flex-end", height: 18 }}>
368
+ {values.map((v, i) => {
369
+ const h = max > 0 ? Math.max(1, (v / max) * 18) : 1;
370
+ const isHi = i === highlight;
371
+ return (
372
+ <div key={i} style={{
373
+ flex: 1,
374
+ height: h,
375
+ borderRadius: 1,
376
+ background: isHi
377
+ ? "var(--af-accent)"
378
+ : v > 0
379
+ ? "color-mix(in srgb, var(--af-accent) 30%, transparent)"
380
+ : "var(--af-border-subtle)",
381
+ }} />
382
+ );
383
+ })}
384
+ </div>
385
+ );
386
+ }
387
+
388
+ /** Single horizontal bar showing a 0..1 ratio with a caption underneath. */
389
+ function MiniRatioBar({ ratio, caption }: { ratio: number; caption: string }) {
390
+ const pct = Math.max(0, Math.min(1, ratio)) * 100;
391
+ return (
392
+ <div>
393
+ <div style={{
394
+ height: 6, borderRadius: 99, overflow: "hidden",
395
+ background: "var(--af-border-subtle)",
396
+ }}>
397
+ <div style={{
398
+ width: `${pct}%`, height: "100%",
399
+ background: "var(--af-accent)",
400
+ }} />
401
+ </div>
402
+ <div style={{
403
+ fontSize: 9, color: "var(--af-text-tertiary)",
404
+ fontFamily: "var(--font-mono)", marginTop: 3,
405
+ }}>{Math.round(pct)}% {caption}</div>
406
+ </div>
407
+ );
408
+ }
409
+
410
+ /** 24-bar sparkline of hours_distribution with the peak window glowing. */
411
+ function MiniHoursSparkline({ hours, peakStart, peakEnd }: { hours: number[]; peakStart: number; peakEnd: number }) {
412
+ const max = Math.max(...hours, 1);
413
+ return (
414
+ <div style={{ display: "flex", gap: 1, alignItems: "flex-end", height: 18 }}>
415
+ {hours.map((v, h) => {
416
+ const inPeak = h >= peakStart && h <= peakEnd;
417
+ const height = max > 0 ? Math.max(1, (v / max) * 18) : 1;
418
+ return (
419
+ <div key={h} style={{
420
+ flex: 1,
421
+ height,
422
+ borderRadius: 1,
423
+ background: inPeak
424
+ ? "var(--af-accent)"
425
+ : v > 0
426
+ ? "color-mix(in srgb, var(--af-accent) 22%, transparent)"
427
+ : "var(--af-border-subtle)",
428
+ }} />
429
+ );
430
+ })}
431
+ </div>
432
+ );
433
+ }
434
+
435
+ function DaysActiveBars({ digest }: { digest: WeekDigestType }) {
436
+ if (digest.days_active.length === 0) return null;
437
+ const maxMin = Math.max(...digest.days_active.map(d => d.agent_min));
438
+ const dayMap = new Map(digest.days_active.map(d => [d.date, d]));
439
+ const dates = weekDateLabels(digest.window.start);
440
+ return (
441
+ <section style={{
442
+ display: "flex", gap: 6, alignItems: "stretch",
443
+ padding: "12px 0", marginBottom: 24,
444
+ }}>
445
+ {dates.map((date, i) => {
446
+ const d = dayMap.get(date);
447
+ const mins = d?.agent_min ?? 0;
448
+ const heightPct = maxMin > 0 && mins > 0 ? Math.max(8, (mins / maxMin) * 100) : 0;
449
+ const tone = d ? OUTCOME_TONE[d.outcome_day] : "var(--af-text-tertiary)";
450
+ const helpTone = d?.helpfulness_day ? HELP_COLORS[d.helpfulness_day] : null;
451
+ return (
452
+ <Link
453
+ key={i}
454
+ href={d ? `/digest/${date}` : "#"}
455
+ style={{
456
+ flex: 1, display: "flex", flexDirection: "column", alignItems: "stretch", gap: 2,
457
+ textDecoration: "none",
458
+ pointerEvents: d ? "auto" : "none",
459
+ }}
460
+ title={d
461
+ ? `${date} · ${Math.round(mins)}m · ${d.outcome_day}${d.shipped_count > 0 ? ` · ${d.shipped_count} PR` : ""}${d.helpfulness_day ? ` · helpfulness ${d.helpfulness_day}` : ""}`
462
+ : "no activity"
463
+ }
464
+ >
465
+ <div style={{
466
+ height: 56, borderRadius: 4, position: "relative",
467
+ background: "var(--af-surface)",
468
+ border: `1px solid ${d ? `color-mix(in srgb, ${tone} 28%, var(--af-border-subtle))` : "var(--af-border-subtle)"}`,
469
+ overflow: "hidden",
470
+ }}>
471
+ {d && (
472
+ <div style={{
473
+ position: "absolute", bottom: 0, left: 0, right: 0,
474
+ height: `${heightPct}%`,
475
+ background: `color-mix(in srgb, ${tone} 28%, transparent)`,
476
+ }} />
477
+ )}
478
+ {d?.dominant_shape && SHAPE_COLORS[d.dominant_shape] && (
479
+ <div
480
+ title={`${SHAPE_LABELS[d.dominant_shape] ?? d.dominant_shape}`}
481
+ style={{
482
+ position: "absolute", top: 0, left: 0, right: 0,
483
+ height: 3,
484
+ background: SHAPE_COLORS[d.dominant_shape],
485
+ }}
486
+ />
487
+ )}
488
+ {helpTone && (
489
+ <div style={{
490
+ position: "absolute", top: 4, right: 4,
491
+ width: 6, height: 6, borderRadius: 999,
492
+ background: helpTone,
493
+ }} title={d?.helpfulness_day ?? ""} />
494
+ )}
495
+ </div>
496
+ <div style={{
497
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.06em",
498
+ color: "var(--af-text-tertiary)", textAlign: "center",
499
+ }}>
500
+ {DAY_LABELS[i]}
501
+ </div>
502
+ <div style={{
503
+ fontSize: 9, fontFamily: "var(--font-mono)",
504
+ color: d ? "var(--af-text-secondary)" : "var(--af-text-tertiary)",
505
+ textAlign: "center",
506
+ }}>
507
+ {d ? `${Math.round(mins)}m` : "—"}
508
+ </div>
509
+ </Link>
510
+ );
511
+ })}
512
+ </section>
513
+ );
514
+ }
515
+
516
+ function ByTheNumbersFold({ modes }: { modes: NonNullable<WeekDigestType["interaction_modes"]> }) {
517
+ const [open, setOpen] = useState(false);
518
+ return (
519
+ <section style={{ marginBottom: 28 }}>
520
+ <button
521
+ type="button"
522
+ onClick={() => setOpen(o => !o)}
523
+ style={{
524
+ display: "inline-flex", alignItems: "center", gap: 6,
525
+ background: "transparent", border: "none", cursor: "pointer",
526
+ padding: "6px 0", color: "var(--af-text-tertiary)",
527
+ fontSize: 11, fontWeight: 700, letterSpacing: "0.08em",
528
+ textTransform: "uppercase",
529
+ }}
530
+ >
531
+ {open ? "▾" : "▸"} By the numbers
532
+ </button>
533
+ {open && (
534
+ <div style={{
535
+ display: "grid",
536
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
537
+ gap: 8, marginTop: 8,
538
+ }}>
539
+ <NumberCard label="Orchestration" lines={[
540
+ `${modes.orchestration.subagent_calls} dispatches · ${modes.orchestration.days_with_subagents}/7 days`,
541
+ modes.orchestration.top_types.slice(0, 3).map(t => `${t.type} ×${t.count}`).join(" · "),
542
+ modes.orchestration.task_ops > 0 ? `${modes.orchestration.task_ops} TodoWrite ops` : "",
543
+ ].filter(Boolean)} />
544
+ <NumberCard label="Skill-driven" lines={[
545
+ `${modes.skill_use.skill_calls} loads · ${modes.skill_use.days_with_skills}/7 days`,
546
+ modes.skill_use.top_skills.slice(0, 3).map(s => `${s.skill} ×${s.count}`).join(" · "),
547
+ ].filter(Boolean)} />
548
+ <NumberCard label="Plan-gated" lines={[
549
+ `${modes.plan_gating.exit_plan_calls} ExitPlan · ${modes.plan_gating.days_with_plan}/7 days`,
550
+ ]} />
551
+ <NumberCard label="Turn shape" lines={[
552
+ `${modes.turn_shape.tools_per_turn.toFixed(1)} tools/turn · ${modes.turn_shape.label}`,
553
+ `${modes.turn_shape.long_autonomous_days} long-autonomous days · ${modes.turn_shape.interrupts} interrupts`,
554
+ ]} />
555
+ </div>
556
+ )}
557
+ </section>
558
+ );
559
+ }
560
+
561
+ function NumberCard({ label, lines }: { label: string; lines: string[] }) {
562
+ return (
563
+ <div style={{
564
+ padding: "10px 12px", borderRadius: 8,
565
+ background: "var(--af-surface)",
566
+ border: "1px solid var(--af-border-subtle)",
567
+ }}>
568
+ <div style={{
569
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
570
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
571
+ marginBottom: 4,
572
+ }}>{label}</div>
573
+ {lines.map((ln, i) => (
574
+ <div key={i} style={{
575
+ fontSize: 11, color: i === 0 ? "var(--af-text)" : "var(--af-text-secondary)",
576
+ fontFamily: "var(--font-mono)", lineHeight: 1.45,
577
+ }}>{ln}</div>
578
+ ))}
579
+ </div>
580
+ );
581
+ }
582
+
583
+ const OUTCOME_TONE: Record<string, string> = {
584
+ shipped: "#48bb78",
585
+ partial: "#4299e1",
586
+ blocked: "#f56565",
587
+ exploratory: "#a0aec0",
588
+ trivial: "#cbd5e0",
589
+ idle: "#cbd5e0",
590
+ };
591
+
592
+ /** Smallest window length we accept as a "peak"; below this and the window
593
+ * isn't carrying enough of the week's activity to be characteristic. */
594
+ const PEAK_HOUR_COVERAGE_THRESHOLD = 0.6;
595
+
596
+ function peakHour(hours: number[]): { start: number; end: number; share_pct: number } | null {
597
+ const total = hours.reduce((a, b) => a + b, 0);
598
+ if (total === 0) return null;
599
+ for (let len = 1; len <= 24; len++) {
600
+ let bestSum = 0, bestStart = 0;
601
+ for (let s = 0; s <= 24 - len; s++) {
602
+ let sum = 0;
603
+ for (let i = 0; i < len; i++) sum += hours[s + i] ?? 0;
604
+ if (sum > bestSum) { bestSum = sum; bestStart = s; }
605
+ }
606
+ if (bestSum / total >= PEAK_HOUR_COVERAGE_THRESHOLD) {
607
+ return { start: bestStart, end: bestStart + len, share_pct: (bestSum / total) * 100 };
608
+ }
609
+ }
610
+ return null;
611
+ }
612
+
613
+ function formatHourRange(start: number, end: number): string {
614
+ const fmt = (h: number) => {
615
+ const period = h < 12 || h === 24 ? "am" : "pm";
616
+ const mod = ((h % 12) || 12);
617
+ return `${mod}${period}`;
618
+ };
619
+ return `${fmt(start)}–${fmt(end)}`;
620
+ }
621
+
622
+ function weekDateLabels(startIso: string): string[] {
623
+ const out: string[] = [];
624
+ const start = new Date(startIso);
625
+ for (let i = 0; i < 7; i++) {
626
+ const d = new Date(start);
627
+ d.setDate(start.getDate() + i);
628
+ const y = d.getFullYear();
629
+ const m = String(d.getMonth() + 1).padStart(2, "0");
630
+ const day = String(d.getDate()).padStart(2, "0");
631
+ out.push(`${y}-${m}-${day}`);
632
+ }
633
+ return out;
634
+ }
635
+
636
+ function DayChips({ dates, tone }: { dates: string[]; tone: string }) {
637
+ return (
638
+ <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
639
+ {dates.map(d => (
640
+ <Link key={d} href={`/digest/${d}`} style={{
641
+ display: "inline-flex", alignItems: "center", gap: 4,
642
+ padding: "2px 8px", borderRadius: 999,
643
+ background: `color-mix(in srgb, ${tone} 12%, transparent)`,
644
+ border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
645
+ fontSize: 10, fontFamily: "var(--font-mono)",
646
+ color: tone, textDecoration: "none", fontWeight: 600,
647
+ }}>
648
+ {dayName(d)} {d.slice(5)}
649
+ </Link>
650
+ ))}
651
+ </div>
652
+ );
653
+ }
654
+
655
+ /** "Through the week" combined section — trajectory + standout days +
656
+ * project areas under one heading. Trajectory + standout share the top row
657
+ * in two columns (60/40); project areas spread below in a 2-col grid. */
658
+ function ThroughTheWeek({ digest }: { digest: WeekDigestType }) {
659
+ const hasTrajectory = digest.trajectory && digest.trajectory.length > 0;
660
+ const hasStandout = digest.standout_days && digest.standout_days.length > 0;
661
+ const visibleProjects = digest.projects.filter(p => p.share_pct >= PROJECT_MIN_SHARE || p.shipped_count > 0);
662
+ const hasProjects = visibleProjects.length > 0;
663
+ if (!hasTrajectory && !hasStandout && !hasProjects) return null;
664
+
665
+ return (
666
+ <Section title="Through the week" anchor="through-the-week">
667
+ {(hasTrajectory || hasStandout) && (
668
+ <div style={{
669
+ display: "grid",
670
+ gridTemplateColumns: hasTrajectory && hasStandout ? "minmax(0, 1.4fr) minmax(0, 1fr)" : "1fr",
671
+ gap: 28, marginBottom: hasProjects ? 22 : 0,
672
+ }}>
673
+ {hasTrajectory && (
674
+ <div>
675
+ <SubSectionLabel>Trajectory</SubSectionLabel>
676
+ <ol style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }}>
677
+ {digest.trajectory!.map(t => (
678
+ <li key={t.date} style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
679
+ <Link href={`/digest/${t.date}`} style={{
680
+ fontSize: 10, fontWeight: 600, color: "var(--af-text-tertiary)",
681
+ fontFamily: "var(--font-mono)", flexShrink: 0, width: 64, paddingTop: 3,
682
+ textDecoration: "none",
683
+ }}>
684
+ {dayName(t.date)} {t.date.slice(5)}
685
+ </Link>
686
+ <span style={{ fontSize: 12.5, lineHeight: 1.55, color: "var(--af-text-secondary)" }}>
687
+ {renderWithFlagChips(t.line)}
688
+ </span>
689
+ </li>
690
+ ))}
691
+ </ol>
692
+ </div>
693
+ )}
694
+ {hasStandout && (
695
+ <div>
696
+ <SubSectionLabel>Standout days</SubSectionLabel>
697
+ <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 8 }}>
698
+ {digest.standout_days!.map(s => (
699
+ <li key={s.date} style={{
700
+ padding: "10px 12px", borderRadius: 8, background: "var(--af-surface)",
701
+ border: "1px solid var(--af-border-subtle)",
702
+ }}>
703
+ <Link href={`/digest/${s.date}`} style={{
704
+ fontSize: 11, fontWeight: 600,
705
+ color: "var(--af-accent)", textDecoration: "none",
706
+ fontFamily: "var(--font-mono)", marginRight: 8,
707
+ }}>
708
+ {dayName(s.date)} {s.date.slice(5)}
709
+ </Link>
710
+ <span style={{ fontSize: 12.5, lineHeight: 1.55, color: "var(--af-text)" }}>
711
+ {renderWithFlagChips(s.why)}
712
+ </span>
713
+ </li>
714
+ ))}
715
+ </ul>
716
+ </div>
717
+ )}
718
+ </div>
719
+ )}
720
+ {hasProjects && (
721
+ <div>
722
+ <SubSectionLabel>Project areas</SubSectionLabel>
723
+ <ul style={{
724
+ listStyle: "none", padding: 0, margin: 0,
725
+ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
726
+ gap: 10,
727
+ }}>
728
+ {visibleProjects.map(p => (
729
+ <li key={p.name} style={{
730
+ padding: "11px 13px", borderRadius: 8,
731
+ background: "var(--af-surface)",
732
+ border: "1px solid var(--af-border-subtle)",
733
+ display: "flex", flexDirection: "column",
734
+ }}>
735
+ <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: p.description ? 6 : 0 }}>
736
+ <Link
737
+ href={`/projects/${encodeURIComponent(p.name)}`}
738
+ style={{ fontSize: 13, fontWeight: 600, color: "var(--af-text)", textDecoration: "none", flex: 1, minWidth: 0, letterSpacing: "-0.01em" }}
739
+ >
740
+ {p.display_name}
741
+ </Link>
742
+ <span style={{
743
+ fontFamily: "var(--font-mono)", fontSize: 10,
744
+ color: "var(--af-text-tertiary)",
745
+ }}>
746
+ {Math.round(p.agent_min)}m · {p.share_pct.toFixed(0)}% · {p.shipped_count} PR
747
+ </span>
748
+ </div>
749
+ {p.description && (
750
+ <p style={{ fontSize: 12, lineHeight: 1.5, margin: 0, color: "var(--af-text-secondary)" }}>
751
+ {p.description}
752
+ </p>
753
+ )}
754
+ </li>
755
+ ))}
756
+ </ul>
757
+ </div>
758
+ )}
759
+ </Section>
760
+ );
761
+ }
762
+
763
+ function SubSectionLabel({ children }: { children: ReactNode }) {
764
+ return (
765
+ <div style={{
766
+ fontSize: 10, color: "var(--af-text-tertiary)", textTransform: "uppercase",
767
+ letterSpacing: "0.08em", fontWeight: 700, marginBottom: 10,
768
+ }}>
769
+ {children}
770
+ </div>
771
+ );
772
+ }
773
+
774
+ function CopyBlock({ label, payload }: { label: string; payload: string }) {
775
+ const [copied, setCopied] = useState(false);
776
+ return (
777
+ <div style={{ position: "relative" }}>
778
+ <pre style={{
779
+ margin: 0,
780
+ padding: "10px 12px",
781
+ paddingRight: 96,
782
+ borderRadius: 6,
783
+ background: "var(--af-surface-raised)",
784
+ border: "1px solid var(--af-border-subtle)",
785
+ fontSize: 11,
786
+ fontFamily: "var(--font-mono)",
787
+ color: "var(--af-text)",
788
+ whiteSpace: "pre-wrap",
789
+ wordBreak: "break-word",
790
+ lineHeight: 1.55,
791
+ overflowX: "auto",
792
+ maxHeight: 280,
793
+ overflowY: "auto",
794
+ }}>{payload}</pre>
795
+ <button
796
+ type="button"
797
+ onClick={async () => {
798
+ try {
799
+ await navigator.clipboard.writeText(payload);
800
+ setCopied(true);
801
+ setTimeout(() => setCopied(false), 1500);
802
+ } catch {
803
+ // best-effort
804
+ }
805
+ }}
806
+ style={{
807
+ position: "absolute", top: 8, right: 8,
808
+ display: "inline-flex", alignItems: "center", gap: 4,
809
+ padding: "4px 8px", borderRadius: 5,
810
+ background: copied ? "color-mix(in srgb, #48bb78 18%, var(--af-surface))" : "var(--af-surface)",
811
+ border: `1px solid ${copied ? "#48bb78" : "var(--af-border-subtle)"}`,
812
+ color: copied ? "#48bb78" : "var(--af-text-secondary)",
813
+ fontSize: 10, fontWeight: 600, cursor: "pointer",
814
+ }}
815
+ >
816
+ {copied ? <><Check size={10} /> Copied</> : <><Copy size={10} /> {label}</>}
817
+ </button>
818
+ </div>
819
+ );
820
+ }
821
+
822
+ function ShippedList({ shipped }: { shipped: WeekDigestType["shipped"] }) {
823
+ const [expanded, setExpanded] = useState(false);
824
+ if (shipped.length === 0) return null;
825
+ const collapsible = shipped.length >= SHIPPED_COLLAPSE_THRESHOLD;
826
+ const visible = collapsible && !expanded ? shipped.slice(0, 5) : shipped;
827
+ const hidden = shipped.length - visible.length;
828
+ return (
829
+ <Section title={`Shipped (${shipped.length})`} anchor="shipped">
830
+ <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 4 }}>
831
+ {visible.map((s, i) => (
832
+ <li key={i} style={{
833
+ display: "flex", gap: 10, alignItems: "baseline",
834
+ padding: "6px 10px", borderRadius: 6,
835
+ fontSize: 12, color: "var(--af-text)",
836
+ }}>
837
+ <Link href={`/digest/${s.date}`} style={{
838
+ fontFamily: "var(--font-mono)", fontSize: 10,
839
+ color: "var(--af-text-tertiary)", flexShrink: 0, width: 56,
840
+ textDecoration: "none",
841
+ }}>
842
+ {dayName(s.date)} {s.date.slice(5)}
843
+ </Link>
844
+ <span style={{ flex: 1, minWidth: 0 }}>{s.title}</span>
845
+ <span style={{
846
+ fontSize: 10, color: "var(--af-text-tertiary)",
847
+ fontFamily: "var(--font-mono)",
848
+ }}>
849
+ {s.project}
850
+ </span>
851
+ </li>
852
+ ))}
853
+ </ul>
854
+ {collapsible && hidden > 0 && (
855
+ <button
856
+ type="button"
857
+ onClick={() => setExpanded(true)}
858
+ style={{
859
+ marginTop: 6, padding: "4px 10px", borderRadius: 5,
860
+ background: "transparent", border: "1px solid var(--af-border-subtle)",
861
+ fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
862
+ fontFamily: "var(--font-mono)",
863
+ }}
864
+ >
865
+ +{hidden} more
866
+ </button>
867
+ )}
868
+ {collapsible && expanded && (
869
+ <button
870
+ type="button"
871
+ onClick={() => setExpanded(false)}
872
+ style={{
873
+ marginTop: 6, padding: "4px 10px", borderRadius: 5,
874
+ background: "transparent", border: "1px solid var(--af-border-subtle)",
875
+ fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
876
+ fontFamily: "var(--font-mono)",
877
+ }}
878
+ >
879
+ show less
880
+ </button>
881
+ )}
882
+ </Section>
883
+ );
884
+ }
885
+
886
+ function buildDelta(curr: WeekDigestType, prior: WeekDigestType): string {
887
+ const prDelta = curr.shipped.length - prior.shipped.length;
888
+ const minDelta = curr.agent_min_total - prior.agent_min_total;
889
+ const hrDelta = minDelta / 60;
890
+ const prStr = `${prDelta > 0 ? "+" : ""}${prDelta} PR${Math.abs(prDelta) === 1 ? "" : "s"}`;
891
+ const timeStr = Math.abs(hrDelta) >= 1
892
+ ? `${hrDelta > 0 ? "+" : ""}${hrDelta.toFixed(1)}h`
893
+ : `${minDelta > 0 ? "+" : ""}${Math.round(minDelta)}m`;
894
+ return `${prStr} · ${timeStr}`;
895
+ }
896
+
897
+ // ─── New shape-anchored sections ─────────────────────────────────────────
898
+
899
+ function WorkingShapesSection({
900
+ shapes,
901
+ }: {
902
+ shapes: NonNullable<WeekDigestType["working_shapes"]>;
903
+ }) {
904
+ return (
905
+ <Section title="How you worked" anchor="how-you-worked">
906
+ <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
907
+ {shapes.map((row, i) => {
908
+ const label = SHAPE_LABELS[row.shape] ?? row.shape;
909
+ const description = SHAPE_DESCRIPTIONS[row.shape] ?? "";
910
+ const dayCount = new Set(row.occurrences.map(o => o.date)).size;
911
+ const outcomeStr = formatOutcomeMix(row.outcome_distribution);
912
+ // Prefer the occurrence whose day carries a day_signature for evidence —
913
+ // that's the new day-first signal, more readable than a subagent prompt.
914
+ const sample = row.occurrences.find(o => o.day_signature)
915
+ ?? row.occurrences.find(o => o.evidence_subagent !== null)
916
+ ?? row.occurrences[0]!;
917
+ return (
918
+ <li key={i} style={{
919
+ padding: "14px 16px", borderRadius: 10,
920
+ background: "var(--af-surface)",
921
+ border: "1px solid var(--af-border-subtle)",
922
+ }}>
923
+ <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 4, flexWrap: "wrap" }}>
924
+ <span style={{
925
+ fontSize: 13, fontWeight: 600, color: "var(--af-text)",
926
+ letterSpacing: "-0.01em",
927
+ }}>{label}</span>
928
+ <span style={{
929
+ fontFamily: "var(--font-mono)", fontSize: 11,
930
+ color: "var(--af-text-tertiary)",
931
+ }}>
932
+ {row.occurrences.length} session{row.occurrences.length === 1 ? "" : "s"} · {dayCount} day{dayCount === 1 ? "" : "s"}{outcomeStr ? ` · ${outcomeStr}` : ""}
933
+ </span>
934
+ </div>
935
+ {description && (
936
+ <p style={{
937
+ fontSize: 12, lineHeight: 1.55, margin: "0 0 8px",
938
+ color: "var(--af-text-secondary)",
939
+ }}>{description}</p>
940
+ )}
941
+ {sample.day_signature ? (
942
+ <div style={{
943
+ marginBottom: 8, padding: "8px 10px", borderRadius: 6,
944
+ background: "var(--af-surface-raised)",
945
+ borderLeft: "2px solid var(--af-accent)",
946
+ }}>
947
+ <div style={{
948
+ fontSize: 9, fontWeight: 600, letterSpacing: "0.04em",
949
+ color: "var(--af-accent)", fontFamily: "var(--font-mono)",
950
+ marginBottom: 3,
951
+ }}>
952
+ {dayName(sample.date)} {sample.date.slice(5)}{sample.project_display ? ` · ${sample.project_display}` : ""}
953
+ </div>
954
+ <div style={{
955
+ fontSize: 11, lineHeight: 1.5,
956
+ color: "var(--af-text)", fontStyle: "italic",
957
+ }}>
958
+ <span style={{ color: "var(--af-text-tertiary)" }}>“</span>
959
+ {sample.day_signature}
960
+ <span style={{ color: "var(--af-text-tertiary)" }}>”</span>
961
+ </div>
962
+ </div>
963
+ ) : sample.evidence_subagent ? (
964
+ <div style={{
965
+ marginBottom: 8, padding: "8px 10px", borderRadius: 6,
966
+ background: "var(--af-surface-raised)",
967
+ borderLeft: "2px solid var(--af-accent)",
968
+ }}>
969
+ <div style={{
970
+ fontSize: 9, fontWeight: 600, letterSpacing: "0.04em",
971
+ color: "var(--af-accent)", fontFamily: "var(--font-mono)",
972
+ marginBottom: 3,
973
+ }}>
974
+ {sample.evidence_subagent.type} · {dayName(sample.date)} {sample.date.slice(5)} · {sample.project_display}
975
+ </div>
976
+ <div style={{
977
+ fontSize: 11, lineHeight: 1.5,
978
+ color: "var(--af-text)", fontStyle: "italic",
979
+ }}>
980
+ <span style={{ color: "var(--af-text-tertiary)" }}>“</span>
981
+ {sample.evidence_subagent.prompt_preview}
982
+ <span style={{ color: "var(--af-text-tertiary)" }}>”</span>
983
+ </div>
984
+ </div>
985
+ ) : sample.evidence_first_user ? (
986
+ <div style={{
987
+ marginBottom: 8, padding: "8px 10px", borderRadius: 6,
988
+ background: "var(--af-surface-raised)",
989
+ borderLeft: "2px solid var(--af-border)",
990
+ }}>
991
+ <div style={{
992
+ fontSize: 9, fontWeight: 600, letterSpacing: "0.04em",
993
+ color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)",
994
+ marginBottom: 3,
995
+ }}>
996
+ {dayName(sample.date)} {sample.date.slice(5)} · {sample.project_display}
997
+ </div>
998
+ <div style={{
999
+ fontSize: 11, lineHeight: 1.5,
1000
+ color: "var(--af-text)", fontStyle: "italic",
1001
+ }}>
1002
+ <span style={{ color: "var(--af-text-tertiary)" }}>“</span>
1003
+ {sample.evidence_first_user}
1004
+ <span style={{ color: "var(--af-text-tertiary)" }}>”</span>
1005
+ </div>
1006
+ </div>
1007
+ ) : null}
1008
+ {row.occurrences.length > 1 && (
1009
+ <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
1010
+ {row.occurrences.map((o, j) => (
1011
+ <Link key={j} href={`/digest/${o.date}`} style={{
1012
+ display: "inline-flex", alignItems: "center", gap: 4,
1013
+ padding: "2px 8px", borderRadius: 999,
1014
+ background: "color-mix(in srgb, var(--af-accent) 8%, transparent)",
1015
+ border: "1px solid color-mix(in srgb, var(--af-accent) 22%, transparent)",
1016
+ fontSize: 10, fontFamily: "var(--font-mono)",
1017
+ color: "var(--af-accent)", textDecoration: "none", fontWeight: 600,
1018
+ }}>
1019
+ {dayName(o.date)} {o.date.slice(5)} · {o.project_display}
1020
+ {o.outcome ? ` · ${o.outcome}` : ""}
1021
+ </Link>
1022
+ ))}
1023
+ </div>
1024
+ )}
1025
+ </li>
1026
+ );
1027
+ })}
1028
+ </ul>
1029
+ </Section>
1030
+ );
1031
+ }
1032
+
1033
+ function formatOutcomeMix(dist: Partial<Record<string, number>>): string {
1034
+ const order = ["shipped", "partial", "blocked", "exploratory", "trivial", "idle"];
1035
+ const parts: string[] = [];
1036
+ for (const k of order) {
1037
+ const c = dist[k] ?? 0;
1038
+ if (c > 0) parts.push(`${c} ${k}`);
1039
+ }
1040
+ return parts.join(" · ");
1041
+ }
1042
+
1043
+ function InteractionGrammarSection({
1044
+ grammar,
1045
+ }: {
1046
+ grammar: NonNullable<WeekDigestType["interaction_grammar"]>;
1047
+ }) {
1048
+ const harnessLines = renderHarnessLines(grammar);
1049
+ const commLines = renderCommunicationLines(grammar);
1050
+ const claudeFeatureLines = renderClaudeFeatureLines(grammar);
1051
+ const miscLines = renderMiscGrammarLines(grammar);
1052
+ const allLines = [...harnessLines, ...commLines, ...claudeFeatureLines, ...miscLines];
1053
+
1054
+ if (allLines.length === 0) return null;
1055
+ return (
1056
+ <Section title="Your interaction grammar" anchor="grammar">
1057
+ <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
1058
+ {harnessLines.length > 0 && (
1059
+ <GrammarSubsection title="Your own harness" subtitle="Skills, subagents, and slash commands you authored.">
1060
+ {harnessLines}
1061
+ </GrammarSubsection>
1062
+ )}
1063
+ {commLines.length > 0 && (
1064
+ <GrammarSubsection title="Communication style" subtitle="How you provide context per directive and how much you steer mid-flight.">
1065
+ {commLines}
1066
+ </GrammarSubsection>
1067
+ )}
1068
+ {claudeFeatureLines.length > 0 && (
1069
+ <GrammarSubsection title="Claude features in use" subtitle="Stock framings the user employs (not the user's own inventions).">
1070
+ {claudeFeatureLines}
1071
+ </GrammarSubsection>
1072
+ )}
1073
+ {miscLines.length > 0 && (
1074
+ <GrammarSubsection title="Other patterns" subtitle="Threads, rituals, and gates.">
1075
+ {miscLines}
1076
+ </GrammarSubsection>
1077
+ )}
1078
+ </div>
1079
+ </Section>
1080
+ );
1081
+ }
1082
+
1083
+ function GrammarSubsection({ title, subtitle, children }: { title: string; subtitle: string; children: ReactNode }) {
1084
+ return (
1085
+ <div>
1086
+ <div style={{ marginBottom: 6 }}>
1087
+ <div style={{
1088
+ fontSize: 11, fontWeight: 600, color: "var(--af-text)",
1089
+ letterSpacing: "-0.01em",
1090
+ }}>{title}</div>
1091
+ <div style={{
1092
+ fontSize: 10.5, color: "var(--af-text-tertiary)",
1093
+ lineHeight: 1.45, marginTop: 1,
1094
+ }}>{subtitle}</div>
1095
+ </div>
1096
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
1097
+ {children}
1098
+ </div>
1099
+ </div>
1100
+ );
1101
+ }
1102
+
1103
+ function renderHarnessLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
1104
+ const out: ReactNode[] = [];
1105
+
1106
+ // Skill families take precedence — show the cohesive toolchain.
1107
+ for (const fam of grammar.skill_families ?? []) {
1108
+ out.push(
1109
+ <GrammarLine
1110
+ key={`fam-${fam.family}`}
1111
+ label={`${fam.family}-* skill family`}
1112
+ body={`${fam.total_count} loads across ${fam.members.length} skills: ${fam.members.join(", ")}.`}
1113
+ days={fam.days}
1114
+ />
1115
+ );
1116
+ }
1117
+
1118
+ // Single-skill user-authored entries that aren't already covered by a family.
1119
+ const familyMembers = new Set(
1120
+ (grammar.skill_families ?? []).flatMap(f => f.members)
1121
+ );
1122
+ const standaloneSkills = grammar.user_authored_skills.filter(s => !familyMembers.has(s.skill));
1123
+ if (standaloneSkills.length > 0) {
1124
+ out.push(
1125
+ <GrammarLine
1126
+ key="standalone-skills"
1127
+ label="Other user-authored skills"
1128
+ body={standaloneSkills.slice(0, 5).map(s => `${s.skill} ×${s.count}`).join(" · ")}
1129
+ days={standaloneSkills.flatMap(s => s.days).filter((v, i, a) => a.indexOf(v) === i).sort()}
1130
+ />
1131
+ );
1132
+ }
1133
+
1134
+ for (const sa of grammar.user_authored_subagents ?? []) {
1135
+ out.push(
1136
+ <div key={`sub-${sa.type}`} style={{
1137
+ padding: "10px 12px", borderRadius: 8,
1138
+ background: "var(--af-surface)",
1139
+ border: "1px solid color-mix(in srgb, var(--af-accent) 22%, var(--af-border-subtle))",
1140
+ }}>
1141
+ <div style={{
1142
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
1143
+ textTransform: "uppercase", color: "var(--af-accent)",
1144
+ marginBottom: 4,
1145
+ }}>User-authored subagent · {sa.type}</div>
1146
+ <div style={{ fontSize: 12, color: "var(--af-text)", lineHeight: 1.5 }}>
1147
+ Dispatched {sa.count} time{sa.count === 1 ? "" : "s"} on {sa.days.length} day{sa.days.length === 1 ? "" : "s"}.
1148
+ </div>
1149
+ {sa.sample_prompt_preview && (
1150
+ <div style={{
1151
+ marginTop: 6, padding: "6px 10px", borderRadius: 6,
1152
+ background: "var(--af-surface-raised)",
1153
+ borderLeft: "2px solid var(--af-accent)",
1154
+ fontSize: 11, lineHeight: 1.5, color: "var(--af-text)", fontStyle: "italic",
1155
+ }}>
1156
+ <span style={{ color: "var(--af-text-tertiary)" }}>“</span>
1157
+ {sa.sample_prompt_preview}
1158
+ <span style={{ color: "var(--af-text-tertiary)" }}>”</span>
1159
+ </div>
1160
+ )}
1161
+ {sa.days.length > 0 && (
1162
+ <div style={{ marginTop: 6 }}>
1163
+ <DayChips dates={sa.days} tone="var(--af-accent)" />
1164
+ </div>
1165
+ )}
1166
+ </div>
1167
+ );
1168
+ }
1169
+
1170
+ // Custom slash-command frame — surface here as user-authored commands the
1171
+ // user invoked (the harness's command surface).
1172
+ const slashCmd = grammar.prompt_frames.find(f => f.frame === "slash-command");
1173
+ if (slashCmd) {
1174
+ out.push(
1175
+ <GrammarLine
1176
+ key="slash-cmd"
1177
+ label="Custom slash-commands"
1178
+ body={`Invoked ${slashCmd.count} time${slashCmd.count === 1 ? "" : "s"} via <command-name> framing.`}
1179
+ days={slashCmd.days}
1180
+ />
1181
+ );
1182
+ }
1183
+
1184
+ return out;
1185
+ }
1186
+
1187
+ function renderCommunicationLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
1188
+ const cs = grammar.communication_style;
1189
+ if (!cs) return [];
1190
+ const out: ReactNode[] = [];
1191
+
1192
+ // Verbosity histogram inline.
1193
+ const v = cs.verbosity_distribution;
1194
+ const total = v.short + v.medium + v.long + v.very_long;
1195
+ if (total > 0) {
1196
+ out.push(
1197
+ <GrammarLine
1198
+ key="verbosity"
1199
+ label="Prompt length distribution"
1200
+ body={`${v.short} short (<100c) · ${v.medium} medium (100–500c) · ${v.long} long (500–2000c) · ${v.very_long} very-long (>2000c) — across ${total} session opener${total === 1 ? "" : "s"}.`}
1201
+ />
1202
+ );
1203
+ }
1204
+
1205
+ // External refs.
1206
+ if (cs.external_context_refs && cs.external_context_refs.length > 0) {
1207
+ const byKind = new Map<string, number>();
1208
+ for (const r of cs.external_context_refs) {
1209
+ byKind.set(r.ref_kind, (byKind.get(r.ref_kind) ?? 0) + 1);
1210
+ }
1211
+ const summary = [...byKind.entries()].map(([k, c]) => `${c} ${k}`).join(" · ");
1212
+ out.push(
1213
+ <GrammarLine
1214
+ key="ext-refs"
1215
+ label="External-context references"
1216
+ body={`${cs.external_context_refs.length} session opener${cs.external_context_refs.length === 1 ? "" : "s"} delegated by reference rather than spelling out the work — ${summary}.`}
1217
+ days={[...new Set(cs.external_context_refs.map(r => r.date))].sort()}
1218
+ />
1219
+ );
1220
+ }
1221
+
1222
+ // Steering intensity.
1223
+ const s = cs.steering;
1224
+ if (s && (s.total_interrupts > 0 || s.total_frustrated > 0 || s.total_dissatisfied > 0)) {
1225
+ const intensity = s.total_turns > 0 ? ((s.total_interrupts / s.total_turns) * 100) : 0;
1226
+ const parts: string[] = [];
1227
+ parts.push(`${s.total_interrupts} interrupt${s.total_interrupts === 1 ? "" : "s"} across ${s.total_turns} turns (${intensity.toFixed(1)}%)`);
1228
+ if (s.total_frustrated > 0) parts.push(`${s.total_frustrated} frustrated`);
1229
+ if (s.total_dissatisfied > 0) parts.push(`${s.total_dissatisfied} dissatisfied`);
1230
+ if (s.sessions_with_mid_run_redirect > 0) parts.push(`${s.sessions_with_mid_run_redirect} session${s.sessions_with_mid_run_redirect === 1 ? "" : "s"} with ≥2 interrupts`);
1231
+ out.push(
1232
+ <GrammarLine
1233
+ key="steering"
1234
+ label="Steering intensity"
1235
+ body={parts.join(" · ")}
1236
+ />
1237
+ );
1238
+ } else if (s) {
1239
+ out.push(
1240
+ <GrammarLine
1241
+ key="steering-none"
1242
+ label="Steering intensity"
1243
+ body={`No interrupts or frustrated/dissatisfied signals across ${s.total_turns} turns — let the agent run.`}
1244
+ />
1245
+ );
1246
+ }
1247
+
1248
+ return out;
1249
+ }
1250
+
1251
+ function renderClaudeFeatureLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
1252
+ const out: ReactNode[] = [];
1253
+ for (const f of grammar.prompt_frames) {
1254
+ if (f.origin !== "claude-feature") continue;
1255
+ if (f.frame === "slash-command") continue; // already shown under Harness
1256
+ const label = FRAME_LABELS[f.frame] ?? f.frame;
1257
+ const help = FRAME_HELP[f.frame] ?? "";
1258
+ out.push(
1259
+ <GrammarLine
1260
+ key={`cf-${f.frame}`}
1261
+ label={label}
1262
+ body={`${f.count} session opener${f.count === 1 ? "" : "s"}${help ? `. ${help}` : "."}`}
1263
+ days={f.days}
1264
+ />
1265
+ );
1266
+ }
1267
+ return out;
1268
+ }
1269
+
1270
+ function renderMiscGrammarLines(grammar: NonNullable<WeekDigestType["interaction_grammar"]>): ReactNode[] {
1271
+ const out: ReactNode[] = [];
1272
+
1273
+ if (grammar.brainstorming_warmup_days.length > 0) {
1274
+ out.push(
1275
+ <GrammarLine
1276
+ key="brainstorm"
1277
+ label="Brainstorming as warmup"
1278
+ body={`Loaded a brainstorming/writing-plans skill before tool use on ${grammar.brainstorming_warmup_days.length} day${grammar.brainstorming_warmup_days.length === 1 ? "" : "s"}.`}
1279
+ days={grammar.brainstorming_warmup_days}
1280
+ />
1281
+ );
1282
+ }
1283
+
1284
+ // Personal-habit frames (handoff-prose).
1285
+ for (const f of grammar.prompt_frames) {
1286
+ if (f.origin !== "personal-habit") continue;
1287
+ out.push(
1288
+ <GrammarLine
1289
+ key={`ph-${f.frame}`}
1290
+ label={`${FRAME_LABELS[f.frame] ?? f.frame} (personal habit)`}
1291
+ body={`${f.count} session opener${f.count === 1 ? "" : "s"}. ${FRAME_HELP[f.frame] ?? ""}`}
1292
+ days={f.days}
1293
+ />
1294
+ );
1295
+ }
1296
+
1297
+ // Threads — show all, with a "+N more" fold past 5.
1298
+ if (grammar.threads.length > 0) {
1299
+ out.push(<ThreadList key="threads" threads={grammar.threads} />);
1300
+ }
1301
+
1302
+ if (grammar.todo_ops_total > 0) {
1303
+ out.push(
1304
+ <GrammarLine
1305
+ key="todo"
1306
+ label="TodoWrite ops"
1307
+ body={`${grammar.todo_ops_total} TodoWrite operations across the week — task layer as orchestration substrate.`}
1308
+ />
1309
+ );
1310
+ }
1311
+
1312
+ if (grammar.plan_mode.exit_plan_calls === 0 && grammar.plan_mode.days_with_plan === 0) {
1313
+ out.push(
1314
+ <GrammarLine
1315
+ key="no-plan"
1316
+ label="Plan Mode unused"
1317
+ body="No ExitPlan calls or plan-gated sessions this week. Planning happened by other means (specs / review subagents) or not at all."
1318
+ />
1319
+ );
1320
+ } else {
1321
+ out.push(
1322
+ <GrammarLine
1323
+ key="plan-mode"
1324
+ label="Plan Mode"
1325
+ body={`${grammar.plan_mode.exit_plan_calls} ExitPlan call${grammar.plan_mode.exit_plan_calls === 1 ? "" : "s"} on ${grammar.plan_mode.days_with_plan}/7 days.`}
1326
+ />
1327
+ );
1328
+ }
1329
+
1330
+ return out;
1331
+ }
1332
+
1333
+ function ThreadList({ threads }: { threads: NonNullable<WeekDigestType["interaction_grammar"]>["threads"] }) {
1334
+ const [expanded, setExpanded] = useState(false);
1335
+ const sorted = threads.slice().sort((a, b) => b.entries.length - a.entries.length || b.total_active_min - a.total_active_min);
1336
+ const visible = expanded ? sorted : sorted.slice(0, 5);
1337
+ const hidden = sorted.length - visible.length;
1338
+ return (
1339
+ <div style={{
1340
+ padding: "10px 12px", borderRadius: 8,
1341
+ background: "var(--af-surface)",
1342
+ border: "1px solid var(--af-border-subtle)",
1343
+ }}>
1344
+ <div style={{
1345
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
1346
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
1347
+ marginBottom: 6,
1348
+ }}>Multi-day session threads · {threads.length}</div>
1349
+ <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 4 }}>
1350
+ {visible.map(t => {
1351
+ const dayList = t.entries.map(e => e.date).sort();
1352
+ const project = t.entries[0]?.project_display ?? "";
1353
+ return (
1354
+ <li key={t.thread_id} style={{
1355
+ fontSize: 11, color: "var(--af-text)", lineHeight: 1.5,
1356
+ fontFamily: "var(--font-mono)",
1357
+ display: "flex", gap: 8, alignItems: "baseline",
1358
+ }}>
1359
+ <span style={{ color: "var(--af-text-tertiary)" }}>{t.thread_id.slice(0, 8)}</span>
1360
+ <span style={{ color: "var(--af-text-secondary)" }}>{project}</span>
1361
+ <span style={{ color: "var(--af-text)" }}>{dayList.length}d · {Math.round(t.total_active_min)}m</span>
1362
+ <span style={{ color: "var(--af-text-tertiary)" }}>{dayList[0]} → {dayList[dayList.length - 1]}</span>
1363
+ {t.outcome && <span style={{ color: "var(--af-text-secondary)" }}>· {t.outcome}</span>}
1364
+ </li>
1365
+ );
1366
+ })}
1367
+ </ul>
1368
+ {hidden > 0 && (
1369
+ <button
1370
+ type="button"
1371
+ onClick={() => setExpanded(true)}
1372
+ style={{
1373
+ marginTop: 6, padding: "3px 8px", borderRadius: 4,
1374
+ background: "transparent", border: "1px solid var(--af-border-subtle)",
1375
+ fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
1376
+ fontFamily: "var(--font-mono)",
1377
+ }}
1378
+ >+{hidden} more</button>
1379
+ )}
1380
+ {expanded && sorted.length > 5 && (
1381
+ <button
1382
+ type="button"
1383
+ onClick={() => setExpanded(false)}
1384
+ style={{
1385
+ marginTop: 6, padding: "3px 8px", borderRadius: 4,
1386
+ background: "transparent", border: "1px solid var(--af-border-subtle)",
1387
+ fontSize: 10, color: "var(--af-text-secondary)", cursor: "pointer",
1388
+ fontFamily: "var(--font-mono)",
1389
+ }}
1390
+ >show less</button>
1391
+ )}
1392
+ </div>
1393
+ );
1394
+ }
1395
+
1396
+ function GrammarLine({
1397
+ label, body, days,
1398
+ }: { label: string; body: string; days?: string[] }) {
1399
+ return (
1400
+ <div style={{
1401
+ padding: "10px 12px", borderRadius: 8,
1402
+ background: "var(--af-surface)",
1403
+ border: "1px solid var(--af-border-subtle)",
1404
+ }}>
1405
+ <div style={{
1406
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.08em",
1407
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
1408
+ marginBottom: 4,
1409
+ }}>{label}</div>
1410
+ <div style={{ fontSize: 12, lineHeight: 1.5, color: "var(--af-text)" }}>{body}</div>
1411
+ {days && days.length > 0 && (
1412
+ <div style={{ marginTop: 6 }}>
1413
+ <DayChips dates={days} tone="var(--af-accent)" />
1414
+ </div>
1415
+ )}
1416
+ </div>
1417
+ );
1418
+ }
1419
+
1420
+ function FindingsSection({
1421
+ title, anchor, items, tone, sectionFallbackProse,
1422
+ }: {
1423
+ title: string;
1424
+ anchor: string;
1425
+ items: NonNullable<WeekDigestType["what_worked"]>;
1426
+ tone: string;
1427
+ sectionFallbackProse: string | null;
1428
+ }) {
1429
+ if (!items || items.length === 0) {
1430
+ if (sectionFallbackProse === null) return null;
1431
+ return (
1432
+ <Section title={title} anchor={anchor}>
1433
+ <p style={{
1434
+ fontSize: 12, fontStyle: "italic",
1435
+ color: "var(--af-text-tertiary)",
1436
+ margin: 0, padding: "8px 0",
1437
+ }}>{sectionFallbackProse}</p>
1438
+ </Section>
1439
+ );
1440
+ }
1441
+ return (
1442
+ <Section title={title} anchor={anchor}>
1443
+ <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
1444
+ {items.map((item, i) => (
1445
+ <FindingCard key={i} item={item} tone={tone} />
1446
+ ))}
1447
+ </ul>
1448
+ </Section>
1449
+ );
1450
+ }
1451
+
1452
+ function FindingCard({
1453
+ item, tone,
1454
+ }: {
1455
+ item: NonNullable<WeekDigestType["what_worked"]>[number];
1456
+ tone: string;
1457
+ }) {
1458
+ const anchorLabel = renderAnchor(item.anchor);
1459
+ return (
1460
+ <li style={{
1461
+ padding: "12px 14px", borderRadius: 10,
1462
+ background: `color-mix(in srgb, ${tone} 6%, var(--af-surface))`,
1463
+ border: `1px solid color-mix(in srgb, ${tone} 22%, var(--af-border))`,
1464
+ }}>
1465
+ <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 4, flexWrap: "wrap" }}>
1466
+ <span style={{
1467
+ fontSize: 13, fontWeight: 600, color: "var(--af-text)",
1468
+ letterSpacing: "-0.01em", flex: 1, minWidth: 0,
1469
+ }}>{item.title}</span>
1470
+ <span style={{
1471
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.04em",
1472
+ textTransform: "uppercase", color: tone,
1473
+ padding: "1px 7px", borderRadius: 999,
1474
+ background: `color-mix(in srgb, ${tone} 14%, transparent)`,
1475
+ border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
1476
+ fontFamily: "var(--font-mono)",
1477
+ }} title={`Anchor: ${item.anchor}`}>{anchorLabel}</span>
1478
+ </div>
1479
+ <p style={{ fontSize: 12, lineHeight: 1.6, margin: "0 0 8px", color: "var(--af-text)" }}>
1480
+ {item.detail}
1481
+ </p>
1482
+ <div style={{
1483
+ padding: "6px 10px", borderRadius: 6,
1484
+ background: "var(--af-surface-raised)",
1485
+ borderLeft: `2px solid color-mix(in srgb, ${tone} 50%, transparent)`,
1486
+ display: "flex", gap: 8, alignItems: "flex-start",
1487
+ }}>
1488
+ <Link href={`/digest/${item.evidence.date}`} style={{
1489
+ fontSize: 10, fontWeight: 600, color: tone,
1490
+ fontFamily: "var(--font-mono)", flexShrink: 0,
1491
+ textDecoration: "none", paddingTop: 2,
1492
+ }}>
1493
+ {dayName(item.evidence.date)} {item.evidence.date.slice(5)}
1494
+ </Link>
1495
+ <span style={{ fontSize: 11, lineHeight: 1.5, color: "var(--af-text)", fontStyle: "italic" }}>
1496
+ <span style={{ color: "var(--af-text-tertiary)" }}>“</span>
1497
+ {item.evidence.quote}
1498
+ <span style={{ color: "var(--af-text-tertiary)" }}>”</span>
1499
+ </span>
1500
+ </div>
1501
+ </li>
1502
+ );
1503
+ }
1504
+
1505
+ function renderAnchor(anchor: string): string {
1506
+ if (anchor === "plan-mode-gap") return "Plan-mode gap";
1507
+ if (anchor.startsWith("interaction_grammar.")) {
1508
+ const k = anchor.slice("interaction_grammar.".length);
1509
+ return k.replace(/_/g, " ");
1510
+ }
1511
+ return SHAPE_LABELS[anchor] ?? anchor;
1512
+ }
1513
+
1514
+ function SurprisesSection({
1515
+ items,
1516
+ }: {
1517
+ items: NonNullable<WeekDigestType["what_surprised"]>;
1518
+ }) {
1519
+ if (!items || items.length === 0) return null;
1520
+ return (
1521
+ <Section title="What surprised" anchor="what-surprised">
1522
+ <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
1523
+ {items.map((item, i) => {
1524
+ const tone = "#b794f4";
1525
+ return (
1526
+ <li key={i} style={{
1527
+ padding: "12px 14px", borderRadius: 10,
1528
+ background: `color-mix(in srgb, ${tone} 6%, var(--af-surface))`,
1529
+ border: `1px solid color-mix(in srgb, ${tone} 22%, var(--af-border))`,
1530
+ }}>
1531
+ <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 4, flexWrap: "wrap" }}>
1532
+ <span style={{
1533
+ fontSize: 13, fontWeight: 600, color: "var(--af-text)",
1534
+ letterSpacing: "-0.01em", flex: 1, minWidth: 0,
1535
+ }}>{item.title}</span>
1536
+ <span style={{
1537
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.04em",
1538
+ textTransform: "uppercase", color: tone,
1539
+ padding: "1px 7px", borderRadius: 999,
1540
+ background: `color-mix(in srgb, ${tone} 14%, transparent)`,
1541
+ border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
1542
+ fontFamily: "var(--font-mono)",
1543
+ }}>{SURPRISE_LABELS[item.surprise_kind] ?? item.surprise_kind}</span>
1544
+ </div>
1545
+ <p style={{ fontSize: 12, lineHeight: 1.6, margin: "0 0 8px", color: "var(--af-text)" }}>
1546
+ {item.detail}
1547
+ </p>
1548
+ <div style={{
1549
+ padding: "6px 10px", borderRadius: 6,
1550
+ background: "var(--af-surface-raised)",
1551
+ borderLeft: `2px solid color-mix(in srgb, ${tone} 50%, transparent)`,
1552
+ display: "flex", gap: 8, alignItems: "flex-start",
1553
+ }}>
1554
+ <Link href={`/digest/${item.evidence.date}`} style={{
1555
+ fontSize: 10, fontWeight: 600, color: tone,
1556
+ fontFamily: "var(--font-mono)", flexShrink: 0,
1557
+ textDecoration: "none", paddingTop: 2,
1558
+ }}>
1559
+ {dayName(item.evidence.date)} {item.evidence.date.slice(5)}
1560
+ </Link>
1561
+ <span style={{ fontSize: 11, lineHeight: 1.5, color: "var(--af-text)", fontStyle: "italic" }}>
1562
+ <span style={{ color: "var(--af-text-tertiary)" }}>“</span>
1563
+ {item.evidence.quote}
1564
+ <span style={{ color: "var(--af-text-tertiary)" }}>”</span>
1565
+ </span>
1566
+ </div>
1567
+ </li>
1568
+ );
1569
+ })}
1570
+ </ul>
1571
+ </Section>
1572
+ );
1573
+ }
1574
+
1575
+ function WhereToLeanSection({
1576
+ items,
1577
+ }: {
1578
+ items: NonNullable<WeekDigestType["where_to_lean"]>;
1579
+ }) {
1580
+ if (!items || items.length === 0) return null;
1581
+ return (
1582
+ <Section title="Where to lean" anchor="where-to-lean">
1583
+ <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 12 }}>
1584
+ {items.map((item, i) => {
1585
+ const tone = "var(--af-accent)";
1586
+ const anchorLabel = renderAnchor(item.anchor);
1587
+ return (
1588
+ <li key={i} style={{
1589
+ padding: "14px 16px", borderRadius: 10,
1590
+ background: "var(--af-surface)",
1591
+ border: "1px solid var(--af-border-subtle)",
1592
+ }}>
1593
+ <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 6, flexWrap: "wrap" }}>
1594
+ <span style={{
1595
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.06em",
1596
+ textTransform: "uppercase", color: tone,
1597
+ padding: "1px 7px", borderRadius: 999,
1598
+ background: `color-mix(in srgb, ${tone} 14%, transparent)`,
1599
+ border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`,
1600
+ fontFamily: "var(--font-mono)",
1601
+ }}>{LEAN_KIND_LABELS[item.lean_kind] ?? item.lean_kind}</span>
1602
+ <span style={{ fontSize: 13, fontWeight: 600, color: "var(--af-text)", flex: 1, minWidth: 0 }}>
1603
+ {item.title}
1604
+ </span>
1605
+ <span style={{
1606
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.04em",
1607
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
1608
+ fontFamily: "var(--font-mono)",
1609
+ }} title={`Anchor: ${item.anchor}`}>↳ {anchorLabel}</span>
1610
+ </div>
1611
+ <p style={{ fontSize: 12, lineHeight: 1.6, margin: "0 0 10px", color: "var(--af-text)" }}>
1612
+ {item.detail}
1613
+ </p>
1614
+ {item.copyable && (
1615
+ <CopyBlock label={`Copy ${item.lean_kind === "claude-md" ? "block" : "snippet"}`} payload={item.copyable} />
1616
+ )}
1617
+ <div style={{
1618
+ marginTop: 8, fontSize: 10, color: "var(--af-text-tertiary)",
1619
+ fontFamily: "var(--font-mono)",
1620
+ }}>
1621
+ Evidence · <Link href={`/digest/${item.evidence.date}`} style={{ color: "var(--af-text-tertiary)", textDecoration: "underline" }}>
1622
+ {dayName(item.evidence.date)} {item.evidence.date.slice(5)}
1623
+ </Link> · “{item.evidence.quote.slice(0, 100)}{item.evidence.quote.length > 100 ? "…" : ""}”
1624
+ </div>
1625
+ </li>
1626
+ );
1627
+ })}
1628
+ </ul>
1629
+ </Section>
1630
+ );
1631
+ }
1632
+
1633
+ // ─── Helpers ────────────────────────────────────────────────────────────
1634
+
1635
+ function Section({ title, anchor, children }: { title: string; anchor?: string; children: ReactNode }) {
1636
+ return (
1637
+ <section id={anchor} style={{ marginBottom: 28, scrollMarginTop: 24 }}>
1638
+ <h2 style={sectionTitleStyle()}>{title}</h2>
1639
+ {children}
1640
+ </section>
1641
+ );
1642
+ }
1643
+
1644
+ function sectionTitleStyle(): React.CSSProperties {
1645
+ return {
1646
+ fontSize: 11, fontWeight: 700, letterSpacing: "0.08em",
1647
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
1648
+ margin: "0 0 12px",
1649
+ };
1650
+ }
1651
+
1652
+ function cardStyle(): React.CSSProperties {
1653
+ return {
1654
+ padding: "12px 14px", borderRadius: 8,
1655
+ background: "var(--af-surface)",
1656
+ border: "1px solid var(--af-border-subtle)",
1657
+ };
1658
+ }
1659
+
1660
+ function dayName(date: string): string {
1661
+ const d = new Date(`${date}T12:00:00`);
1662
+ return d.toLocaleDateString("en-US", { weekday: "short" });
1663
+ }
1664
+
1665
+ function formatRange(startIso: string, endIso: string): string {
1666
+ const s = new Date(startIso);
1667
+ const e = new Date(endIso);
1668
+ const fmt = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
1669
+ return `${fmt(s)} – ${fmt(e)}`;
1670
+ }
1671
+
1672
+ // ─── Top sessions section ─────────────────────────────────────────────────
1673
+
1674
+ const PIN_COLORS: Record<string, string> = {
1675
+ "user-steering": "#ed8936",
1676
+ "subagent-burst": "#9f7aea",
1677
+ "long-autonomous": "#4299e1",
1678
+ "plan-mode": "#38b2ac",
1679
+ "pr-ship": "#48bb78",
1680
+ "harness-chain": "#ed64a6",
1681
+ "interrupt": "#f56565",
1682
+ "brainstorm-loop": "#9f7aea",
1683
+ "agent-loop": "#f56565",
1684
+ };
1685
+
1686
+ const PIN_LABELS: Record<string, string> = {
1687
+ "user-steering": "Steering",
1688
+ "subagent-burst": "Subagents",
1689
+ "long-autonomous": "Autonomous",
1690
+ "plan-mode": "Plan",
1691
+ "pr-ship": "Shipped",
1692
+ "harness-chain": "Harness",
1693
+ "interrupt": "Interrupt",
1694
+ "brainstorm-loop": "Brainstorm",
1695
+ "agent-loop": "Loop",
1696
+ };
1697
+
1698
+ function TopSessionsSection({ sessions }: { sessions: WeekTopSession[] }) {
1699
+ return (
1700
+ <Section title={`Top ${sessions.length} session${sessions.length === 1 ? "" : "s"} this week`} anchor="top-sessions">
1701
+ <p style={{
1702
+ fontSize: 12, color: "var(--af-text-tertiary)", marginTop: -4,
1703
+ marginBottom: 14, fontStyle: "italic", lineHeight: 1.5, maxWidth: 720,
1704
+ }}>
1705
+ Picked by significance — active time × subagents × ships, capped to one per project. Each card walks the timeline with annotated moments to surface the texture of how you drove the agent.
1706
+ </p>
1707
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
1708
+ {sessions.map(s => <TopSessionCard key={s.session_id} session={s} />)}
1709
+ </div>
1710
+ </Section>
1711
+ );
1712
+ }
1713
+
1714
+ function TopSessionCard({ session }: { session: WeekTopSession }) {
1715
+ const shapeLabel = session.working_shape ? (SHAPE_LABELS[session.working_shape] ?? session.working_shape) : null;
1716
+ const shapeColor = session.working_shape ? (SHAPE_COLORS[session.working_shape] ?? "var(--af-text-tertiary)") : "var(--af-text-tertiary)";
1717
+ const wallH = session.wall_min / 60;
1718
+ const activeH = session.active_min / 60;
1719
+ const wallStr = wallH >= 1 ? `${wallH.toFixed(1)}h wall` : `${Math.round(session.wall_min)}m wall`;
1720
+ const activeStr = activeH >= 1 ? `${activeH.toFixed(1)}h active` : `${Math.round(session.active_min)}m active`;
1721
+
1722
+ return (
1723
+ <div style={{
1724
+ borderRadius: 10,
1725
+ background: "var(--af-surface)",
1726
+ border: "1px solid var(--af-border-subtle)",
1727
+ borderTop: `3px solid ${shapeColor}`,
1728
+ overflow: "hidden",
1729
+ }}>
1730
+ <div style={{ padding: "16px 18px" }}>
1731
+ {/* Header row */}
1732
+ <div style={{
1733
+ display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap",
1734
+ marginBottom: 6,
1735
+ }}>
1736
+ <Link
1737
+ href={`/sessions/${session.session_id}`}
1738
+ style={{
1739
+ fontSize: 14, fontWeight: 600, color: "var(--af-text)",
1740
+ letterSpacing: "-0.01em", textDecoration: "none",
1741
+ }}
1742
+ >
1743
+ {session.project_display}
1744
+ </Link>
1745
+ <Link
1746
+ href={`/digest/${session.date}`}
1747
+ style={{
1748
+ fontSize: 11, fontFamily: "var(--font-mono)",
1749
+ color: "var(--af-accent)", textDecoration: "none",
1750
+ }}
1751
+ >
1752
+ {dayName(session.date)} {session.date.slice(5)}
1753
+ </Link>
1754
+ <span style={{
1755
+ fontSize: 11, color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)",
1756
+ }}>
1757
+ {activeStr} · {wallStr} · {session.turn_count} turns
1758
+ </span>
1759
+ {shapeLabel && (
1760
+ <span style={{
1761
+ padding: "2px 9px", borderRadius: 999, fontSize: 11, fontWeight: 600,
1762
+ background: `color-mix(in srgb, ${shapeColor} 14%, transparent)`,
1763
+ color: shapeColor,
1764
+ }}>
1765
+ {shapeLabel}
1766
+ </span>
1767
+ )}
1768
+ {session.outcome === "shipped" && session.shipped_prs.length > 0 && (
1769
+ <span style={{
1770
+ padding: "2px 9px", borderRadius: 999, fontSize: 11, fontWeight: 600,
1771
+ background: "color-mix(in srgb, #48bb78 14%, transparent)",
1772
+ color: "#48bb78",
1773
+ }}>
1774
+ ✓ {session.shipped_prs.length} PR{session.shipped_prs.length === 1 ? "" : "s"}
1775
+ </span>
1776
+ )}
1777
+ </div>
1778
+
1779
+ {/* day_signature italic subhead */}
1780
+ {session.day_signature && (
1781
+ <p style={{
1782
+ fontSize: 12, fontStyle: "italic", color: "var(--af-text-secondary)",
1783
+ margin: "0 0 14px", lineHeight: 1.5, maxWidth: 760,
1784
+ }}>
1785
+ “{session.day_signature}”
1786
+ </p>
1787
+ )}
1788
+
1789
+ {/* Structured deep-dive: why-picked / what-happened / what-worked / what-hit-friction */}
1790
+ <NarrativeBlock session={session} />
1791
+
1792
+ {/* Steering summary */}
1793
+ {session.steering_summary && (
1794
+ <p style={{
1795
+ fontSize: 12, lineHeight: 1.55, margin: "10px 0 12px",
1796
+ color: "var(--af-text-secondary)", maxWidth: 780,
1797
+ paddingLeft: 10, borderLeft: "2px solid var(--af-border-subtle)",
1798
+ }}>
1799
+ <span style={{
1800
+ fontSize: 9, fontWeight: 700, letterSpacing: "0.06em",
1801
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
1802
+ marginRight: 6,
1803
+ }}>steering</span>
1804
+ {session.steering_summary}
1805
+ </p>
1806
+ )}
1807
+
1808
+ {/* Harness signature chips */}
1809
+ <HarnessChips session={session} />
1810
+
1811
+ {/* Timeline minimap */}
1812
+ <SessionTimelineMinimap session={session} />
1813
+
1814
+ {/* Pin list */}
1815
+ {session.pins.length > 0 && (
1816
+ <ol style={{
1817
+ listStyle: "none", padding: 0, margin: "12px 0 0",
1818
+ display: "flex", flexDirection: "column", gap: 6,
1819
+ }}>
1820
+ {session.pins.map((pin, i) => <PinRow key={i} pin={pin} />)}
1821
+ </ol>
1822
+ )}
1823
+ </div>
1824
+ </div>
1825
+ );
1826
+ }
1827
+
1828
+ function NarrativeBlock({ session }: { session: WeekTopSession }) {
1829
+ // Backward-compat: cached pre-refactor digests only have session_summary.
1830
+ // Render the new structured fields when present; fall back to the old
1831
+ // single summary otherwise.
1832
+ const hasStructured = session.why_picked || session.what_worked || session.what_hit_friction;
1833
+ if (!hasStructured) {
1834
+ if (!session.session_summary) return null;
1835
+ return (
1836
+ <p style={{
1837
+ fontSize: 13, lineHeight: 1.55, margin: "0 0 8px",
1838
+ color: "var(--af-text)", maxWidth: 780,
1839
+ }}>
1840
+ {session.session_summary}
1841
+ </p>
1842
+ );
1843
+ }
1844
+ return (
1845
+ <div style={{ display: "flex", flexDirection: "column", gap: 10, margin: "0 0 6px" }}>
1846
+ {session.why_picked && (
1847
+ <NarrativeRow
1848
+ label="Why this session"
1849
+ tone="var(--af-text-tertiary)"
1850
+ accent="var(--af-text-tertiary)"
1851
+ text={session.why_picked}
1852
+ />
1853
+ )}
1854
+ {session.session_summary && (
1855
+ <NarrativeRow
1856
+ label="What happened"
1857
+ tone="var(--af-text)"
1858
+ accent="var(--af-text-secondary)"
1859
+ text={session.session_summary}
1860
+ weight={500}
1861
+ />
1862
+ )}
1863
+ {session.what_worked && (
1864
+ <NarrativeRow
1865
+ label="What worked"
1866
+ tone="var(--af-text)"
1867
+ accent="#48bb78"
1868
+ text={session.what_worked}
1869
+ />
1870
+ )}
1871
+ {session.what_hit_friction && (
1872
+ <NarrativeRow
1873
+ label="What hit friction"
1874
+ tone="var(--af-text)"
1875
+ accent="#ed8936"
1876
+ text={session.what_hit_friction}
1877
+ />
1878
+ )}
1879
+ </div>
1880
+ );
1881
+ }
1882
+
1883
+ function NarrativeRow({ label, tone, accent, text, weight = 400 }: {
1884
+ label: string;
1885
+ tone: string;
1886
+ accent: string;
1887
+ text: string;
1888
+ weight?: number;
1889
+ }) {
1890
+ return (
1891
+ <div style={{ display: "flex", gap: 12, alignItems: "flex-start", maxWidth: 800 }}>
1892
+ <span style={{
1893
+ flexShrink: 0, width: 132, paddingTop: 2,
1894
+ fontSize: 9.5, fontWeight: 700, letterSpacing: "0.06em",
1895
+ textTransform: "uppercase", color: accent,
1896
+ }}>
1897
+ {label}
1898
+ </span>
1899
+ <span style={{
1900
+ fontSize: 13, lineHeight: 1.55, color: tone, fontWeight: weight,
1901
+ }}>
1902
+ {text}
1903
+ </span>
1904
+ </div>
1905
+ );
1906
+ }
1907
+
1908
+ function HarnessChips({ session }: { session: WeekTopSession }) {
1909
+ const { user_authored_skills, user_authored_subagents, stock_skills, top_tools } = session;
1910
+ const hasAny = user_authored_skills.length > 0 || user_authored_subagents.length > 0
1911
+ || stock_skills.length > 0 || top_tools.length > 0;
1912
+ if (!hasAny) return null;
1913
+ return (
1914
+ <div style={{ marginBottom: 4 }}>
1915
+ {user_authored_subagents.length > 0 && (
1916
+ <ChipRow label="Your subagents">
1917
+ {user_authored_subagents.map(sa => (
1918
+ <Chip key={sa.type} tone="user">
1919
+ {sa.type}{sa.count > 1 ? ` ×${sa.count}` : ""}
1920
+ </Chip>
1921
+ ))}
1922
+ </ChipRow>
1923
+ )}
1924
+ {(user_authored_skills.length > 0 || stock_skills.length > 0) && (
1925
+ <ChipRow label="Skills loaded">
1926
+ {user_authored_skills.map(s => (<Chip key={s} tone="user">{s}</Chip>))}
1927
+ {stock_skills.slice(0, 6).map(s => (<Chip key={s} tone="stock">{s}</Chip>))}
1928
+ </ChipRow>
1929
+ )}
1930
+ {top_tools.length > 0 && (
1931
+ <ChipRow label="Top tools">
1932
+ {top_tools.slice(0, 4).map((t, i) => (<Chip key={i}>{t}</Chip>))}
1933
+ </ChipRow>
1934
+ )}
1935
+ </div>
1936
+ );
1937
+ }
1938
+
1939
+ function ChipRow({ label, children }: { label: string; children: ReactNode }) {
1940
+ return (
1941
+ <div style={{
1942
+ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap",
1943
+ padding: "3px 0", fontSize: 11,
1944
+ }}>
1945
+ <span style={{
1946
+ minWidth: 96, color: "var(--af-text-tertiary)", fontWeight: 500,
1947
+ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.04em",
1948
+ }}>
1949
+ {label}
1950
+ </span>
1951
+ <div style={{ display: "flex", gap: 5, flexWrap: "wrap", flex: 1 }}>{children}</div>
1952
+ </div>
1953
+ );
1954
+ }
1955
+
1956
+ function Chip({ children, tone, title }: { children: ReactNode; tone?: "user" | "stock"; title?: string }) {
1957
+ const bg = tone === "user" ? "var(--af-accent-subtle)" : "var(--af-border-subtle)";
1958
+ const fg = tone === "user" ? "var(--af-accent)" : "var(--af-text-secondary)";
1959
+ return (
1960
+ <span title={title} style={{
1961
+ display: "inline-flex", alignItems: "center", padding: "2px 8px",
1962
+ borderRadius: 999, fontSize: 10, fontFamily: "var(--font-mono)",
1963
+ background: bg, color: fg, whiteSpace: "nowrap",
1964
+ maxWidth: 280, overflow: "hidden", textOverflow: "ellipsis",
1965
+ }}>
1966
+ {children}
1967
+ </span>
1968
+ );
1969
+ }
1970
+
1971
+ function SessionTimelineMinimap({ session }: { session: WeekTopSession }) {
1972
+ const intervals = session.timeline.active_intervals;
1973
+ // Compute the actual span the bars cover so we never clip an interval off
1974
+ // the right edge. Older cached digests stored duration_min as active_min
1975
+ // while intervals are positioned in wall-clock minutes — taking max() of
1976
+ // both makes the renderer correct regardless of which convention the
1977
+ // cached digest used.
1978
+ const observedMax = intervals.length > 0
1979
+ ? Math.max(...intervals.map(iv => iv.end_min))
1980
+ : 0;
1981
+ const duration = Math.max(1, session.timeline.duration_min, observedMax);
1982
+ return (
1983
+ <div style={{ marginTop: 10, marginBottom: 4 }}>
1984
+ <div style={{
1985
+ position: "relative", width: "100%", height: 28,
1986
+ background: "var(--af-border-subtle)", borderRadius: 4,
1987
+ }}>
1988
+ {/* Active intervals */}
1989
+ {intervals.map((iv, i) => {
1990
+ const left = (iv.start_min / duration) * 100;
1991
+ const width = Math.max(0.6, ((iv.end_min - iv.start_min) / duration) * 100);
1992
+ return (
1993
+ <div key={i} style={{
1994
+ position: "absolute", top: 6, bottom: 6,
1995
+ left: `${left}%`, width: `${width}%`,
1996
+ background: "color-mix(in srgb, var(--af-accent) 30%, transparent)",
1997
+ borderRadius: 2,
1998
+ }} />
1999
+ );
2000
+ })}
2001
+ {/* Pins */}
2002
+ {session.pins.map((pin, i) => {
2003
+ const left = Math.max(0, Math.min(100, (pin.start_min / duration) * 100));
2004
+ const right = pin.end_min !== undefined
2005
+ ? Math.max(left + 0.5, Math.min(100, (pin.end_min / duration) * 100))
2006
+ : left;
2007
+ const isSpan = pin.end_min !== undefined && right > left + 0.5;
2008
+ const color = PIN_COLORS[pin.kind] ?? "var(--af-accent)";
2009
+ return (
2010
+ <div key={i}>
2011
+ {isSpan && (
2012
+ <div style={{
2013
+ position: "absolute", top: 4, bottom: 4,
2014
+ left: `${left}%`, width: `${right - left}%`,
2015
+ background: `color-mix(in srgb, ${color} 35%, transparent)`,
2016
+ border: `1px solid ${color}`,
2017
+ borderRadius: 3,
2018
+ }} title={`${PIN_LABELS[pin.kind]}: ${pin.label}`} />
2019
+ )}
2020
+ <div style={{
2021
+ position: "absolute", top: -3, bottom: -3,
2022
+ left: `${left}%`, width: 3,
2023
+ background: color,
2024
+ borderRadius: 1,
2025
+ transform: "translateX(-1.5px)",
2026
+ }} title={`${PIN_LABELS[pin.kind]}: ${pin.label}`} />
2027
+ </div>
2028
+ );
2029
+ })}
2030
+ </div>
2031
+ {/* Min markers — wall-clock from session start; teal bars are active
2032
+ intervals, gaps are idle periods (>3min between events). */}
2033
+ <div style={{
2034
+ display: "flex", justifyContent: "space-between",
2035
+ fontSize: 9, fontFamily: "var(--font-mono)",
2036
+ color: "var(--af-text-tertiary)",
2037
+ marginTop: 3,
2038
+ }}>
2039
+ <span>0m</span>
2040
+ <span>{formatMin(duration / 2)}</span>
2041
+ <span>{formatMin(duration)} wall</span>
2042
+ </div>
2043
+ </div>
2044
+ );
2045
+ }
2046
+
2047
+ function PinRow({ pin }: { pin: SessionPin }) {
2048
+ const color = PIN_COLORS[pin.kind] ?? "var(--af-accent)";
2049
+ const isSpan = pin.end_min !== undefined && pin.end_min > pin.start_min + 0.5;
2050
+ const t = isSpan
2051
+ ? `${formatMin(pin.start_min)}–${formatMin(pin.end_min!)}`
2052
+ : formatMin(pin.start_min);
2053
+ return (
2054
+ <li style={{
2055
+ display: "flex", gap: 10, alignItems: "flex-start",
2056
+ fontSize: 12, lineHeight: 1.5,
2057
+ }}>
2058
+ <span style={{
2059
+ flexShrink: 0, width: 3, height: 3, marginTop: 7,
2060
+ borderRadius: 999, background: color,
2061
+ }} />
2062
+ <span style={{
2063
+ flexShrink: 0, fontFamily: "var(--font-mono)", fontSize: 10,
2064
+ color: "var(--af-text-tertiary)", paddingTop: 1, minWidth: 60,
2065
+ }}>
2066
+ {t}
2067
+ </span>
2068
+ <span style={{
2069
+ flexShrink: 0, padding: "1px 7px", borderRadius: 999, fontSize: 9,
2070
+ fontWeight: 700, letterSpacing: "0.04em", textTransform: "uppercase",
2071
+ background: `color-mix(in srgb, ${color} 15%, transparent)`,
2072
+ color: color, marginTop: 1,
2073
+ }}>
2074
+ {PIN_LABELS[pin.kind] ?? pin.kind}
2075
+ </span>
2076
+ <span style={{ color: "var(--af-text)", maxWidth: 640 }}>
2077
+ {pin.label}
2078
+ </span>
2079
+ </li>
2080
+ );
2081
+ }
2082
+
2083
+ function formatMin(m: number): string {
2084
+ if (m >= 60) {
2085
+ // Round to whole minutes first, THEN split into hours+minutes — otherwise
2086
+ // a value like 179.6 rounds to "2h 60m" because Math.round(179.6 % 60)
2087
+ // wraps to 60 without bumping the hour.
2088
+ const total = Math.round(m);
2089
+ const h = Math.floor(total / 60);
2090
+ const min = total % 60;
2091
+ return `${h}h${min > 0 ? ` ${min}m` : ""}`;
2092
+ }
2093
+ return `${Math.round(m * 10) / 10}m`;
2094
+ }
2095
+
2096
+ // ─── Pattern rollups fold-down (replaces inline working_shapes + grammar) ─
2097
+
2098
+ function PatternRollupsFold({
2099
+ shapes, grammar,
2100
+ }: {
2101
+ shapes: WeekDigestType["working_shapes"];
2102
+ grammar: WeekDigestType["interaction_grammar"];
2103
+ }) {
2104
+ const [open, setOpen] = useState(false);
2105
+ return (
2106
+ <section style={{ marginBottom: 28 }}>
2107
+ <button
2108
+ type="button"
2109
+ onClick={() => setOpen(o => !o)}
2110
+ style={{
2111
+ display: "inline-flex", alignItems: "center", gap: 6,
2112
+ background: "transparent", border: "none", cursor: "pointer",
2113
+ padding: "6px 0", color: "var(--af-text-tertiary)",
2114
+ fontSize: 11, fontWeight: 700, letterSpacing: "0.08em",
2115
+ textTransform: "uppercase",
2116
+ }}
2117
+ >
2118
+ {open ? "▾" : "▸"} Weekly pattern rollups
2119
+ </button>
2120
+ {open && (
2121
+ <div style={{ marginTop: 8 }}>
2122
+ {shapes && shapes.length > 0 && <WorkingShapesSection shapes={shapes} />}
2123
+ {grammar && <InteractionGrammarSection grammar={grammar} />}
2124
+ </div>
2125
+ )}
2126
+ </section>
2127
+ );
2128
+ }