@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.
- package/dist/src/presentation/web/app/api/agent-events/route.js +1 -1
- package/dist/src/presentation/web/app/api/sessions/route.d.ts.map +1 -1
- package/dist/src/presentation/web/app/api/sessions/route.js +2 -268
- package/dist/src/presentation/web/app/api/sessions-batch/route.d.ts +17 -0
- package/dist/src/presentation/web/app/api/sessions-batch/route.d.ts.map +1 -0
- package/dist/src/presentation/web/app/api/sessions-batch/route.js +61 -0
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.js +15 -73
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.stories.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.stories.js +18 -17
- package/dist/src/presentation/web/components/features/control-center/control-center.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/control-center.js +2 -1
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +4 -1
- package/dist/src/presentation/web/components/layouts/app-sidebar/app-sidebar.d.ts.map +1 -1
- package/dist/src/presentation/web/components/layouts/app-sidebar/app-sidebar.js +32 -33
- package/dist/src/presentation/web/hooks/sessions-provider.d.ts +12 -0
- package/dist/src/presentation/web/hooks/sessions-provider.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/sessions-provider.js +57 -0
- package/dist/src/presentation/web/hooks/use-deploy-action.d.ts.map +1 -1
- package/dist/src/presentation/web/hooks/use-deploy-action.js +8 -54
- package/dist/src/presentation/web/lib/session-scanner.d.ts +27 -0
- package/dist/src/presentation/web/lib/session-scanner.d.ts.map +1 -0
- package/dist/src/presentation/web/lib/session-scanner.js +255 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-path-routes-manifest.json +1 -0
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +2 -2
- package/web/.next/required-server-files.json +2 -2
- package/web/.next/routes-manifest.json +6 -0
- package/web/.next/server/app/(dashboard)/@drawer/adopt/page/server-reference-manifest.json +28 -28
- package/web/.next/server/app/(dashboard)/@drawer/adopt/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/adopt/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/create/page/server-reference-manifest.json +28 -28
- package/web/.next/server/app/(dashboard)/@drawer/create/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/create/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
- package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/create/page/server-reference-manifest.json +28 -28
- package/web/.next/server/app/(dashboard)/create/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/create/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/page/server-reference-manifest.json +26 -26
- package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
- package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found/page/server-reference-manifest.json +3 -3
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/api/attachments/preview/route.js.nft.json +1 -1
- package/web/.next/server/app/api/evidence/route.js.nft.json +1 -1
- package/web/.next/server/app/api/graph-data/route.js.nft.json +1 -1
- package/web/.next/server/app/api/sessions/route.js +2 -3
- package/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/web/.next/server/app/api/sessions-batch/route/app-paths-manifest.json +3 -0
- package/web/.next/server/app/api/sessions-batch/route/build-manifest.json +11 -0
- package/web/.next/server/app/api/sessions-batch/route/server-reference-manifest.json +4 -0
- package/web/.next/server/app/api/sessions-batch/route.js +7 -0
- package/web/.next/server/app/api/sessions-batch/route.js.map +5 -0
- package/web/.next/server/app/api/sessions-batch/route.js.nft.json +1 -0
- package/web/.next/server/app/api/sessions-batch/route_client-reference-manifest.js +2 -0
- package/web/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills/page/server-reference-manifest.json +8 -8
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/tools/page/server-reference-manifest.json +8 -8
- package/web/.next/server/app/tools/page.js.nft.json +1 -1
- package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version/page/server-reference-manifest.json +3 -3
- package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app-paths-manifest.json +1 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +1 -1
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -1
- package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_sessions-batch_route_actions_4859f283.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__0d33c29e._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__0d33c29e._.js.map +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__2f61738a._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__2f61738a._.js.map +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__a402b567._.js +1 -1
- package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js +1 -1
- package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2138fa7e._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__357d99f9._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__3ef34e4c._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_05c23ad9._.js +1 -1
- package/web/.next/server/chunks/ssr/_05c23ad9._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_0c5f56e3._.js +2 -2
- package/web/.next/server/chunks/ssr/_0c5f56e3._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_16eb4fec._.js +1 -1
- package/web/.next/server/chunks/ssr/_16eb4fec._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_1b719e7f._.js +1 -1
- package/web/.next/server/chunks/ssr/_1b719e7f._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_37e8548b._.js +1 -1
- package/web/.next/server/chunks/ssr/_37e8548b._.js.map +1 -1
- package/web/.next/server/chunks/ssr/{_fe63a7f9._.js → _458e9a64._.js} +2 -2
- package/web/.next/server/chunks/ssr/{_fe63a7f9._.js.map → _458e9a64._.js.map} +1 -1
- package/web/.next/server/chunks/ssr/_5022e2b1._.js +4 -0
- package/web/.next/server/chunks/ssr/_5022e2b1._.js.map +1 -0
- package/web/.next/server/chunks/ssr/_55d763e2._.js +1 -1
- package/web/.next/server/chunks/ssr/_55d763e2._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_6256a985._.js +1 -1
- package/web/.next/server/chunks/ssr/_6256a985._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_64bdfc6f._.js +2 -2
- package/web/.next/server/chunks/ssr/_64bdfc6f._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_8fcc39d4._.js +1 -1
- package/web/.next/server/chunks/ssr/_b71645b4._.js +1 -1
- package/web/.next/server/chunks/ssr/_b71645b4._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_d8575088._.js +1 -1
- package/web/.next/server/chunks/ssr/_d8575088._.js.map +1 -1
- package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js +1 -1
- package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js.map +1 -1
- package/web/.next/server/chunks/ssr/{src_presentation_web_7b2fda40._.js → src_presentation_web_35159458._.js} +2 -2
- package/web/.next/server/chunks/ssr/{src_presentation_web_7b2fda40._.js.map → src_presentation_web_35159458._.js.map} +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_open-ide_ts_baaca5d5._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js.map +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +44 -44
- package/web/.next/static/chunks/{0137d4850cab3c45.js → 24b1c1e60fd3b7b5.js} +2 -2
- package/web/.next/static/chunks/{c731682077fbac4f.js → 3e7a130816229439.js} +1 -1
- package/web/.next/static/chunks/{7c5131e33516a325.js → 3f1b33498b472b00.js} +1 -1
- package/web/.next/static/chunks/{04869f1d3f5d9071.js → 4ef564fb1174e497.js} +1 -1
- package/web/.next/static/chunks/75834e430247b325.js +1 -0
- package/web/.next/static/chunks/79dc2e2f1c2ff519.js +1 -0
- package/web/.next/static/chunks/{063a24b49d9818a0.js → a086f8dfef2c3325.js} +1 -1
- package/web/.next/static/chunks/{48850e202dd814ac.js → a6363f73e05ccf47.js} +1 -1
- package/web/.next/static/chunks/{6f76e63ead3fac2e.js → b7126c0b3a97e77e.js} +1 -1
- package/web/.next/static/chunks/d3df6e6434e16519.js +1 -0
- package/web/.next/static/chunks/eaca60cc3ab0bf9f.js +2 -0
- package/web/.next/static/chunks/{9dad6769d10a32df.js → fe5d48f8ca483935.js} +1 -1
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_ff60e4a5.js +0 -3
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_ff60e4a5.js.map +0 -1
- package/web/.next/server/chunks/[externals]__448264a3._.js +0 -3
- package/web/.next/server/chunks/ssr/_4533d6f8._.js +0 -4
- package/web/.next/server/chunks/ssr/_4533d6f8._.js.map +0 -1
- package/web/.next/static/chunks/21e82fee1a7e1668.js +0 -1
- package/web/.next/static/chunks/682563e4503cbd58.js +0 -1
- package/web/.next/static/chunks/683b1d85e789c2eb.js +0 -2
- package/web/.next/static/chunks/d62ae5e449d87057.js +0 -1
- /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
- /package/web/.next/static/{zYKuE1zbe1UWwAJv5EVwg → 1CQHYZVn3VajyhdvnsCaw}/_buildManifest.js +0 -0
- /package/web/.next/static/{zYKuE1zbe1UWwAJv5EVwg → 1CQHYZVn3VajyhdvnsCaw}/_clientMiddlewareManifest.json +0 -0
- /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 =
|
|
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;
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts
CHANGED
|
@@ -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,
|
|
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
|
package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.js
CHANGED
|
@@ -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
|
|
4
|
-
import { History, Copy, ExternalLink, Terminal, MessageSquare, Clock,
|
|
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,
|
|
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
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
128
|
-
}, [
|
|
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,
|
|
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
|
|
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" }),
|
|
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;
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
41
|
+
projectPath: REPO_PATH,
|
|
40
42
|
};
|
|
41
|
-
|
|
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
|
-
|
|
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: [
|
|
71
|
+
decorators: [createSessionsMock({ [REPO_PATH]: mockSessions })],
|
|
71
72
|
};
|
|
72
73
|
export const WithActiveSessions = {
|
|
73
|
-
decorators: [
|
|
74
|
+
decorators: [createSessionsMock({ [REPO_PATH]: [mockActiveSession, ...mockSessions] })],
|
|
74
75
|
};
|
|
75
76
|
export const Empty = {
|
|
76
|
-
decorators: [
|
|
77
|
+
decorators: [createSessionsMock({ [REPO_PATH]: [] })],
|
|
77
78
|
};
|
package/dist/src/presentation/web/components/features/control-center/control-center.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
}
|