vibepulse 0.2.2 → 0.3.0-beta.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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/fallback-build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/routes-manifest.json +8 -0
- package/.next/server/app/_global-error/page.js +1 -1
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/server/app/api/node/sessions/route.js +5 -3
- package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
- package/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
- package/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
- package/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
- package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/sessions/route.js +4 -2
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +1 -0
- package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__98073dd6._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__d8e61048._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__f441109e._.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js.map +1 -0
- package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
- package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js.map → [root-of-the-server]__c91a8380._.js.map} +1 -1
- package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
- package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js.map +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +2 -2
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +8 -0
- package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/node/sessions/route.js +5 -3
- package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
- package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
- package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/sessions/route.js +4 -2
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
- package/.next/standalone/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +2 -2
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/b3bc362202331708.css +3 -0
- package/.next/standalone/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
- package/.next/standalone/README.md +29 -5
- package/.next/standalone/docs/session-status-detection.md +36 -0
- package/.next/standalone/docs/superpowers/specs/2026-04-09-claude-capability-alignment-design.md +39 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.test.ts +60 -1
- package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.ts +77 -22
- package/.next/standalone/src/app/api/node/sessions/route.test.ts +282 -0
- package/.next/standalone/src/app/api/node/sessions/route.ts +141 -17
- package/.next/standalone/src/app/api/opencode-events/route.test.ts +3 -1
- package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +101 -0
- package/.next/standalone/src/app/api/sessions/[id]/archive/route.ts +47 -12
- package/.next/standalone/src/app/api/sessions/[id]/delete/route.test.ts +92 -0
- package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +45 -10
- package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.test.ts +74 -0
- package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.ts +22 -2
- package/.next/standalone/src/app/api/sessions/[id]/restore/route.test.ts +186 -0
- package/.next/standalone/src/app/api/sessions/[id]/restore/route.ts +184 -0
- package/.next/standalone/src/app/api/sessions/route.test.ts +1889 -107
- package/.next/standalone/src/app/api/sessions/route.ts +365 -981
- package/.next/standalone/src/components/KanbanBoard.test.tsx +307 -1
- package/.next/standalone/src/components/KanbanBoard.tsx +105 -18
- package/.next/standalone/src/components/ProjectCard.test.tsx +416 -2
- package/.next/standalone/src/components/ProjectCard.tsx +238 -86
- package/.next/standalone/src/components/SessionCard.test.tsx +253 -2
- package/.next/standalone/src/components/SessionCard.tsx +182 -76
- package/.next/standalone/src/hooks/useOpencodeSync.test.ts +321 -1
- package/.next/standalone/src/hooks/useOpencodeSync.ts +16 -12
- package/.next/standalone/src/lib/claudeSessionOverrides.test.ts +75 -0
- package/.next/standalone/src/lib/claudeSessionOverrides.ts +169 -0
- package/.next/standalone/src/lib/session-providers/claudeCode.test.ts +2288 -0
- package/.next/standalone/src/lib/session-providers/claudeCode.ts +1083 -0
- package/.next/standalone/src/lib/session-providers/localAggregator.test.ts +322 -0
- package/.next/standalone/src/lib/session-providers/localAggregator.ts +302 -0
- package/.next/standalone/src/lib/session-providers/opencodeProvider.ts +723 -0
- package/.next/standalone/src/lib/session-providers/providerIds.test.ts +337 -0
- package/.next/standalone/src/lib/session-providers/providerIds.ts +176 -0
- package/.next/standalone/src/lib/session-providers/types.ts +131 -0
- package/.next/standalone/src/lib/transform.test.ts +253 -0
- package/.next/standalone/src/lib/transform.ts +96 -37
- package/.next/standalone/src/types/index.ts +23 -17
- package/.next/static/chunks/b3bc362202331708.css +3 -0
- package/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +9 -0
- package/README.md +29 -5
- package/package.json +1 -1
- package/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__2f981540._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__3745b314._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__6c428a24._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__73a00b88._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
- package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +0 -3
- package/.next/static/chunks/f42202943f6742e5.css +0 -3
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
discoverOpencodePortsWithMeta,
|
|
6
|
+
discoverOpencodeProcessCwdsWithoutPortWithMeta,
|
|
7
|
+
} from '@/lib/opencodeDiscovery';
|
|
8
|
+
import {
|
|
9
|
+
pruneSessionStickyStatusBlocked,
|
|
10
|
+
pruneSessionForceUnarchived,
|
|
11
|
+
shouldForceSessionUnarchived,
|
|
12
|
+
} from '@/lib/sessionArchiveOverrides';
|
|
13
|
+
import {
|
|
14
|
+
applyStickyStatusStabilization,
|
|
15
|
+
pruneStickyState,
|
|
16
|
+
shouldSkipSessionStatusStabilization,
|
|
17
|
+
} from './localAggregator';
|
|
18
|
+
import type {
|
|
19
|
+
ChildEntry,
|
|
20
|
+
EnrichedSession,
|
|
21
|
+
LocalSessionProvider,
|
|
22
|
+
MessagePart,
|
|
23
|
+
MessageStateStatus,
|
|
24
|
+
ProcessHint,
|
|
25
|
+
SessionLike,
|
|
26
|
+
SessionsRouteResult,
|
|
27
|
+
StableRealtimeStatus,
|
|
28
|
+
} from './types';
|
|
29
|
+
|
|
30
|
+
const CHILD_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
31
|
+
const CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS = 2 * 60 * 1000;
|
|
32
|
+
const CHILD_STATUS_MESSAGE_CHECK_LIMIT = 50;
|
|
33
|
+
const STALL_DETECTION_WINDOW_MS = 30 * 1000;
|
|
34
|
+
const GIT_COMMAND_TIMEOUT_MS = 1200;
|
|
35
|
+
const sessionListTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_LIST_TIMEOUT_MS', 6000);
|
|
36
|
+
const sessionStatusTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_STATUS_TIMEOUT_MS', 4000);
|
|
37
|
+
const sessionMessagesTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_MESSAGES_TIMEOUT_MS', 2500);
|
|
38
|
+
|
|
39
|
+
const WAITING_PART_STATUSES = new Set<string>([
|
|
40
|
+
'awaiting-input',
|
|
41
|
+
'awaiting_input',
|
|
42
|
+
'input-required',
|
|
43
|
+
'input_required',
|
|
44
|
+
'requires-input',
|
|
45
|
+
'requires_input',
|
|
46
|
+
'blocked',
|
|
47
|
+
'paused',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function readPositiveTimeoutEnv(name: string, fallback: number): number {
|
|
51
|
+
const raw = process.env[name];
|
|
52
|
+
const parsed = Number(raw);
|
|
53
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
54
|
+
return Math.floor(parsed);
|
|
55
|
+
}
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function withTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
60
|
+
const timeoutError = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
61
|
+
const timeoutController = new AbortController();
|
|
62
|
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
63
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
64
|
+
timeoutHandle = setTimeout(() => {
|
|
65
|
+
timeoutController.abort();
|
|
66
|
+
reject(timeoutError);
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const operationPromise = operation(timeoutController.signal).catch((error) => {
|
|
71
|
+
if (timeoutController.signal.aborted) {
|
|
72
|
+
throw timeoutError;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return Promise.race([operationPromise, timeoutPromise]).finally(() => {
|
|
79
|
+
if (timeoutHandle) {
|
|
80
|
+
clearTimeout(timeoutHandle);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizePartStatus(status: string): string {
|
|
86
|
+
return status.trim().toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isWaitingPartStatus(status: string): boolean {
|
|
90
|
+
return WAITING_PART_STATUSES.has(normalizePartStatus(status));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectPartStatuses(messages: Array<{ parts?: MessagePart[] }>): MessageStateStatus[] {
|
|
94
|
+
const partStatuses: MessageStateStatus[] = [];
|
|
95
|
+
|
|
96
|
+
for (const message of messages) {
|
|
97
|
+
for (const part of message.parts || []) {
|
|
98
|
+
const status = part?.state?.status;
|
|
99
|
+
if (typeof status === 'string') {
|
|
100
|
+
const normalized = normalizePartStatus(status);
|
|
101
|
+
if (normalized) {
|
|
102
|
+
partStatuses.push(normalized);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return partStatuses;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function fetchPartStatuses(
|
|
112
|
+
client: ReturnType<typeof createOpencodeClient>,
|
|
113
|
+
sessionId: string,
|
|
114
|
+
timeoutMs: number
|
|
115
|
+
): Promise<MessageStateStatus[]> {
|
|
116
|
+
const messagesResult = await withTimeout(
|
|
117
|
+
(signal) =>
|
|
118
|
+
client.session.messages({
|
|
119
|
+
path: { id: sessionId },
|
|
120
|
+
query: { limit: 8 },
|
|
121
|
+
signal,
|
|
122
|
+
}),
|
|
123
|
+
timeoutMs,
|
|
124
|
+
`session.messages(${sessionId})`
|
|
125
|
+
);
|
|
126
|
+
const messages = (messagesResult.data || []) as Array<{ parts?: MessagePart[] }>;
|
|
127
|
+
return collectPartStatuses(messages);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getUpdatedAt(session: { time?: { updated?: number; created?: number } }): number {
|
|
131
|
+
return session.time?.updated || session.time?.created || 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasRecentActivity(session: { time?: { updated?: number } }, now: number): boolean {
|
|
135
|
+
const updatedAt = session.time?.updated;
|
|
136
|
+
if (!updatedAt) return false;
|
|
137
|
+
return now - updatedAt <= STALL_DETECTION_WINDOW_MS;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function toChildEntry(
|
|
141
|
+
child: SessionLike,
|
|
142
|
+
status: 'idle' | 'busy' | 'retry',
|
|
143
|
+
waitingForUser = false
|
|
144
|
+
): ChildEntry {
|
|
145
|
+
return {
|
|
146
|
+
id: child.id,
|
|
147
|
+
slug: child.slug,
|
|
148
|
+
title: child.title,
|
|
149
|
+
directory: child.directory,
|
|
150
|
+
debugReason: child.debugReason,
|
|
151
|
+
parentID: child.parentID,
|
|
152
|
+
time: child.time,
|
|
153
|
+
realTimeStatus: status,
|
|
154
|
+
waitingForUser,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getProjectName(directory: string): string {
|
|
159
|
+
return path.basename(directory);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isGitRepo(directory: string): boolean {
|
|
163
|
+
try {
|
|
164
|
+
const result = execSync('git rev-parse --is-inside-work-tree', {
|
|
165
|
+
cwd: directory,
|
|
166
|
+
encoding: 'utf-8',
|
|
167
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
168
|
+
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
169
|
+
});
|
|
170
|
+
return result.trim() === 'true';
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getGitBranch(directory: string): string | null {
|
|
177
|
+
if (!isGitRepo(directory)) return null;
|
|
178
|
+
try {
|
|
179
|
+
const branch = execSync('git branch --show-current', {
|
|
180
|
+
cwd: directory,
|
|
181
|
+
encoding: 'utf-8',
|
|
182
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
183
|
+
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
184
|
+
});
|
|
185
|
+
return branch.trim() || null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sortChildEntries(children: ChildEntry[]): void {
|
|
192
|
+
children.sort((a, b) => {
|
|
193
|
+
const aActive = a.realTimeStatus === 'busy' || a.realTimeStatus === 'retry';
|
|
194
|
+
const bActive = b.realTimeStatus === 'busy' || b.realTimeStatus === 'retry';
|
|
195
|
+
|
|
196
|
+
if (aActive && !bActive) return -1;
|
|
197
|
+
if (!aActive && bActive) return 1;
|
|
198
|
+
|
|
199
|
+
const aTime = a.time?.updated || a.time?.created || 0;
|
|
200
|
+
const bTime = b.time?.updated || b.time?.created || 0;
|
|
201
|
+
return bTime - aTime;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function toErrorMessage(error: unknown): string {
|
|
206
|
+
return error instanceof Error ? error.message : String(error);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const opencodeLocalSessionProvider: LocalSessionProvider = {
|
|
210
|
+
id: 'opencode',
|
|
211
|
+
async getSessionsResult({ stickyBusyDelayMs }): Promise<SessionsRouteResult> {
|
|
212
|
+
const { processes: rawProcessHints, timedOut: processDiscoveryTimedOut } =
|
|
213
|
+
discoverOpencodeProcessCwdsWithoutPortWithMeta();
|
|
214
|
+
const processHintsByDirectory = new Map<string, ProcessHint>();
|
|
215
|
+
for (const process of rawProcessHints) {
|
|
216
|
+
if (!process.cwd || process.cwd.startsWith('/private/tmp/opencode')) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (processHintsByDirectory.has(process.cwd)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
processHintsByDirectory.set(process.cwd, {
|
|
223
|
+
pid: process.pid,
|
|
224
|
+
directory: process.cwd,
|
|
225
|
+
projectName: getProjectName(process.cwd),
|
|
226
|
+
reason: 'process_without_api_port',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { ports, timedOut: portDiscoveryTimedOut } = discoverOpencodePortsWithMeta();
|
|
231
|
+
|
|
232
|
+
if (!ports.length) {
|
|
233
|
+
const processHints = Array.from(processHintsByDirectory.values());
|
|
234
|
+
|
|
235
|
+
if (portDiscoveryTimedOut || processDiscoveryTimedOut) {
|
|
236
|
+
return {
|
|
237
|
+
payload: {
|
|
238
|
+
error: 'OpenCode discovery timed out',
|
|
239
|
+
hint: 'Host process discovery exceeded timeout. Retry shortly, or increase OPENCODE_DISCOVERY_TIMEOUT_MS.',
|
|
240
|
+
...(processHints.length > 0 ? { processHints } : {}),
|
|
241
|
+
},
|
|
242
|
+
status: 503,
|
|
243
|
+
sourceMeta: {
|
|
244
|
+
online: false,
|
|
245
|
+
degraded: true,
|
|
246
|
+
reason: 'OpenCode discovery timed out',
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (processHints.length > 0) {
|
|
252
|
+
return {
|
|
253
|
+
payload: { sessions: [], processHints },
|
|
254
|
+
sourceMeta: {
|
|
255
|
+
online: false,
|
|
256
|
+
reason: 'OpenCode server not found',
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
payload: {
|
|
263
|
+
error: 'OpenCode server not found',
|
|
264
|
+
hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).',
|
|
265
|
+
},
|
|
266
|
+
status: 503,
|
|
267
|
+
sourceMeta: {
|
|
268
|
+
online: false,
|
|
269
|
+
reason: 'OpenCode server not found',
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const results = await Promise.allSettled(ports.map(async (port) => {
|
|
276
|
+
const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
|
|
277
|
+
const sessionsResult = await withTimeout(
|
|
278
|
+
(signal) => client.session.list({ signal }),
|
|
279
|
+
sessionListTimeoutMs,
|
|
280
|
+
`session.list(${port})`
|
|
281
|
+
);
|
|
282
|
+
const statusResult = await withTimeout(
|
|
283
|
+
(signal) => client.session.status({ signal }),
|
|
284
|
+
sessionStatusTimeoutMs,
|
|
285
|
+
`session.status(${port})`
|
|
286
|
+
).catch(() => ({ data: {} }));
|
|
287
|
+
return { port, client, sessions: sessionsResult.data || [], status: statusResult.data || {} };
|
|
288
|
+
}));
|
|
289
|
+
|
|
290
|
+
const allSessions: SessionLike[] = [];
|
|
291
|
+
const statusMap: Record<string, { type: StableRealtimeStatus }> = {};
|
|
292
|
+
const clientByPort: Record<number, ReturnType<typeof createOpencodeClient>> = {};
|
|
293
|
+
const sessionPortMap: Record<string, number> = {};
|
|
294
|
+
const failedPorts: Array<{ port: number; reason: string }> = [];
|
|
295
|
+
|
|
296
|
+
for (let i = 0; i < results.length; i++) {
|
|
297
|
+
const result = results[i];
|
|
298
|
+
const port = ports[i];
|
|
299
|
+
if (result.status !== 'fulfilled') {
|
|
300
|
+
failedPorts.push({
|
|
301
|
+
port,
|
|
302
|
+
reason: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
allSessions.push(...(result.value.sessions as SessionLike[]));
|
|
307
|
+
Object.assign(statusMap, result.value.status);
|
|
308
|
+
clientByPort[result.value.port] = result.value.client;
|
|
309
|
+
for (const session of result.value.sessions as SessionLike[]) {
|
|
310
|
+
if (!(session.id in sessionPortMap)) {
|
|
311
|
+
sessionPortMap[session.id] = result.value.port;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const sessions: SessionLike[] = [];
|
|
317
|
+
const seen = new Set<string>();
|
|
318
|
+
for (const session of allSessions) {
|
|
319
|
+
if (seen.has(session.id)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
seen.add(session.id);
|
|
323
|
+
sessions.push(session);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const parentSessions = sessions.filter((session) => !session.parentID);
|
|
327
|
+
const childSessions = sessions.filter((session) => !!session.parentID);
|
|
328
|
+
|
|
329
|
+
const lifecycleNow = Date.now();
|
|
330
|
+
pruneSessionForceUnarchived(lifecycleNow);
|
|
331
|
+
pruneSessionStickyStatusBlocked(lifecycleNow);
|
|
332
|
+
|
|
333
|
+
for (const session of parentSessions) {
|
|
334
|
+
if (session.time?.archived !== undefined && shouldForceSessionUnarchived(session.id, lifecycleNow)) {
|
|
335
|
+
session.time = {
|
|
336
|
+
...session.time,
|
|
337
|
+
archived: undefined,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const child of childSessions) {
|
|
343
|
+
if (child.time?.archived !== undefined && shouldForceSessionUnarchived(child.id, lifecycleNow)) {
|
|
344
|
+
child.time = {
|
|
345
|
+
...child.time,
|
|
346
|
+
archived: undefined,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (results.length > 0 && failedPorts.length === results.length) {
|
|
352
|
+
pruneStickyState(Date.now(), new Set<string>());
|
|
353
|
+
return {
|
|
354
|
+
payload: {
|
|
355
|
+
error: 'Failed to fetch sessions from OpenCode ports',
|
|
356
|
+
hint: 'All discovered OpenCode API ports timed out or failed. Retry shortly or increase OPENCODE_SESSIONS_LIST_TIMEOUT_MS.',
|
|
357
|
+
failedPorts,
|
|
358
|
+
},
|
|
359
|
+
status: 503,
|
|
360
|
+
sourceMeta: {
|
|
361
|
+
online: false,
|
|
362
|
+
degraded: true,
|
|
363
|
+
reason: 'Failed to fetch sessions from OpenCode ports',
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (failedPorts.length > 0 && parentSessions.length === 0 && childSessions.length === 0) {
|
|
369
|
+
pruneStickyState(Date.now(), new Set<string>());
|
|
370
|
+
return {
|
|
371
|
+
payload: {
|
|
372
|
+
sessions: [],
|
|
373
|
+
processHints: Array.from(processHintsByDirectory.values()),
|
|
374
|
+
failedPorts,
|
|
375
|
+
degraded: true,
|
|
376
|
+
},
|
|
377
|
+
sourceMeta: {
|
|
378
|
+
online: true,
|
|
379
|
+
degraded: true,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const enrichedSessions: EnrichedSession[] = parentSessions.map((session) => ({
|
|
385
|
+
...session,
|
|
386
|
+
projectName: getProjectName(session.directory),
|
|
387
|
+
branch: getGitBranch(session.directory),
|
|
388
|
+
realTimeStatus: statusMap[session.id]?.type || 'idle',
|
|
389
|
+
waitingForUser: false,
|
|
390
|
+
children: [],
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
const parentById = new Map(enrichedSessions.map((session) => [session.id, session]));
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
const unresolvedChildren: Array<{ parentId: string; child: SessionLike; childUpdatedAt: number }> = [];
|
|
396
|
+
|
|
397
|
+
for (const child of childSessions) {
|
|
398
|
+
let parent = child.parentID ? parentById.get(child.parentID) : undefined;
|
|
399
|
+
|
|
400
|
+
if (!parent) {
|
|
401
|
+
const candidates = enrichedSessions
|
|
402
|
+
.filter((session) => session.directory === child.directory)
|
|
403
|
+
.sort((a, b) => getUpdatedAt(b) - getUpdatedAt(a));
|
|
404
|
+
|
|
405
|
+
parent =
|
|
406
|
+
candidates.find((session) => session.realTimeStatus === 'busy' || session.realTimeStatus === 'retry') ||
|
|
407
|
+
candidates[0];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!parent) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const statusFromMap = statusMap[child.id]?.type;
|
|
415
|
+
const childUpdatedAt = getUpdatedAt(child);
|
|
416
|
+
const isRecent = childUpdatedAt > 0 && now - childUpdatedAt <= CHILD_ACTIVE_WINDOW_MS;
|
|
417
|
+
const shouldSkipArchivedChild = !!child.time?.archived && !statusFromMap && !isRecent;
|
|
418
|
+
|
|
419
|
+
if (shouldSkipArchivedChild) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (statusFromMap && statusFromMap !== 'idle') {
|
|
424
|
+
parent.children.push(toChildEntry(child, statusFromMap));
|
|
425
|
+
} else if (isRecent) {
|
|
426
|
+
if (unresolvedChildren.length < CHILD_STATUS_MESSAGE_CHECK_LIMIT) {
|
|
427
|
+
unresolvedChildren.push({ parentId: parent.id, child, childUpdatedAt });
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (unresolvedChildren.length > 0) {
|
|
435
|
+
const unresolvedChecks = await Promise.allSettled(
|
|
436
|
+
unresolvedChildren.map(async ({ parentId, child, childUpdatedAt }) => {
|
|
437
|
+
const port = sessionPortMap[child.id] ?? sessionPortMap[parentId];
|
|
438
|
+
const client = port ? clientByPort[port] : undefined;
|
|
439
|
+
const assumeBusyForUnknown = childUpdatedAt > 0 && now - childUpdatedAt <= CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS;
|
|
440
|
+
if (!client) {
|
|
441
|
+
return {
|
|
442
|
+
parentId,
|
|
443
|
+
child,
|
|
444
|
+
childStatus: assumeBusyForUnknown ? ('busy' as const) : ('idle' as const),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const partStatuses = await fetchPartStatuses(client, child.id, sessionMessagesTimeoutMs);
|
|
450
|
+
const hasRunningState = partStatuses.some((status) => status === 'running');
|
|
451
|
+
const hasWaitingState = !hasRunningState && partStatuses.some(isWaitingPartStatus);
|
|
452
|
+
const hasActiveState = hasWaitingState || hasRunningState;
|
|
453
|
+
const recentlyActive = childUpdatedAt > 0 && now - childUpdatedAt <= 5 * 60 * 1000;
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
parentId,
|
|
457
|
+
child,
|
|
458
|
+
childWaitingForUser: hasWaitingState,
|
|
459
|
+
childStatus: hasActiveState
|
|
460
|
+
? ('busy' as const)
|
|
461
|
+
: recentlyActive || assumeBusyForUnknown
|
|
462
|
+
? ('busy' as const)
|
|
463
|
+
: ('idle' as const),
|
|
464
|
+
};
|
|
465
|
+
} catch {
|
|
466
|
+
return {
|
|
467
|
+
parentId,
|
|
468
|
+
child,
|
|
469
|
+
childWaitingForUser: false,
|
|
470
|
+
childStatus: assumeBusyForUnknown ? ('busy' as const) : ('idle' as const),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
})
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
for (const check of unresolvedChecks) {
|
|
477
|
+
if (check.status !== 'fulfilled') continue;
|
|
478
|
+
if (check.value.childStatus === 'idle') continue;
|
|
479
|
+
const parent = parentById.get(check.value.parentId);
|
|
480
|
+
if (!parent) continue;
|
|
481
|
+
parent.children.push(toChildEntry(check.value.child, check.value.childStatus, check.value.childWaitingForUser));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const parentStatusFallbackCandidates = enrichedSessions
|
|
486
|
+
.filter((session) => {
|
|
487
|
+
if (session.realTimeStatus !== 'idle') return false;
|
|
488
|
+
const updatedAt = getUpdatedAt(session);
|
|
489
|
+
if (updatedAt > 0 && now - updatedAt <= CHILD_ACTIVE_WINDOW_MS) return true;
|
|
490
|
+
return !!session.time?.archived;
|
|
491
|
+
})
|
|
492
|
+
.sort((a, b) => getUpdatedAt(b) - getUpdatedAt(a))
|
|
493
|
+
.slice(0, CHILD_STATUS_MESSAGE_CHECK_LIMIT);
|
|
494
|
+
|
|
495
|
+
if (parentStatusFallbackCandidates.length > 0) {
|
|
496
|
+
const parentFallbackChecks = await Promise.allSettled(
|
|
497
|
+
parentStatusFallbackCandidates.map(async (session) => {
|
|
498
|
+
const updatedAt = getUpdatedAt(session);
|
|
499
|
+
const assumeBusyForUnknown = updatedAt > 0 && now - updatedAt <= CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS;
|
|
500
|
+
const port = sessionPortMap[session.id];
|
|
501
|
+
const client = port ? clientByPort[port] : undefined;
|
|
502
|
+
|
|
503
|
+
if (!client) {
|
|
504
|
+
return {
|
|
505
|
+
sessionId: session.id,
|
|
506
|
+
status: assumeBusyForUnknown ? ('busy' as const) : ('idle' as const),
|
|
507
|
+
waitingForUser: false,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const partStatuses = await fetchPartStatuses(client, session.id, sessionMessagesTimeoutMs);
|
|
513
|
+
const hasRunningState = partStatuses.some((status) => status === 'running');
|
|
514
|
+
const hasWaitingState = !hasRunningState && partStatuses.some(isWaitingPartStatus);
|
|
515
|
+
const hasCompletedState =
|
|
516
|
+
partStatuses.length > 0 && partStatuses.every((status) => status === 'completed');
|
|
517
|
+
const recentlyActive = hasRecentActivity(session, now);
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
sessionId: session.id,
|
|
521
|
+
status: hasRunningState || hasWaitingState
|
|
522
|
+
? ('busy' as const)
|
|
523
|
+
: hasCompletedState && !recentlyActive
|
|
524
|
+
? ('idle' as const)
|
|
525
|
+
: assumeBusyForUnknown || recentlyActive
|
|
526
|
+
? ('busy' as const)
|
|
527
|
+
: ('idle' as const),
|
|
528
|
+
waitingForUser: hasWaitingState,
|
|
529
|
+
};
|
|
530
|
+
} catch {
|
|
531
|
+
return {
|
|
532
|
+
sessionId: session.id,
|
|
533
|
+
status: assumeBusyForUnknown ? ('busy' as const) : ('idle' as const),
|
|
534
|
+
waitingForUser: false,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
for (const check of parentFallbackChecks) {
|
|
541
|
+
if (check.status !== 'fulfilled') continue;
|
|
542
|
+
if (check.value.status === 'idle') continue;
|
|
543
|
+
const session = parentById.get(check.value.sessionId);
|
|
544
|
+
if (!session) continue;
|
|
545
|
+
session.realTimeStatus = check.value.status;
|
|
546
|
+
if (check.value.waitingForUser) {
|
|
547
|
+
session.waitingForUser = true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (const session of enrichedSessions) {
|
|
553
|
+
if (session.children.length > 0) {
|
|
554
|
+
sortChildEntries(session.children);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const sessionsForInteractionChecks = enrichedSessions.filter(
|
|
559
|
+
(session) =>
|
|
560
|
+
session.realTimeStatus === 'busy' ||
|
|
561
|
+
!!session.time?.archived ||
|
|
562
|
+
session.children.some((child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry')
|
|
563
|
+
);
|
|
564
|
+
if (sessionsForInteractionChecks.length > 0) {
|
|
565
|
+
const pendingChecks = await Promise.allSettled(
|
|
566
|
+
sessionsForInteractionChecks.map(async (session) => {
|
|
567
|
+
const port = sessionPortMap[session.id];
|
|
568
|
+
const client = port ? clientByPort[port] : undefined;
|
|
569
|
+
if (!client) {
|
|
570
|
+
return {
|
|
571
|
+
sessionId: session.id,
|
|
572
|
+
parentWaiting: false,
|
|
573
|
+
waiting: false,
|
|
574
|
+
running: false,
|
|
575
|
+
waitingChildIds: new Set<string>(),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const partStatuses = await fetchPartStatuses(client, session.id, sessionMessagesTimeoutMs);
|
|
581
|
+
const hasRunning = partStatuses.some((status) => status === 'running');
|
|
582
|
+
const hasInteractionWait = !hasRunning && partStatuses.some(isWaitingPartStatus);
|
|
583
|
+
|
|
584
|
+
const childStateChecks = await Promise.allSettled(
|
|
585
|
+
session.children
|
|
586
|
+
.filter((child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry')
|
|
587
|
+
.map(async (child) => {
|
|
588
|
+
const childPort = sessionPortMap[child.id] ?? sessionPortMap[session.id];
|
|
589
|
+
const childClient = childPort ? clientByPort[childPort] : undefined;
|
|
590
|
+
if (!childClient) {
|
|
591
|
+
return { childId: child.id, waiting: false };
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const childStatuses = await fetchPartStatuses(childClient, child.id, sessionMessagesTimeoutMs);
|
|
595
|
+
const childHasRunning = childStatuses.some((status) => status === 'running');
|
|
596
|
+
return {
|
|
597
|
+
childId: child.id,
|
|
598
|
+
waiting: !childHasRunning && childStatuses.some(isWaitingPartStatus),
|
|
599
|
+
};
|
|
600
|
+
} catch {
|
|
601
|
+
return { childId: child.id, waiting: false };
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const waitingChildIds = new Set(
|
|
607
|
+
childStateChecks
|
|
608
|
+
.filter((result): result is PromiseFulfilledResult<{ childId: string; waiting: boolean }> => result.status === 'fulfilled')
|
|
609
|
+
.filter((result) => result.value.waiting)
|
|
610
|
+
.map((result) => result.value.childId)
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const hasWaitingChildren =
|
|
614
|
+
waitingChildIds.size > 0 ||
|
|
615
|
+
session.children.some((child) => child.waitingForUser || child.realTimeStatus === 'retry');
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
sessionId: session.id,
|
|
619
|
+
parentWaiting: hasInteractionWait,
|
|
620
|
+
waiting: hasInteractionWait || hasWaitingChildren,
|
|
621
|
+
running: hasRunning,
|
|
622
|
+
waitingChildIds,
|
|
623
|
+
};
|
|
624
|
+
} catch {
|
|
625
|
+
return {
|
|
626
|
+
sessionId: session.id,
|
|
627
|
+
parentWaiting: false,
|
|
628
|
+
waiting: false,
|
|
629
|
+
running: false,
|
|
630
|
+
waitingChildIds: new Set<string>(),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
for (const result of pendingChecks) {
|
|
637
|
+
if (result.status === 'fulfilled') {
|
|
638
|
+
const session = enrichedSessions.find((candidate) => candidate.id === result.value.sessionId);
|
|
639
|
+
if (!session) continue;
|
|
640
|
+
for (const child of session.children) {
|
|
641
|
+
if (result.value.waitingChildIds.has(child.id)) {
|
|
642
|
+
child.waitingForUser = true;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (result.value.running) {
|
|
646
|
+
session.realTimeStatus = 'busy';
|
|
647
|
+
}
|
|
648
|
+
if (result.value.parentWaiting) {
|
|
649
|
+
session.waitingForUser = true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const stickyNow = Date.now();
|
|
656
|
+
const activeStickyIds = new Set<string>();
|
|
657
|
+
|
|
658
|
+
for (const session of enrichedSessions) {
|
|
659
|
+
activeStickyIds.add(session.id);
|
|
660
|
+
for (const child of session.children) {
|
|
661
|
+
activeStickyIds.add(`child:${child.id}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
for (const session of enrichedSessions) {
|
|
666
|
+
if (shouldSkipSessionStatusStabilization(session, stickyNow)) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
applyStickyStatusStabilization(session, stickyNow, stickyBusyDelayMs);
|
|
671
|
+
}
|
|
672
|
+
pruneStickyState(stickyNow, activeStickyIds);
|
|
673
|
+
|
|
674
|
+
const knownDirectories = new Set<string>();
|
|
675
|
+
for (const session of sessions) {
|
|
676
|
+
if (session.directory) {
|
|
677
|
+
knownDirectories.add(session.directory);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const processHints = Array.from(processHintsByDirectory.values()).filter(
|
|
682
|
+
(hint) => !knownDirectories.has(hint.directory)
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
const payload = {
|
|
686
|
+
sessions: enrichedSessions,
|
|
687
|
+
processHints,
|
|
688
|
+
} as NonNullable<SessionsRouteResult['payload']>;
|
|
689
|
+
|
|
690
|
+
if (failedPorts.length > 0) {
|
|
691
|
+
payload.failedPorts = failedPorts;
|
|
692
|
+
payload.degraded = true;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
payload,
|
|
697
|
+
sourceMeta: {
|
|
698
|
+
online: true,
|
|
699
|
+
...(failedPorts.length > 0 ? { degraded: true } : {}),
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error('Error fetching sessions:', error);
|
|
704
|
+
return {
|
|
705
|
+
payload: {
|
|
706
|
+
error: 'Failed to fetch sessions',
|
|
707
|
+
details: error instanceof Error ? error.message : String(error),
|
|
708
|
+
hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).',
|
|
709
|
+
},
|
|
710
|
+
status: 500,
|
|
711
|
+
sourceMeta: {
|
|
712
|
+
online: false,
|
|
713
|
+
degraded: true,
|
|
714
|
+
reason: 'Failed to fetch sessions',
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
export function createOpencodeLocalSessionProvider(): LocalSessionProvider {
|
|
722
|
+
return opencodeLocalSessionProvider;
|
|
723
|
+
}
|