@shepai/cli 1.148.0 → 1.149.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 (193) hide show
  1. package/dist/src/presentation/web/app/api/agent-events/route.js +1 -1
  2. package/dist/src/presentation/web/app/api/sessions/route.d.ts.map +1 -1
  3. package/dist/src/presentation/web/app/api/sessions/route.js +2 -268
  4. package/dist/src/presentation/web/app/api/sessions-batch/route.d.ts +17 -0
  5. package/dist/src/presentation/web/app/api/sessions-batch/route.d.ts.map +1 -0
  6. package/dist/src/presentation/web/app/api/sessions-batch/route.js +61 -0
  7. package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts +1 -1
  8. package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts.map +1 -1
  9. package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.js +15 -73
  10. package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.stories.d.ts.map +1 -1
  11. package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.stories.js +18 -17
  12. package/dist/src/presentation/web/components/features/control-center/control-center.d.ts.map +1 -1
  13. package/dist/src/presentation/web/components/features/control-center/control-center.js +2 -1
  14. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
  15. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +4 -1
  16. package/dist/src/presentation/web/components/layouts/app-sidebar/app-sidebar.d.ts.map +1 -1
  17. package/dist/src/presentation/web/components/layouts/app-sidebar/app-sidebar.js +32 -33
  18. package/dist/src/presentation/web/hooks/sessions-provider.d.ts +12 -0
  19. package/dist/src/presentation/web/hooks/sessions-provider.d.ts.map +1 -0
  20. package/dist/src/presentation/web/hooks/sessions-provider.js +57 -0
  21. package/dist/src/presentation/web/hooks/use-deploy-action.d.ts.map +1 -1
  22. package/dist/src/presentation/web/hooks/use-deploy-action.js +8 -54
  23. package/dist/src/presentation/web/lib/session-scanner.d.ts +27 -0
  24. package/dist/src/presentation/web/lib/session-scanner.d.ts.map +1 -0
  25. package/dist/src/presentation/web/lib/session-scanner.js +255 -0
  26. package/dist/tsconfig.build.tsbuildinfo +1 -1
  27. package/package.json +1 -1
  28. package/web/.next/BUILD_ID +1 -1
  29. package/web/.next/app-path-routes-manifest.json +1 -0
  30. package/web/.next/build-manifest.json +2 -2
  31. package/web/.next/fallback-build-manifest.json +2 -2
  32. package/web/.next/prerender-manifest.json +3 -3
  33. package/web/.next/required-server-files.js +2 -2
  34. package/web/.next/required-server-files.json +2 -2
  35. package/web/.next/routes-manifest.json +6 -0
  36. package/web/.next/server/app/(dashboard)/@drawer/adopt/page/server-reference-manifest.json +28 -28
  37. package/web/.next/server/app/(dashboard)/@drawer/adopt/page.js.nft.json +1 -1
  38. package/web/.next/server/app/(dashboard)/@drawer/adopt/page_client-reference-manifest.js +1 -1
  39. package/web/.next/server/app/(dashboard)/@drawer/create/page/server-reference-manifest.json +28 -28
  40. package/web/.next/server/app/(dashboard)/@drawer/create/page.js.nft.json +1 -1
  41. package/web/.next/server/app/(dashboard)/@drawer/create/page_client-reference-manifest.js +1 -1
  42. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
  43. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page.js.nft.json +1 -1
  44. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
  45. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page/server-reference-manifest.json +36 -36
  46. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js.nft.json +1 -1
  47. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page_client-reference-manifest.js +1 -1
  48. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
  49. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js.nft.json +1 -1
  50. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  51. package/web/.next/server/app/(dashboard)/create/page/server-reference-manifest.json +28 -28
  52. package/web/.next/server/app/(dashboard)/create/page.js.nft.json +1 -1
  53. package/web/.next/server/app/(dashboard)/create/page_client-reference-manifest.js +1 -1
  54. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
  55. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page.js.nft.json +1 -1
  56. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
  57. package/web/.next/server/app/(dashboard)/feature/[featureId]/page/server-reference-manifest.json +36 -36
  58. package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js.nft.json +1 -1
  59. package/web/.next/server/app/(dashboard)/feature/[featureId]/page_client-reference-manifest.js +1 -1
  60. package/web/.next/server/app/(dashboard)/page/server-reference-manifest.json +26 -26
  61. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  62. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  63. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
  64. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js.nft.json +1 -1
  65. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  66. package/web/.next/server/app/_global-error.html +2 -2
  67. package/web/.next/server/app/_global-error.rsc +1 -1
  68. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  69. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  70. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  71. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  72. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  73. package/web/.next/server/app/_not-found/page/server-reference-manifest.json +3 -3
  74. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  75. package/web/.next/server/app/api/attachments/preview/route.js.nft.json +1 -1
  76. package/web/.next/server/app/api/evidence/route.js.nft.json +1 -1
  77. package/web/.next/server/app/api/graph-data/route.js.nft.json +1 -1
  78. package/web/.next/server/app/api/sessions/route.js +2 -3
  79. package/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
  80. package/web/.next/server/app/api/sessions-batch/route/app-paths-manifest.json +3 -0
  81. package/web/.next/server/app/api/sessions-batch/route/build-manifest.json +11 -0
  82. package/web/.next/server/app/api/sessions-batch/route/server-reference-manifest.json +4 -0
  83. package/web/.next/server/app/api/sessions-batch/route.js +7 -0
  84. package/web/.next/server/app/api/sessions-batch/route.js.map +5 -0
  85. package/web/.next/server/app/api/sessions-batch/route.js.nft.json +1 -0
  86. package/web/.next/server/app/api/sessions-batch/route_client-reference-manifest.js +2 -0
  87. package/web/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  88. package/web/.next/server/app/settings/page.js.nft.json +1 -1
  89. package/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  90. package/web/.next/server/app/skills/page/server-reference-manifest.json +8 -8
  91. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  92. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  93. package/web/.next/server/app/tools/page/server-reference-manifest.json +8 -8
  94. package/web/.next/server/app/tools/page.js.nft.json +1 -1
  95. package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  96. package/web/.next/server/app/version/page/server-reference-manifest.json +3 -3
  97. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  98. package/web/.next/server/app-paths-manifest.json +1 -0
  99. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +1 -1
  100. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -1
  101. package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_sessions-batch_route_actions_4859f283.js +3 -0
  102. package/web/.next/server/chunks/[root-of-the-server]__0d33c29e._.js +3 -0
  103. package/web/.next/server/chunks/[root-of-the-server]__0d33c29e._.js.map +1 -0
  104. package/web/.next/server/chunks/[root-of-the-server]__2f61738a._.js +3 -0
  105. package/web/.next/server/chunks/[root-of-the-server]__2f61738a._.js.map +1 -0
  106. package/web/.next/server/chunks/[root-of-the-server]__a402b567._.js +1 -1
  107. package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js +1 -1
  108. package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js.map +1 -1
  109. package/web/.next/server/chunks/ssr/[root-of-the-server]__2138fa7e._.js +2 -2
  110. package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js +1 -1
  111. package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js.map +1 -1
  112. package/web/.next/server/chunks/ssr/[root-of-the-server]__357d99f9._.js +1 -1
  113. package/web/.next/server/chunks/ssr/[root-of-the-server]__3ef34e4c._.js +1 -1
  114. package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js +1 -1
  115. package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js.map +1 -1
  116. package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js +1 -1
  117. package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js.map +1 -1
  118. package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js +2 -2
  119. package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js.map +1 -1
  120. package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js +1 -1
  121. package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js.map +1 -1
  122. package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js +1 -1
  123. package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js.map +1 -1
  124. package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js +1 -1
  125. package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js.map +1 -1
  126. package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js +1 -1
  127. package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js.map +1 -1
  128. package/web/.next/server/chunks/ssr/_05c23ad9._.js +1 -1
  129. package/web/.next/server/chunks/ssr/_05c23ad9._.js.map +1 -1
  130. package/web/.next/server/chunks/ssr/_0c5f56e3._.js +2 -2
  131. package/web/.next/server/chunks/ssr/_0c5f56e3._.js.map +1 -1
  132. package/web/.next/server/chunks/ssr/_16eb4fec._.js +1 -1
  133. package/web/.next/server/chunks/ssr/_16eb4fec._.js.map +1 -1
  134. package/web/.next/server/chunks/ssr/_1b719e7f._.js +1 -1
  135. package/web/.next/server/chunks/ssr/_1b719e7f._.js.map +1 -1
  136. package/web/.next/server/chunks/ssr/_37e8548b._.js +1 -1
  137. package/web/.next/server/chunks/ssr/_37e8548b._.js.map +1 -1
  138. package/web/.next/server/chunks/ssr/{_fe63a7f9._.js → _458e9a64._.js} +2 -2
  139. package/web/.next/server/chunks/ssr/{_fe63a7f9._.js.map → _458e9a64._.js.map} +1 -1
  140. package/web/.next/server/chunks/ssr/_5022e2b1._.js +4 -0
  141. package/web/.next/server/chunks/ssr/_5022e2b1._.js.map +1 -0
  142. package/web/.next/server/chunks/ssr/_55d763e2._.js +1 -1
  143. package/web/.next/server/chunks/ssr/_55d763e2._.js.map +1 -1
  144. package/web/.next/server/chunks/ssr/_6256a985._.js +1 -1
  145. package/web/.next/server/chunks/ssr/_6256a985._.js.map +1 -1
  146. package/web/.next/server/chunks/ssr/_64bdfc6f._.js +2 -2
  147. package/web/.next/server/chunks/ssr/_64bdfc6f._.js.map +1 -1
  148. package/web/.next/server/chunks/ssr/_8fcc39d4._.js +1 -1
  149. package/web/.next/server/chunks/ssr/_b71645b4._.js +1 -1
  150. package/web/.next/server/chunks/ssr/_b71645b4._.js.map +1 -1
  151. package/web/.next/server/chunks/ssr/_d8575088._.js +1 -1
  152. package/web/.next/server/chunks/ssr/_d8575088._.js.map +1 -1
  153. package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js +1 -1
  154. package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js.map +1 -1
  155. package/web/.next/server/chunks/ssr/{src_presentation_web_7b2fda40._.js → src_presentation_web_35159458._.js} +2 -2
  156. package/web/.next/server/chunks/ssr/{src_presentation_web_7b2fda40._.js.map → src_presentation_web_35159458._.js.map} +1 -1
  157. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js +1 -1
  158. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js.map +1 -1
  159. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js +1 -1
  160. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js.map +1 -1
  161. package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_open-ide_ts_baaca5d5._.js +1 -1
  162. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  163. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
  164. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js +1 -1
  165. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js.map +1 -1
  166. package/web/.next/server/pages/500.html +2 -2
  167. package/web/.next/server/server-reference-manifest.js +1 -1
  168. package/web/.next/server/server-reference-manifest.json +44 -44
  169. package/web/.next/static/chunks/{0137d4850cab3c45.js → 24b1c1e60fd3b7b5.js} +2 -2
  170. package/web/.next/static/chunks/{c731682077fbac4f.js → 3e7a130816229439.js} +1 -1
  171. package/web/.next/static/chunks/{7c5131e33516a325.js → 3f1b33498b472b00.js} +1 -1
  172. package/web/.next/static/chunks/{04869f1d3f5d9071.js → 4ef564fb1174e497.js} +1 -1
  173. package/web/.next/static/chunks/75834e430247b325.js +1 -0
  174. package/web/.next/static/chunks/79dc2e2f1c2ff519.js +1 -0
  175. package/web/.next/static/chunks/{063a24b49d9818a0.js → a086f8dfef2c3325.js} +1 -1
  176. package/web/.next/static/chunks/{48850e202dd814ac.js → a6363f73e05ccf47.js} +1 -1
  177. package/web/.next/static/chunks/{6f76e63ead3fac2e.js → b7126c0b3a97e77e.js} +1 -1
  178. package/web/.next/static/chunks/d3df6e6434e16519.js +1 -0
  179. package/web/.next/static/chunks/eaca60cc3ab0bf9f.js +2 -0
  180. package/web/.next/static/chunks/{9dad6769d10a32df.js → fe5d48f8ca483935.js} +1 -1
  181. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_ff60e4a5.js +0 -3
  182. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_ff60e4a5.js.map +0 -1
  183. package/web/.next/server/chunks/[externals]__448264a3._.js +0 -3
  184. package/web/.next/server/chunks/ssr/_4533d6f8._.js +0 -4
  185. package/web/.next/server/chunks/ssr/_4533d6f8._.js.map +0 -1
  186. package/web/.next/static/chunks/21e82fee1a7e1668.js +0 -1
  187. package/web/.next/static/chunks/682563e4503cbd58.js +0 -1
  188. package/web/.next/static/chunks/683b1d85e789c2eb.js +0 -2
  189. package/web/.next/static/chunks/d62ae5e449d87057.js +0 -1
  190. /package/web/.next/server/chunks/{[externals]__448264a3._.js.map → 744ca_web__next-internal_server_app_api_sessions-batch_route_actions_4859f283.js.map} +0 -0
  191. /package/web/.next/static/{zYKuE1zbe1UWwAJv5EVwg → 1CQHYZVn3VajyhdvnsCaw}/_buildManifest.js +0 -0
  192. /package/web/.next/static/{zYKuE1zbe1UWwAJv5EVwg → 1CQHYZVn3VajyhdvnsCaw}/_clientMiddlewareManifest.json +0 -0
  193. /package/web/.next/static/{zYKuE1zbe1UWwAJv5EVwg → 1CQHYZVn3VajyhdvnsCaw}/_ssgManifest.js +0 -0
@@ -18,7 +18,7 @@ import { AgentRunStatus, SdlcLifecycle, NotificationEventType, NotificationSever
18
18
  import { isProcessAlive } from '../../../../../../packages/core/src/infrastructure/services/process/is-process-alive.js';
19
19
  // Force dynamic — SSE streams must never be statically optimized or cached
20
20
  export const dynamic = 'force-dynamic';
21
- const POLL_INTERVAL_MS = 500;
21
+ const POLL_INTERVAL_MS = 2_000;
22
22
  const HEARTBEAT_INTERVAL_MS = 30_000;
23
23
  /**
24
24
  * Maps SdlcLifecycle values to agent graph node names so the client
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/sessions/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,eAAO,MAAM,OAAO,kBAAkB,CAAC;AA2VvC;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO;;;;YA9VpC,MAAM;mBACC,MAAM;iBACR,MAAM,GAAG,IAAI;sBACR,MAAM;wBACJ,MAAM,GAAG,IAAI;uBACd,MAAM,GAAG,IAAI;mBACjB,MAAM,GAAG,IAAI;qBACX,MAAM;kBACT,MAAM;;IAoXjB"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/sessions/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAEvC;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO;;;;;;;;;;;;;;IAqBzC"}
@@ -1,264 +1,6 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { createHash } from 'node:crypto';
3
- import { homedir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { readdir, stat } from 'node:fs/promises';
2
+ import { scanSessionsForPath } from '../../../lib/session-scanner.js';
6
3
  export const dynamic = 'force-dynamic';
7
- // ── Path encoding helpers ─────────────────────────────────────────────
8
- /**
9
- * Claude Code encodes paths by replacing '/', '\', '.' with '-'.
10
- * e.g. /home/user/.shep/repos/abc → -home-user--shep-repos-abc
11
- */
12
- function claudeEncodePath(p) {
13
- return p.replace(/[/\\.]/g, '-');
14
- }
15
- /**
16
- * Cursor encodes paths by stripping the leading '/', removing dots,
17
- * and replacing '/' and '\' with '-'.
18
- * e.g. /home/user/.shep/repos/abc → home-user-shep-repos-abc
19
- */
20
- function cursorEncodePath(p) {
21
- return p.replace(/^\//, '').replace(/\./g, '').replace(/[/\\]/g, '-');
22
- }
23
- // ── Claude Code session scanner ───────────────────────────────────────
24
- /**
25
- * Collect .jsonl session files from a single Claude project directory.
26
- */
27
- async function collectJsonlFiles(projectDir) {
28
- let entries;
29
- try {
30
- entries = await readdir(projectDir);
31
- }
32
- catch {
33
- return [];
34
- }
35
- const jsonlFiles = entries.filter((e) => e.endsWith('.jsonl'));
36
- const fileInfos = await Promise.allSettled(jsonlFiles.map(async (name) => {
37
- const filePath = join(projectDir, name);
38
- const s = await stat(filePath);
39
- return { name, filePath, mtime: s.mtime.getTime() };
40
- }));
41
- return fileInfos
42
- .filter((r) => r.status === 'fulfilled')
43
- .map((r) => r.value);
44
- }
45
- async function scanClaudeSessions(repositoryPath, limit, includeWorktrees = false) {
46
- const dirName = claudeEncodePath(repositoryPath);
47
- const projectsRoot = join(homedir(), '.claude', 'projects');
48
- // Collect files from the exact directory
49
- const primaryDir = join(projectsRoot, dirName);
50
- let allFiles = await collectJsonlFiles(primaryDir);
51
- // When includeWorktrees is set, also scan:
52
- // 1. Directories whose name starts with the encoded repo path (git worktrees, .worktrees)
53
- // 2. Shep worktree directories (~/.shep/repos/<hash>/wt/*) which use a hash of the repo path
54
- if (includeWorktrees) {
55
- try {
56
- const allDirs = await readdir(projectsRoot);
57
- // Match git-style worktrees (same prefix as repo path)
58
- const prefixMatches = allDirs.filter((d) => d !== dirName && d.startsWith(dirName));
59
- // Match shep worktrees: compute repo hash → find dirs starting with encoded shep path
60
- const normalizedRepoPath = repositoryPath.replace(/\\/g, '/');
61
- const repoHash = createHash('sha256').update(normalizedRepoPath).digest('hex').slice(0, 16);
62
- const shepHome = join(homedir(), '.shep').replace(/\\/g, '/');
63
- const shepWorktreePrefix = claudeEncodePath(join(shepHome, 'repos', repoHash));
64
- const shepMatches = allDirs.filter((d) => d.startsWith(shepWorktreePrefix) && !prefixMatches.includes(d) && d !== dirName);
65
- const worktreeDirs = [...prefixMatches, ...shepMatches];
66
- const worktreeResults = await Promise.all(worktreeDirs.map((d) => collectJsonlFiles(join(projectsRoot, d))));
67
- for (const files of worktreeResults) {
68
- allFiles = allFiles.concat(files);
69
- }
70
- }
71
- catch {
72
- // projectsRoot doesn't exist — no sessions at all
73
- }
74
- }
75
- const valid = allFiles.sort((a, b) => b.mtime - a.mtime).slice(0, limit);
76
- // Parse each file
77
- const results = await Promise.allSettled(valid.map(async (fi) => parseClaudeSession(fi.filePath, fi.name, fi.mtime, repositoryPath)));
78
- return results
79
- .filter((r) => r.status === 'fulfilled')
80
- .map((r) => r.value)
81
- .filter((s) => s !== null);
82
- }
83
- /**
84
- * Read the first N bytes of a file to extract preview and timestamps
85
- * without loading the entire (potentially multi-MB) session file.
86
- */
87
- const PREVIEW_READ_BYTES = 8_192; // 8KB is enough for first few messages
88
- async function parseClaudeSession(filePath, fileName, mtime, repositoryPath) {
89
- const { createReadStream } = await import('node:fs');
90
- const id = fileName.replace('.jsonl', '');
91
- // Read only the first chunk to extract preview and first timestamp
92
- let preview = null;
93
- let firstTimestamp = null;
94
- let messageCount = 0;
95
- const head = await new Promise((resolve) => {
96
- const chunks = [];
97
- let size = 0;
98
- const stream = createReadStream(filePath, { end: PREVIEW_READ_BYTES - 1 });
99
- stream.on('data', (chunk) => {
100
- chunks.push(chunk);
101
- size += chunk.length;
102
- });
103
- stream.on('end', () => resolve(Buffer.concat(chunks, size).toString('utf-8')));
104
- stream.on('error', () => resolve(''));
105
- });
106
- if (!head)
107
- return null;
108
- const lines = head.split('\n').filter((l) => l.trim());
109
- for (const line of lines) {
110
- try {
111
- const entry = JSON.parse(line);
112
- if (entry.type === 'user' || entry.type === 'assistant') {
113
- const role = entry.message?.role;
114
- if (role === 'user' || role === 'assistant') {
115
- messageCount++;
116
- if (entry.timestamp) {
117
- firstTimestamp ??= entry.timestamp;
118
- }
119
- if (role === 'user' && preview === null) {
120
- preview = extractText(entry.message?.content);
121
- }
122
- }
123
- }
124
- }
125
- catch {
126
- break;
127
- }
128
- }
129
- if (messageCount === 0)
130
- return null;
131
- // Use mtime as lastMessageAt — avoids reading entire file for last line
132
- const mtimeIso = new Date(mtime).toISOString();
133
- return {
134
- id,
135
- agentType: 'claude-code',
136
- preview,
137
- messageCount,
138
- firstMessageAt: firstTimestamp,
139
- lastMessageAt: mtimeIso,
140
- createdAt: firstTimestamp ?? mtimeIso,
141
- projectPath: repositoryPath,
142
- filePath,
143
- _mtime: mtime,
144
- };
145
- }
146
- // ── Cursor session scanner ────────────────────────────────────────────
147
- async function scanCursorSessions(repositoryPath, limit) {
148
- const dirName = cursorEncodePath(repositoryPath);
149
- const transcriptsDir = join(homedir(), '.cursor', 'projects', dirName, 'agent-transcripts');
150
- let entries;
151
- try {
152
- entries = await readdir(transcriptsDir);
153
- }
154
- catch {
155
- return [];
156
- }
157
- // Cursor has two session structures:
158
- // 1. Flat: agent-transcripts/<uuid>.jsonl
159
- // 2. Nested: agent-transcripts/<uuid>/<uuid>.jsonl
160
- const fileInfos = await Promise.allSettled(entries.map(async (entry) => {
161
- const entryPath = join(transcriptsDir, entry);
162
- const s = await stat(entryPath);
163
- if (s.isFile() && entry.endsWith('.jsonl')) {
164
- // Flat structure
165
- return { name: entry, filePath: entryPath, mtime: s.mtime.getTime() };
166
- }
167
- if (s.isDirectory()) {
168
- // Nested structure — look for <uuid>.jsonl inside
169
- const jsonlPath = join(entryPath, `${entry}.jsonl`);
170
- try {
171
- const jsonlStat = await stat(jsonlPath);
172
- return {
173
- name: `${entry}.jsonl`,
174
- filePath: jsonlPath,
175
- mtime: jsonlStat.mtime.getTime(),
176
- };
177
- }
178
- catch {
179
- return null;
180
- }
181
- }
182
- return null;
183
- }));
184
- const valid = fileInfos
185
- .filter((r) => r.status === 'fulfilled')
186
- .map((r) => r.value)
187
- .filter((v) => v !== null)
188
- .sort((a, b) => b.mtime - a.mtime)
189
- .slice(0, limit);
190
- const results = await Promise.allSettled(valid.map(async (fi) => parseCursorSession(fi.filePath, fi.name, fi.mtime, repositoryPath)));
191
- return results
192
- .filter((r) => r.status === 'fulfilled')
193
- .map((r) => r.value)
194
- .filter((s) => s !== null);
195
- }
196
- async function parseCursorSession(filePath, fileName, mtime, repositoryPath) {
197
- const { createReadStream } = await import('node:fs');
198
- const id = fileName.replace('.jsonl', '');
199
- // Read only the first chunk for preview extraction
200
- const head = await new Promise((resolve) => {
201
- const chunks = [];
202
- let size = 0;
203
- const stream = createReadStream(filePath, { end: PREVIEW_READ_BYTES - 1 });
204
- stream.on('data', (chunk) => {
205
- chunks.push(chunk);
206
- size += chunk.length;
207
- });
208
- stream.on('end', () => resolve(Buffer.concat(chunks, size).toString('utf-8')));
209
- stream.on('error', () => resolve(''));
210
- });
211
- if (!head)
212
- return null;
213
- let preview = null;
214
- let messageCount = 0;
215
- const lines = head.split('\n').filter((l) => l.trim());
216
- for (const line of lines) {
217
- try {
218
- const entry = JSON.parse(line);
219
- if (entry.role === 'user' || entry.role === 'assistant') {
220
- messageCount++;
221
- if (entry.role === 'user' && preview === null) {
222
- preview = extractText(entry.message?.content);
223
- }
224
- }
225
- }
226
- catch {
227
- break;
228
- }
229
- }
230
- if (messageCount === 0)
231
- return null;
232
- const mtimeIso = new Date(mtime).toISOString();
233
- return {
234
- id,
235
- agentType: 'cursor',
236
- preview,
237
- messageCount,
238
- firstMessageAt: mtimeIso,
239
- lastMessageAt: mtimeIso,
240
- createdAt: mtimeIso,
241
- projectPath: repositoryPath,
242
- filePath,
243
- _mtime: mtime,
244
- };
245
- }
246
- // ── Shared helpers ────────────────────────────────────────────────────
247
- function extractText(content) {
248
- if (typeof content === 'string')
249
- return content;
250
- if (Array.isArray(content)) {
251
- for (const block of content) {
252
- if (typeof block === 'object' && block !== null) {
253
- const b = block;
254
- if (b.type === 'text' && typeof b.text === 'string')
255
- return b.text;
256
- }
257
- }
258
- }
259
- return null;
260
- }
261
- // ── Route handler ─────────────────────────────────────────────────────
262
4
  /**
263
5
  * GET /api/sessions?repositoryPath=<path>&limit=<n>
264
6
  *
@@ -274,15 +16,7 @@ export async function GET(request) {
274
16
  return NextResponse.json({ error: 'repositoryPath is required' }, { status: 400 });
275
17
  }
276
18
  try {
277
- // Scan all providers in parallel
278
- const [claudeSessions, cursorSessions] = await Promise.all([
279
- scanClaudeSessions(repositoryPath, limit, includeWorktrees),
280
- scanCursorSessions(repositoryPath, limit),
281
- ]);
282
- // Merge and sort by mtime descending, apply limit
283
- const allSessions = [...claudeSessions, ...cursorSessions]
284
- .sort((a, b) => b._mtime - a._mtime)
285
- .slice(0, Math.min(limit, 50));
19
+ const allSessions = await scanSessionsForPath(repositoryPath, limit, includeWorktrees);
286
20
  return NextResponse.json({
287
21
  sessions: allSessions.map(({ _mtime, ...s }) => s),
288
22
  });
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { type SessionResult } from '../../../lib/session-scanner.js';
3
+ export declare const dynamic = "force-dynamic";
4
+ type SessionSummaryFromBatch = Omit<SessionResult, '_mtime'>;
5
+ /**
6
+ * GET /api/sessions-batch
7
+ *
8
+ * No parameters needed — resolves all repos and features from the DI container,
9
+ * scans sessions for each, and returns { sessionsByPath: Record<string, SessionSummary[]> }.
10
+ */
11
+ export declare function GET(): Promise<NextResponse<{
12
+ sessionsByPath: Record<string, SessionSummaryFromBatch[]>;
13
+ }> | NextResponse<{
14
+ error: string;
15
+ }>>;
16
+ export {};
17
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/sessions-batch/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEhF,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAEvC,KAAK,uBAAuB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AAW7D;;;;;GAKG;AACH,wBAAsB,GAAG;;;;IAsDxB"}
@@ -0,0 +1,61 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { resolve } from '../../../lib/server-container.js';
3
+ import { scanSessionsForPath } from '../../../lib/session-scanner.js';
4
+ export const dynamic = 'force-dynamic';
5
+ const SESSIONS_PER_PATH = 5;
6
+ // ── Server-side cache ─────────────────────────────────────────────────
7
+ const CACHE_TTL_MS = 30_000;
8
+ let cache = null;
9
+ // ── Route handler ─────────────────────────────────────────────────────
10
+ /**
11
+ * GET /api/sessions-batch
12
+ *
13
+ * No parameters needed — resolves all repos and features from the DI container,
14
+ * scans sessions for each, and returns { sessionsByPath: Record<string, SessionSummary[]> }.
15
+ */
16
+ export async function GET() {
17
+ // Return cache if fresh
18
+ if (cache && Date.now() - cache.createdAt < CACHE_TTL_MS) {
19
+ return NextResponse.json({ sessionsByPath: cache.data });
20
+ }
21
+ try {
22
+ const listRepos = resolve('ListRepositoriesUseCase');
23
+ const listFeatures = resolve('ListFeaturesUseCase');
24
+ const [repositories, features] = await Promise.all([
25
+ listRepos.execute(),
26
+ listFeatures.execute({ includeArchived: false }),
27
+ ]);
28
+ // Build unique path specs: repos with includeWorktrees, features with their worktree path
29
+ const pathSpecs = [];
30
+ const seen = new Set();
31
+ for (const repo of repositories) {
32
+ if (repo.path && !seen.has(repo.path)) {
33
+ seen.add(repo.path);
34
+ pathSpecs.push({ path: repo.path, includeWorktrees: true });
35
+ }
36
+ }
37
+ for (const feature of features) {
38
+ const sessionPath = feature.worktreePath ?? feature.repositoryPath;
39
+ if (sessionPath && !seen.has(sessionPath)) {
40
+ seen.add(sessionPath);
41
+ pathSpecs.push({ path: sessionPath, includeWorktrees: false });
42
+ }
43
+ }
44
+ // Scan all paths in parallel
45
+ const results = await Promise.all(pathSpecs.map(async ({ path, includeWorktrees }) => {
46
+ const sessions = await scanSessionsForPath(path, SESSIONS_PER_PATH, includeWorktrees);
47
+ return { path, sessions: sessions.map(({ _mtime, ...s }) => s) };
48
+ }));
49
+ const sessionsByPath = {};
50
+ for (const { path, sessions } of results) {
51
+ sessionsByPath[path] = sessions;
52
+ }
53
+ cache = { data: sessionsByPath, createdAt: Date.now() };
54
+ return NextResponse.json({ sessionsByPath });
55
+ }
56
+ catch (error) {
57
+ // eslint-disable-next-line no-console
58
+ console.error('[API] GET /api/sessions-batch error:', error);
59
+ return NextResponse.json({ error: String(error) }, { status: 500 });
60
+ }
61
+ }
@@ -18,6 +18,6 @@ interface FeatureSessionsDropdownProps {
18
18
  /** Callback to create a feature from a session. Only shown on repo nodes. */
19
19
  onCreateFromSession?: (session: SessionSummary, sessionFilePath: string) => void;
20
20
  }
21
- export declare function FeatureSessionsDropdown({ repositoryPath, className, includeWorktrees, onCreateFromSession, }: FeatureSessionsDropdownProps): import("react/jsx-runtime").JSX.Element;
21
+ export declare function FeatureSessionsDropdown({ repositoryPath, className, onCreateFromSession, }: FeatureSessionsDropdownProps): import("react/jsx-runtime").JSX.Element;
22
22
  export {};
23
23
  //# sourceMappingURL=feature-sessions-dropdown.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"feature-sessions-dropdown.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-sessions-dropdown.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,4BAA4B;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CAClF;AAmDD,wBAAgB,uBAAuB,CAAC,EACtC,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,mBAAmB,GACpB,EAAE,4BAA4B,2CAiK9B"}
1
+ {"version":3,"file":"feature-sessions-dropdown.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-sessions-dropdown.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,4BAA4B;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CAClF;AAmDD,wBAAgB,uBAAuB,CAAC,EACtC,cAAc,EACd,SAAS,EACT,mBAAmB,GACpB,EAAE,4BAA4B,2CA6F9B"}
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState, useCallback, useEffect, useRef } from 'react';
4
- import { History, Copy, ExternalLink, Terminal, MessageSquare, Clock, Loader2, ChevronDown, Sparkles, } from 'lucide-react';
3
+ import { useState, useCallback } from 'react';
4
+ import { History, Copy, ExternalLink, Terminal, MessageSquare, Clock, ChevronDown, Sparkles, } from 'lucide-react';
5
5
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuPortal, } from '../../ui/dropdown-menu.js';
6
6
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip.js';
7
7
  import { cn } from '../../../lib/utils.js';
8
8
  import { getAgentTypeIcon } from '../../common/feature-node/agent-type-icons.js';
9
+ import { useSessionsContext } from '../../../hooks/sessions-provider.js';
9
10
  const ACTIVE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
10
11
  const PREVIEW_COUNT = 3;
11
12
  function isSessionActive(session) {
@@ -56,88 +57,29 @@ async function copyToClipboard(text) {
56
57
  function stopNodeEvent(e) {
57
58
  e.stopPropagation();
58
59
  }
59
- export function FeatureSessionsDropdown({ repositoryPath, className, includeWorktrees, onCreateFromSession, }) {
60
- const [sessions, setSessions] = useState([]);
61
- const [loading, setLoading] = useState(false);
62
- const [fetched, setFetched] = useState(false);
63
- const [hasActiveSessions, setHasActiveSessions] = useState(false);
60
+ export function FeatureSessionsDropdown({ repositoryPath, className, onCreateFromSession, }) {
64
61
  const [expanded, setExpanded] = useState(false);
65
- const prevPathRef = useRef(repositoryPath);
66
- // Reset when path changes
67
- useEffect(() => {
68
- if (prevPathRef.current !== repositoryPath) {
69
- prevPathRef.current = repositoryPath;
70
- setSessions([]);
71
- setFetched(false);
72
- setHasActiveSessions(false);
73
- setExpanded(false);
74
- }
75
- }, [repositoryPath]);
76
- // Fetch sessions on mount. Fast because we only scan the matching project directory.
77
- // Populates count badge + active indicator, and pre-loads the dropdown.
78
- useEffect(() => {
79
- let cancelled = false;
80
- const params = new URLSearchParams({
81
- repositoryPath,
82
- limit: '10',
83
- ...(includeWorktrees && { includeWorktrees: 'true' }),
84
- });
85
- fetch(`/api/sessions?${params.toString()}`)
86
- .then((res) => (res.ok ? res.json() : null))
87
- .then((data) => {
88
- if (!cancelled && data?.sessions) {
89
- setSessions(data.sessions);
90
- setHasActiveSessions(data.sessions.some(isSessionActive));
91
- setFetched(true);
92
- }
93
- })
94
- .catch(() => undefined);
95
- return () => {
96
- cancelled = true;
97
- };
98
- }, [repositoryPath, includeWorktrees]);
99
- // Re-fetch on dropdown open if not already loaded (e.g. path changed)
100
- const doFetch = useCallback(async () => {
101
- if (fetched)
102
- return;
103
- setLoading(true);
104
- try {
105
- const params = new URLSearchParams({
106
- repositoryPath,
107
- limit: '10',
108
- ...(includeWorktrees && { includeWorktrees: 'true' }),
109
- });
110
- const res = await fetch(`/api/sessions?${params.toString()}`);
111
- if (res.ok) {
112
- const data = (await res.json());
113
- setSessions(data.sessions);
114
- setHasActiveSessions(data.sessions.some(isSessionActive));
115
- }
116
- }
117
- catch {
118
- // Silently fail
119
- }
120
- finally {
121
- setLoading(false);
122
- setFetched(true);
123
- }
124
- }, [repositoryPath, fetched, includeWorktrees]);
62
+ // Read sessions from the centralized SessionsProvider context.
63
+ // Sessions are batch-fetched every 30s — no per-instance HTTP calls.
64
+ const { getSessionsForPath, hasActiveSessions: hasActiveForPath } = useSessionsContext();
65
+ const sessions = getSessionsForPath(repositoryPath);
66
+ const active = hasActiveForPath(repositoryPath);
125
67
  const handleOpenChange = useCallback((open) => {
126
- if (open)
127
- void doFetch();
128
- }, [doFetch]);
68
+ if (!open)
69
+ setExpanded(false);
70
+ }, []);
129
71
  const visibleSessions = expanded ? sessions : sessions.slice(0, PREVIEW_COUNT);
130
72
  const hasMore = sessions.length > PREVIEW_COUNT;
131
- return (_jsxs(DropdownMenu, { modal: false, onOpenChange: handleOpenChange, children: [_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": "View sessions", "data-testid": "feature-node-sessions-button", className: cn('nodrag relative flex h-5 cursor-pointer items-center gap-0.5 rounded px-0.5 text-[10px] transition-colors', 'text-muted-foreground hover:text-foreground hover:bg-muted', className), onClick: stopNodeEvent, onPointerDown: stopNodeEvent, children: [_jsx(History, { className: "h-3 w-3 shrink-0" }), sessions.length > 0 ? (_jsx("span", { "data-testid": "feature-node-sessions-count", children: sessions.length })) : null, hasActiveSessions ? (_jsx("span", { className: "absolute -top-0.5 -right-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500" })) : null] }) }) }), _jsx(TooltipContent, { side: "top", children: hasActiveSessions ? 'Sessions (active)' : 'Sessions' })] }) }), _jsxs(DropdownMenuContent, { align: "start", side: "bottom", className: "w-80", onClick: stopNodeEvent, onPointerDown: stopNodeEvent, children: [_jsxs(DropdownMenuLabel, { className: "flex items-center gap-1.5 text-xs", children: [_jsx(History, { className: "h-3 w-3" }), "Agent Sessions"] }), _jsx(DropdownMenuSeparator, {}), loading ? (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-4 text-xs", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), "Loading..."] })) : sessions.length === 0 ? (_jsx("div", { className: "text-muted-foreground py-4 text-center text-xs", children: "No sessions found" })) : (_jsxs(_Fragment, { children: [visibleSessions.map((session) => (_jsx(SessionRow, { session: session, repositoryPath: repositoryPath, onCreateFromSession: onCreateFromSession }, session.id))), hasMore ? (_jsxs(DropdownMenuItem, { className: "text-muted-foreground justify-center gap-1 py-1.5 text-[10px]", onClick: (e) => {
73
+ return (_jsxs(DropdownMenu, { modal: false, onOpenChange: handleOpenChange, children: [_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": "View sessions", "data-testid": "feature-node-sessions-button", className: cn('nodrag relative flex h-5 cursor-pointer items-center gap-0.5 rounded px-0.5 text-[10px] transition-colors', 'text-muted-foreground hover:text-foreground hover:bg-muted', className), onClick: stopNodeEvent, onPointerDown: stopNodeEvent, children: [_jsx(History, { className: "h-3 w-3 shrink-0" }), sessions.length > 0 ? (_jsx("span", { "data-testid": "feature-node-sessions-count", children: sessions.length })) : null, active ? (_jsx("span", { className: "absolute -top-0.5 -right-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500" })) : null] }) }) }), _jsx(TooltipContent, { side: "top", children: active ? 'Sessions (active)' : 'Sessions' })] }) }), _jsxs(DropdownMenuContent, { align: "start", side: "bottom", className: "w-80", onClick: stopNodeEvent, onPointerDown: stopNodeEvent, children: [_jsxs(DropdownMenuLabel, { className: "flex items-center gap-1.5 text-xs", children: [_jsx(History, { className: "h-3 w-3" }), "Agent Sessions"] }), _jsx(DropdownMenuSeparator, {}), sessions.length === 0 ? (_jsx("div", { className: "text-muted-foreground py-4 text-center text-xs", children: "No sessions found" })) : (_jsxs(_Fragment, { children: [visibleSessions.map((session) => (_jsx(SessionRow, { session: session, repositoryPath: repositoryPath, onCreateFromSession: onCreateFromSession }, session.id))), hasMore ? (_jsxs(DropdownMenuItem, { className: "text-muted-foreground justify-center gap-1 py-1.5 text-[10px]", onClick: (e) => {
132
74
  e.preventDefault();
133
75
  setExpanded((v) => !v);
134
76
  }, children: [_jsx(ChevronDown, { className: cn('h-3 w-3 transition-transform', expanded && 'rotate-180') }), expanded ? 'Show less' : `Show ${sessions.length - PREVIEW_COUNT} more`] })) : null] }))] })] }));
135
77
  }
136
78
  // ── Session row component ─────────────────────────────────────────────
137
79
  function SessionRow({ session, repositoryPath, onCreateFromSession, }) {
138
- const active = isSessionActive(session);
80
+ const sessionActive = isSessionActive(session);
139
81
  const AgentIcon = getAgentTypeIcon(session.agentType);
140
- return (_jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { className: "flex items-start gap-2 py-2 pr-2", children: [_jsxs("div", { className: "relative mt-0.5 shrink-0", children: [_jsx(AgentIcon, { className: "h-4 w-4" }), active ? (_jsx("span", { className: "border-background absolute -right-0.5 -bottom-0.5 h-2 w-2 rounded-full border bg-emerald-500" })) : null] }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "truncate text-xs leading-tight", children: truncatePreview(session.preview) }), _jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-[10px] leading-tight", children: [_jsxs("span", { className: "flex items-center gap-0.5", children: [_jsx(MessageSquare, { className: "h-2.5 w-2.5" }), session.messageCount] }), session.firstMessageAt ? (_jsxs("span", { className: "flex items-center gap-0.5", children: [_jsx(Clock, { className: "h-2.5 w-2.5" }), new Date(session.firstMessageAt).toLocaleDateString()] })) : null, session.lastMessageAt ? (_jsx("span", { className: cn('ml-auto shrink-0', active ? 'font-medium text-emerald-600' : ''), children: formatRelativeTime(session.lastMessageAt) })) : null] })] })] }), _jsx(DropdownMenuPortal, { children: _jsxs(DropdownMenuSubContent, { onClick: stopNodeEvent, onPointerDown: stopNodeEvent, children: [_jsxs(DropdownMenuItem, { className: "gap-2 text-xs", onClick: () => void copyToClipboard(`claude --resume ${session.id} --project ${repositoryPath}`), children: [_jsx(Terminal, { className: "h-3.5 w-3.5" }), "Copy resume command"] }), _jsxs(DropdownMenuItem, { className: "gap-2 text-xs", onClick: () => void copyToClipboard(session.id), children: [_jsx(Copy, { className: "h-3.5 w-3.5" }), "Copy session ID"] }), _jsxs(DropdownMenuItem, { className: "gap-2 text-xs", onClick: () => {
82
+ return (_jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { className: "flex items-start gap-2 py-2 pr-2", children: [_jsxs("div", { className: "relative mt-0.5 shrink-0", children: [_jsx(AgentIcon, { className: "h-4 w-4" }), sessionActive ? (_jsx("span", { className: "border-background absolute -right-0.5 -bottom-0.5 h-2 w-2 rounded-full border bg-emerald-500" })) : null] }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "truncate text-xs leading-tight", children: truncatePreview(session.preview) }), _jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-[10px] leading-tight", children: [_jsxs("span", { className: "flex items-center gap-0.5", children: [_jsx(MessageSquare, { className: "h-2.5 w-2.5" }), session.messageCount] }), session.firstMessageAt ? (_jsxs("span", { className: "flex items-center gap-0.5", children: [_jsx(Clock, { className: "h-2.5 w-2.5" }), new Date(session.firstMessageAt).toLocaleDateString()] })) : null, session.lastMessageAt ? (_jsx("span", { className: cn('ml-auto shrink-0', sessionActive ? 'font-medium text-emerald-600' : ''), children: formatRelativeTime(session.lastMessageAt) })) : null] })] })] }), _jsx(DropdownMenuPortal, { children: _jsxs(DropdownMenuSubContent, { onClick: stopNodeEvent, onPointerDown: stopNodeEvent, children: [_jsxs(DropdownMenuItem, { className: "gap-2 text-xs", onClick: () => void copyToClipboard(`claude --resume ${session.id} --project ${repositoryPath}`), children: [_jsx(Terminal, { className: "h-3.5 w-3.5" }), "Copy resume command"] }), _jsxs(DropdownMenuItem, { className: "gap-2 text-xs", onClick: () => void copyToClipboard(session.id), children: [_jsx(Copy, { className: "h-3.5 w-3.5" }), "Copy session ID"] }), _jsxs(DropdownMenuItem, { className: "gap-2 text-xs", onClick: () => {
141
83
  const vscodeUri = `vscode://file${repositoryPath}`;
142
84
  window.open(vscodeUri, '_blank');
143
85
  }, children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Open in IDE"] }), onCreateFromSession && session.filePath ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 text-xs font-medium text-violet-700 focus:bg-violet-50 focus:text-violet-800", onClick: () => onCreateFromSession(session, session.filePath), children: [_jsx(Sparkles, { className: "h-3.5 w-3.5 text-violet-500" }), "Create feature from session"] })] })) : null] }) })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"feature-sessions-dropdown.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-sessions-dropdown.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AA4DtE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,uBAAuB,CAU9C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAEtD,eAAO,MAAM,YAAY,EAAE,KAE1B,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,KAEhC,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAEnB,CAAC"}
1
+ {"version":3,"file":"feature-sessions-dropdown.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-sessions-dropdown.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAsEtE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,uBAAuB,CAM9C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAEtD,eAAO,MAAM,YAAY,EAAE,KAE1B,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,KAEhC,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAEnB,CAAC"}
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { FeatureSessionsDropdown } from './feature-sessions-dropdown.js';
3
+ import { SessionsProvider } from '../../../hooks/sessions-provider.js';
4
+ const REPO_PATH = '/home/user/workspaces/my-project';
3
5
  const mockSessions = [
4
6
  {
5
7
  id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
@@ -8,7 +10,7 @@ const mockSessions = [
8
10
  firstMessageAt: new Date(Date.now() - 3_600_000).toISOString(),
9
11
  lastMessageAt: new Date(Date.now() - 1_800_000).toISOString(),
10
12
  createdAt: new Date(Date.now() - 3_600_000).toISOString(),
11
- projectPath: '~/workspaces/my-project',
13
+ projectPath: REPO_PATH,
12
14
  },
13
15
  {
14
16
  id: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
@@ -17,7 +19,7 @@ const mockSessions = [
17
19
  firstMessageAt: new Date(Date.now() - 86_400_000).toISOString(),
18
20
  lastMessageAt: new Date(Date.now() - 82_800_000).toISOString(),
19
21
  createdAt: new Date(Date.now() - 86_400_000).toISOString(),
20
- projectPath: '~/workspaces/my-project',
22
+ projectPath: REPO_PATH,
21
23
  },
22
24
  {
23
25
  id: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
@@ -26,7 +28,7 @@ const mockSessions = [
26
28
  firstMessageAt: new Date(Date.now() - 172_800_000).toISOString(),
27
29
  lastMessageAt: new Date(Date.now() - 169_200_000).toISOString(),
28
30
  createdAt: new Date(Date.now() - 172_800_000).toISOString(),
29
- projectPath: '~/workspaces/my-project',
31
+ projectPath: REPO_PATH,
30
32
  },
31
33
  ];
32
34
  const mockActiveSession = {
@@ -36,42 +38,41 @@ const mockActiveSession = {
36
38
  firstMessageAt: new Date(Date.now() - 180_000).toISOString(),
37
39
  lastMessageAt: new Date(Date.now() - 60_000).toISOString(), // 1 min ago — active
38
40
  createdAt: new Date(Date.now() - 180_000).toISOString(),
39
- projectPath: '~/workspaces/my-project',
41
+ projectPath: REPO_PATH,
40
42
  };
41
- function createFetchMock(sessions) {
43
+ /**
44
+ * Mock fetch for /api/sessions-batch, then wrap with SessionsProvider.
45
+ */
46
+ function createSessionsMock(sessions) {
42
47
  return (Story) => {
43
48
  const originalFetch = window.fetch;
44
49
  window.fetch = (async (input, init) => {
45
50
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
46
- if (url.includes('/api/sessions')) {
47
- return new Response(JSON.stringify({ sessions }), {
51
+ if (url.includes('/api/sessions-batch')) {
52
+ return new Response(JSON.stringify({ sessionsByPath: sessions }), {
48
53
  status: 200,
49
54
  headers: { 'Content-Type': 'application/json' },
50
55
  });
51
56
  }
52
57
  return originalFetch(input, init);
53
58
  });
54
- return _jsx(Story, {});
59
+ return (_jsx(SessionsProvider, { children: _jsx(Story, {}) }));
55
60
  };
56
61
  }
57
62
  const meta = {
58
63
  title: 'Composed/FeatureSessionsDropdown',
59
64
  component: FeatureSessionsDropdown,
60
65
  tags: ['autodocs'],
61
- parameters: {
62
- layout: 'centered',
63
- },
64
- args: {
65
- repositoryPath: '/home/user/workspaces/my-project',
66
- },
66
+ parameters: { layout: 'centered' },
67
+ args: { repositoryPath: REPO_PATH },
67
68
  };
68
69
  export default meta;
69
70
  export const WithSessions = {
70
- decorators: [createFetchMock(mockSessions)],
71
+ decorators: [createSessionsMock({ [REPO_PATH]: mockSessions })],
71
72
  };
72
73
  export const WithActiveSessions = {
73
- decorators: [createFetchMock([mockActiveSession, ...mockSessions])],
74
+ decorators: [createSessionsMock({ [REPO_PATH]: [mockActiveSession, ...mockSessions] })],
74
75
  };
75
76
  export const Empty = {
76
- decorators: [createFetchMock([])],
77
+ decorators: [createSessionsMock({ [REPO_PATH]: [] })],
77
78
  };
@@ -1 +1 @@
1
- {"version":3,"file":"control-center.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/control-center/control-center.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAG5E,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,YAAY,EAAE,IAAI,EAAE,CAAC;IACrB,kEAAkE;IAClE,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,kBAAkB,2CASvF"}
1
+ {"version":3,"file":"control-center.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/control-center/control-center.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAI5E,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,YAAY,EAAE,IAAI,EAAE,CAAC;IACrB,kEAAkE;IAClE,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,kBAAkB,2CAWvF"}
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { ReactFlowProvider } from '@xyflow/react';
4
+ import { SessionsProvider } from '../../../hooks/sessions-provider.js';
4
5
  import { ControlCenterInner } from './control-center-inner.js';
5
6
  export function ControlCenter({ initialNodes, initialEdges, drawer }) {
6
- return (_jsxs("div", { "data-testid": "control-center", className: "h-full w-full", children: [_jsx(ReactFlowProvider, { children: _jsx(ControlCenterInner, { initialNodes: initialNodes, initialEdges: initialEdges }) }), _jsx("div", { children: drawer }, "drawer")] }));
7
+ return (_jsxs("div", { "data-testid": "control-center", className: "h-full w-full", children: [_jsx(SessionsProvider, { children: _jsx(ReactFlowProvider, { children: _jsx(ControlCenterInner, { initialNodes: initialNodes, initialEdges: initialEdges }) }) }), _jsx("div", { children: drawer }, "drawer")] }));
7
8
  }