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
|
@@ -117,6 +117,8 @@ function createSession(overrides: Partial<OpencodeSession> & Pick<OpencodeSessio
|
|
|
117
117
|
rawSessionId: overrides.rawSessionId,
|
|
118
118
|
sourceSessionKey: overrides.sourceSessionKey ?? overrides.id,
|
|
119
119
|
readOnly: overrides.readOnly,
|
|
120
|
+
provider: overrides.provider,
|
|
121
|
+
providerRawId: overrides.providerRawId,
|
|
120
122
|
};
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -164,7 +166,9 @@ describe('useOpencodeSync', () => {
|
|
|
164
166
|
delete mockLocalStorage[key];
|
|
165
167
|
},
|
|
166
168
|
clear: () => {
|
|
167
|
-
Object.keys(mockLocalStorage)
|
|
169
|
+
for (const key of Object.keys(mockLocalStorage)) {
|
|
170
|
+
delete mockLocalStorage[key];
|
|
171
|
+
}
|
|
168
172
|
},
|
|
169
173
|
});
|
|
170
174
|
(getSseStatusSnapshot() as Map<string, unknown>).clear();
|
|
@@ -384,4 +388,320 @@ describe('useOpencodeSync', () => {
|
|
|
384
388
|
|
|
385
389
|
unmount();
|
|
386
390
|
});
|
|
391
|
+
|
|
392
|
+
it('keeps recently-idle child sessions nested instead of removing them immediately', () => {
|
|
393
|
+
const eventTimestamp = 75_000;
|
|
394
|
+
const parentSession = createSession({
|
|
395
|
+
id: 'local:parent',
|
|
396
|
+
rawSessionId: 'parent',
|
|
397
|
+
hostId: 'local',
|
|
398
|
+
hostLabel: 'Local',
|
|
399
|
+
hostKind: 'local',
|
|
400
|
+
children: [
|
|
401
|
+
{
|
|
402
|
+
id: 'local:child',
|
|
403
|
+
slug: 'child',
|
|
404
|
+
title: 'Child Session',
|
|
405
|
+
directory: '/tmp/project',
|
|
406
|
+
projectName: 'Project',
|
|
407
|
+
parentID: 'local:parent',
|
|
408
|
+
time: { created: 1000, updated: 2000 },
|
|
409
|
+
realTimeStatus: 'busy',
|
|
410
|
+
waitingForUser: false,
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
|
|
416
|
+
sessions: [parentSession],
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
act(() => {
|
|
420
|
+
eventSource.emitMessage({
|
|
421
|
+
type: 'session.status',
|
|
422
|
+
properties: {
|
|
423
|
+
sessionID: 'child',
|
|
424
|
+
status: { type: 'idle' },
|
|
425
|
+
},
|
|
426
|
+
timestamp: eventTimestamp,
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
|
|
431
|
+
const nextParent = data?.sessions.find((session) => session.id === 'local:parent');
|
|
432
|
+
|
|
433
|
+
expect(nextParent?.children).toHaveLength(1);
|
|
434
|
+
expect(nextParent?.children?.[0]?.id).toBe('local:child');
|
|
435
|
+
expect(nextParent?.children?.[0]?.realTimeStatus).toBe('idle');
|
|
436
|
+
expect(nextParent?.children?.[0]?.time.updated).toBe(eventTimestamp);
|
|
437
|
+
|
|
438
|
+
unmount();
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe('useOpencodeSync provider defaults', () => {
|
|
443
|
+
let mockLocalStorage: Record<string, string>;
|
|
444
|
+
|
|
445
|
+
beforeEach(() => {
|
|
446
|
+
mockLocalStorage = {};
|
|
447
|
+
MockEventSource.reset();
|
|
448
|
+
vi.useFakeTimers();
|
|
449
|
+
vi.stubGlobal('EventSource', MockEventSource as unknown as typeof EventSource);
|
|
450
|
+
vi.stubGlobal('localStorage', {
|
|
451
|
+
getItem: (key: string) => mockLocalStorage[key] || null,
|
|
452
|
+
setItem: (key: string, value: string) => {
|
|
453
|
+
mockLocalStorage[key] = value;
|
|
454
|
+
},
|
|
455
|
+
removeItem: (key: string) => {
|
|
456
|
+
delete mockLocalStorage[key];
|
|
457
|
+
},
|
|
458
|
+
clear: () => {
|
|
459
|
+
for (const key of Object.keys(mockLocalStorage)) {
|
|
460
|
+
delete mockLocalStorage[key];
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
(getSseStatusSnapshot() as Map<string, unknown>).clear();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
afterEach(() => {
|
|
468
|
+
vi.clearAllTimers();
|
|
469
|
+
vi.useRealTimers();
|
|
470
|
+
vi.unstubAllGlobals();
|
|
471
|
+
MockEventSource.reset();
|
|
472
|
+
(getSseStatusSnapshot() as Map<string, unknown>).clear();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('applies default provider opencode to new sessions from session.created event', () => {
|
|
476
|
+
const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
|
|
477
|
+
sessions: [],
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
act(() => {
|
|
481
|
+
eventSource.emitMessage({
|
|
482
|
+
type: 'session.created',
|
|
483
|
+
properties: {
|
|
484
|
+
info: {
|
|
485
|
+
id: 'ses_1744181234567_build',
|
|
486
|
+
slug: 'session_1744181234567_build',
|
|
487
|
+
title: 'New Session',
|
|
488
|
+
directory: '/tmp/project',
|
|
489
|
+
time: { created: Date.now(), updated: Date.now() },
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
timestamp: Date.now(),
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
|
|
497
|
+
const newSession = data?.sessions.find((s) => s.id === 'local:ses_1744181234567_build');
|
|
498
|
+
|
|
499
|
+
expect(newSession?.provider).toBe('opencode');
|
|
500
|
+
expect(newSession?.readOnly).toBe(false);
|
|
501
|
+
expect(newSession?.capabilities).toEqual({
|
|
502
|
+
openProject: true,
|
|
503
|
+
openEditor: true,
|
|
504
|
+
archive: true,
|
|
505
|
+
delete: true,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
unmount();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('applies default provider opencode to existing sessions on update', () => {
|
|
512
|
+
const existingSession = createSession({
|
|
513
|
+
id: 'local:ses_123',
|
|
514
|
+
rawSessionId: 'ses_123',
|
|
515
|
+
hostId: 'local',
|
|
516
|
+
hostLabel: 'Local',
|
|
517
|
+
hostKind: 'local',
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
|
|
521
|
+
sessions: [existingSession],
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
act(() => {
|
|
525
|
+
eventSource.emitMessage({
|
|
526
|
+
type: 'session.updated',
|
|
527
|
+
properties: {
|
|
528
|
+
info: {
|
|
529
|
+
id: 'ses_123',
|
|
530
|
+
slug: 'session_123',
|
|
531
|
+
title: 'Updated Session',
|
|
532
|
+
directory: '/tmp/project',
|
|
533
|
+
time: { created: 1000, updated: Date.now() },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
timestamp: Date.now(),
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
|
|
541
|
+
const updatedSession = data?.sessions.find((s) => s.id === 'local:ses_123');
|
|
542
|
+
|
|
543
|
+
expect(updatedSession?.provider).toBe('opencode');
|
|
544
|
+
expect(updatedSession?.readOnly).toBe(false);
|
|
545
|
+
expect(updatedSession?.capabilities).toEqual({
|
|
546
|
+
openProject: true,
|
|
547
|
+
openEditor: true,
|
|
548
|
+
archive: true,
|
|
549
|
+
delete: true,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
unmount();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('preserves explicit claude-code provider from event info', () => {
|
|
556
|
+
const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
|
|
557
|
+
sessions: [],
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
act(() => {
|
|
561
|
+
eventSource.emitMessage({
|
|
562
|
+
type: 'session.created',
|
|
563
|
+
properties: {
|
|
564
|
+
info: {
|
|
565
|
+
id: 'claude~550e8400-e29b-41d4-a716-446655440000',
|
|
566
|
+
slug: 'claude_session',
|
|
567
|
+
title: 'Claude Session',
|
|
568
|
+
directory: '/tmp/claude-project',
|
|
569
|
+
time: { created: Date.now(), updated: Date.now() },
|
|
570
|
+
provider: 'claude-code',
|
|
571
|
+
providerRawId: '550e8400-e29b-41d4-a716-446655440000',
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
timestamp: Date.now(),
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
|
|
579
|
+
const newSession = data?.sessions.find((s) => s.id === 'local:claude~550e8400-e29b-41d4-a716-446655440000');
|
|
580
|
+
|
|
581
|
+
expect(newSession?.provider).toBe('claude-code');
|
|
582
|
+
expect(newSession?.providerRawId).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
583
|
+
expect(newSession?.capabilities).toEqual({
|
|
584
|
+
openProject: true,
|
|
585
|
+
openEditor: false,
|
|
586
|
+
archive: true,
|
|
587
|
+
delete: true,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
unmount();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('preserves explicit readOnly true when specified', () => {
|
|
594
|
+
const existingSession = createSession({
|
|
595
|
+
id: 'local:ses_123',
|
|
596
|
+
rawSessionId: 'ses_123',
|
|
597
|
+
hostId: 'local',
|
|
598
|
+
readOnly: true,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
|
|
602
|
+
sessions: [existingSession],
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
act(() => {
|
|
606
|
+
eventSource.emitMessage({
|
|
607
|
+
type: 'session.status',
|
|
608
|
+
properties: {
|
|
609
|
+
sessionID: 'ses_123',
|
|
610
|
+
status: { type: 'busy' },
|
|
611
|
+
},
|
|
612
|
+
timestamp: Date.now(),
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
|
|
617
|
+
const updatedSession = data?.sessions.find((s) => s.id === 'local:ses_123');
|
|
618
|
+
|
|
619
|
+
expect(updatedSession?.readOnly).toBe(true);
|
|
620
|
+
|
|
621
|
+
unmount();
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('propagates provider and readOnly on session.status events for existing sessions', () => {
|
|
625
|
+
const localSession = createSession({
|
|
626
|
+
id: 'local:abc',
|
|
627
|
+
rawSessionId: 'abc',
|
|
628
|
+
hostId: 'local',
|
|
629
|
+
provider: 'claude-code',
|
|
630
|
+
readOnly: true,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
|
|
634
|
+
sessions: [localSession],
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
act(() => {
|
|
638
|
+
eventSource.emitMessage({
|
|
639
|
+
type: 'session.status',
|
|
640
|
+
properties: {
|
|
641
|
+
sessionID: 'abc',
|
|
642
|
+
status: { type: 'busy' },
|
|
643
|
+
},
|
|
644
|
+
timestamp: Date.now(),
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
|
|
649
|
+
const updatedSession = data?.sessions.find((s) => s.id === 'local:abc');
|
|
650
|
+
|
|
651
|
+
expect(updatedSession?.provider).toBe('claude-code');
|
|
652
|
+
expect(updatedSession?.readOnly).toBe(true);
|
|
653
|
+
|
|
654
|
+
unmount();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('keeps OpenCode SSE updates scoped to plain local ids when a Claude session shares the same raw uuid', () => {
|
|
658
|
+
const sharedUuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
659
|
+
const openCodeSession = createSession({
|
|
660
|
+
id: `local:${sharedUuid}`,
|
|
661
|
+
rawSessionId: sharedUuid,
|
|
662
|
+
hostId: 'local',
|
|
663
|
+
hostLabel: 'Local',
|
|
664
|
+
hostKind: 'local',
|
|
665
|
+
provider: 'opencode',
|
|
666
|
+
readOnly: false,
|
|
667
|
+
});
|
|
668
|
+
const claudeSession = createSession({
|
|
669
|
+
id: `local:claude~${sharedUuid}`,
|
|
670
|
+
rawSessionId: sharedUuid,
|
|
671
|
+
hostId: 'local',
|
|
672
|
+
hostLabel: 'Local',
|
|
673
|
+
hostKind: 'local',
|
|
674
|
+
provider: 'claude-code',
|
|
675
|
+
providerRawId: sharedUuid,
|
|
676
|
+
readOnly: true,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
|
|
680
|
+
sessions: [openCodeSession, claudeSession],
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
act(() => {
|
|
684
|
+
eventSource.emitMessage({
|
|
685
|
+
type: 'session.status',
|
|
686
|
+
properties: {
|
|
687
|
+
sessionID: sharedUuid,
|
|
688
|
+
status: { type: 'busy' },
|
|
689
|
+
},
|
|
690
|
+
timestamp: Date.now(),
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
|
|
695
|
+
const updatedOpenCodeSession = data?.sessions.find((s) => s.id === `local:${sharedUuid}`);
|
|
696
|
+
const updatedClaudeSession = data?.sessions.find((s) => s.id === `local:claude~${sharedUuid}`);
|
|
697
|
+
|
|
698
|
+
expect(updatedOpenCodeSession?.provider).toBe('opencode');
|
|
699
|
+
expect(updatedOpenCodeSession?.realTimeStatus).toBe('busy');
|
|
700
|
+
expect(updatedClaudeSession?.provider).toBe('claude-code');
|
|
701
|
+
expect(updatedClaudeSession?.realTimeStatus).toBe('idle');
|
|
702
|
+
expect(getSseStatusSnapshot().get(`local:${sharedUuid}`)?.status).toBe('busy');
|
|
703
|
+
expect(getSseStatusSnapshot().has(`local:claude~${sharedUuid}`)).toBe(false);
|
|
704
|
+
|
|
705
|
+
unmount();
|
|
706
|
+
});
|
|
387
707
|
});
|
|
@@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|
|
5
5
|
import { OpencodeEvent, OpencodeSession } from '@/types';
|
|
6
6
|
import { playAlertSound, playAttentionSound } from '@/lib/notificationSound';
|
|
7
7
|
import { composeSourceKey, getSessionIdFromSourceKey } from '@/lib/hostIdentity';
|
|
8
|
+
import { DEFAULT_PROVIDER_CONTEXT, getDefaultProviderContext } from '@/lib/session-providers/providerIds';
|
|
8
9
|
|
|
9
10
|
const WAITING_STORAGE_KEY = 'vibepulse:waiting-sessions:v2';
|
|
10
11
|
const WAITING_ENTER_DELAY_MS = 1500;
|
|
@@ -104,6 +105,7 @@ function toSourceKey(hostId: string, sessionId: string): string {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function normalizeSessionForSource(info: OpencodeSession, source: EventSourceContext): OpencodeSession {
|
|
108
|
+
const providerDefaults = getDefaultProviderContext(info.provider ?? DEFAULT_PROVIDER_CONTEXT.provider);
|
|
107
109
|
const rawSessionId = getSessionIdFromSourceKey(info.rawSessionId ?? info.id) ?? info.rawSessionId ?? info.id;
|
|
108
110
|
const sourceSessionKey = composeSourceKey(source.hostId, rawSessionId);
|
|
109
111
|
const rawParentId = info.parentID ? getSessionIdFromSourceKey(info.parentID) ?? info.parentID : info.parentID;
|
|
@@ -118,7 +120,10 @@ function normalizeSessionForSource(info: OpencodeSession, source: EventSourceCon
|
|
|
118
120
|
hostBaseUrl: source.hostBaseUrl,
|
|
119
121
|
rawSessionId,
|
|
120
122
|
sourceSessionKey,
|
|
121
|
-
readOnly:
|
|
123
|
+
readOnly: info.readOnly ?? providerDefaults.readOnly,
|
|
124
|
+
capabilities: info.capabilities ?? providerDefaults.capabilities,
|
|
125
|
+
provider: info.provider ?? providerDefaults.provider,
|
|
126
|
+
providerRawId: info.providerRawId ?? rawSessionId,
|
|
122
127
|
children: info.children?.map((child) =>
|
|
123
128
|
normalizeSessionForSource({
|
|
124
129
|
...child,
|
|
@@ -354,19 +359,24 @@ export function useOpencodeSync() {
|
|
|
354
359
|
}
|
|
355
360
|
|
|
356
361
|
const applyEvent = (s: OpencodeSession): OpencodeSession => {
|
|
362
|
+
const providerDefaults = getDefaultProviderContext(s.provider ?? DEFAULT_PROVIDER_CONTEXT.provider);
|
|
357
363
|
const baseSession: OpencodeSession = {
|
|
358
364
|
...s,
|
|
359
365
|
hostId: source.hostId,
|
|
360
366
|
hostLabel: source.hostLabel,
|
|
361
367
|
hostKind: source.hostKind,
|
|
362
368
|
hostBaseUrl: source.hostBaseUrl ?? s.hostBaseUrl,
|
|
363
|
-
readOnly:
|
|
369
|
+
readOnly: s.readOnly ?? providerDefaults.readOnly,
|
|
370
|
+
capabilities: s.capabilities ?? providerDefaults.capabilities,
|
|
371
|
+
provider: s.provider ?? providerDefaults.provider,
|
|
372
|
+
providerRawId: s.providerRawId ?? s.rawSessionId,
|
|
364
373
|
};
|
|
365
374
|
|
|
366
375
|
switch (event.type) {
|
|
367
376
|
case 'session.status': {
|
|
368
377
|
const statusType = event.properties?.status?.type as 'idle' | 'busy' | 'retry' | undefined;
|
|
369
378
|
if (!statusType) return baseSession;
|
|
379
|
+
const statusTimestamp = typeof event.timestamp === 'number' ? event.timestamp : Date.now();
|
|
370
380
|
recordSseStatus(s.id, statusType);
|
|
371
381
|
const isParentSession = !s.parentID;
|
|
372
382
|
const shouldAutoUnarchive = statusType === 'busy' || statusType === 'retry';
|
|
@@ -393,7 +403,9 @@ export function useOpencodeSync() {
|
|
|
393
403
|
}
|
|
394
404
|
return {
|
|
395
405
|
...baseSession,
|
|
396
|
-
time: shouldAutoUnarchive
|
|
406
|
+
time: shouldAutoUnarchive
|
|
407
|
+
? { ...(s.time || {}), updated: statusTimestamp, archived: undefined }
|
|
408
|
+
: { ...(s.time || {}), updated: statusTimestamp },
|
|
397
409
|
realTimeStatus: statusType,
|
|
398
410
|
waitingForUser:
|
|
399
411
|
statusType === 'retry'
|
|
@@ -448,15 +460,7 @@ export function useOpencodeSync() {
|
|
|
448
460
|
}
|
|
449
461
|
if (session.children?.some(c => c.id === sourceSessionId)) {
|
|
450
462
|
found = true;
|
|
451
|
-
|
|
452
|
-
// so it disappears from the UI without needing a full refetch, matching backend logic.
|
|
453
|
-
if (event.type === 'session.status' && event.properties?.status?.type === 'idle') {
|
|
454
|
-
return {
|
|
455
|
-
...session,
|
|
456
|
-
children: session.children.filter(c => c.id !== sourceSessionId)
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
463
|
+
|
|
460
464
|
return {
|
|
461
465
|
...session,
|
|
462
466
|
children: session.children.map(c => c.id === sourceSessionId ? applyEvent(c) : c)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
async function withTempHome<T>(fn: (homeDir: string) => Promise<T>): Promise<T> {
|
|
7
|
+
const homeDir = await mkdtemp(join(tmpdir(), 'vibepulse-claude-overrides-'));
|
|
8
|
+
const originalHome = process.env.HOME;
|
|
9
|
+
process.env.HOME = homeDir;
|
|
10
|
+
try {
|
|
11
|
+
return await fn(homeDir);
|
|
12
|
+
} finally {
|
|
13
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
14
|
+
else process.env.HOME = originalHome;
|
|
15
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('claudeSessionOverrides', () => {
|
|
20
|
+
it('persists archived and deleted Claude overrides to the local registry file', async () => {
|
|
21
|
+
await withTempHome(async (homeDir) => {
|
|
22
|
+
const mod = await import('./claudeSessionOverrides');
|
|
23
|
+
await mod.markClaudeSessionArchived('session-a', 100);
|
|
24
|
+
await mod.markClaudeSessionDeleted('session-b', 200);
|
|
25
|
+
|
|
26
|
+
const entries = await mod.listClaudeSessionOverrides();
|
|
27
|
+
expect(entries).toEqual(expect.arrayContaining([
|
|
28
|
+
expect.objectContaining({ sessionId: 'session-a', archivedAt: 100 }),
|
|
29
|
+
expect.objectContaining({ sessionId: 'session-b', deletedAt: 200 }),
|
|
30
|
+
]));
|
|
31
|
+
|
|
32
|
+
const content = await readFile(join(homeDir, '.config', 'vibepulse', 'claude-session-overrides.jsonc'), 'utf-8');
|
|
33
|
+
expect(content).toContain('session-a');
|
|
34
|
+
expect(content).toContain('session-b');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('preserves all entries under concurrent override writes', async () => {
|
|
39
|
+
await withTempHome(async () => {
|
|
40
|
+
const mod = await import('./claudeSessionOverrides');
|
|
41
|
+
|
|
42
|
+
await Promise.all([
|
|
43
|
+
mod.markClaudeSessionArchived('session-a', 100),
|
|
44
|
+
mod.markClaudeSessionDeleted('session-b', 200),
|
|
45
|
+
mod.markClaudeSessionArchived('session-c', 300),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const entries = await mod.listClaudeSessionOverrides();
|
|
49
|
+
expect(entries).toEqual(expect.arrayContaining([
|
|
50
|
+
expect.objectContaining({ sessionId: 'session-a', archivedAt: 100 }),
|
|
51
|
+
expect.objectContaining({ sessionId: 'session-b', deletedAt: 200 }),
|
|
52
|
+
expect.objectContaining({ sessionId: 'session-c', archivedAt: 300 }),
|
|
53
|
+
]));
|
|
54
|
+
expect(entries).toHaveLength(3);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('can clear archived state while preserving deleted state when restoring a Claude session', async () => {
|
|
59
|
+
await withTempHome(async () => {
|
|
60
|
+
const mod = await import('./claudeSessionOverrides');
|
|
61
|
+
await mod.markClaudeSessionArchived('session-a', 100);
|
|
62
|
+
await mod.markClaudeSessionDeleted('session-b', 200);
|
|
63
|
+
await mod.clearClaudeSessionArchived('session-a');
|
|
64
|
+
await mod.clearClaudeSessionArchived('session-b');
|
|
65
|
+
|
|
66
|
+
const entries = await mod.listClaudeSessionOverrides();
|
|
67
|
+
expect(entries).toEqual(expect.arrayContaining([
|
|
68
|
+
expect.objectContaining({ sessionId: 'session-b', deletedAt: 200, restoredAt: expect.any(Number) }),
|
|
69
|
+
]));
|
|
70
|
+
expect(entries).toEqual(expect.arrayContaining([
|
|
71
|
+
expect.objectContaining({ sessionId: 'session-a', restoredAt: expect.any(Number) }),
|
|
72
|
+
]));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { readFile, rename, writeFile } from 'fs/promises';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { parse, stringify } from 'comment-json';
|
|
6
|
+
|
|
7
|
+
export const VIBEPULSE_CONFIG_DIR = join(homedir(), '.config', 'vibepulse');
|
|
8
|
+
export const CLAUDE_SESSION_OVERRIDES_PATH = join(VIBEPULSE_CONFIG_DIR, 'claude-session-overrides.jsonc');
|
|
9
|
+
|
|
10
|
+
export interface ClaudeSessionOverrideEntry {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
archivedAt?: number;
|
|
13
|
+
deletedAt?: number;
|
|
14
|
+
restoredAt?: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ClaudeSessionOverridesFile {
|
|
19
|
+
version: number;
|
|
20
|
+
sessions: ClaudeSessionOverrideEntry[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let claudeOverrideWriteQueue: Promise<void> = Promise.resolve();
|
|
24
|
+
|
|
25
|
+
function ensureConfigDir(): void {
|
|
26
|
+
if (!existsSync(VIBEPULSE_CONFIG_DIR)) {
|
|
27
|
+
mkdirSync(VIBEPULSE_CONFIG_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultOverrides(): ClaudeSessionOverridesFile {
|
|
32
|
+
return {
|
|
33
|
+
version: 1,
|
|
34
|
+
sessions: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeEntry(entry: unknown): ClaudeSessionOverrideEntry | null {
|
|
39
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
40
|
+
const candidate = entry as Record<string, unknown>;
|
|
41
|
+
if (typeof candidate.sessionId !== 'string' || typeof candidate.updatedAt !== 'number') return null;
|
|
42
|
+
if (candidate.archivedAt !== undefined && typeof candidate.archivedAt !== 'number') return null;
|
|
43
|
+
if (candidate.deletedAt !== undefined && typeof candidate.deletedAt !== 'number') return null;
|
|
44
|
+
if (candidate.restoredAt !== undefined && typeof candidate.restoredAt !== 'number') return null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
sessionId: candidate.sessionId,
|
|
48
|
+
updatedAt: candidate.updatedAt,
|
|
49
|
+
...(typeof candidate.archivedAt === 'number' ? { archivedAt: candidate.archivedAt } : {}),
|
|
50
|
+
...(typeof candidate.deletedAt === 'number' ? { deletedAt: candidate.deletedAt } : {}),
|
|
51
|
+
...(typeof candidate.restoredAt === 'number' ? { restoredAt: candidate.restoredAt } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readOverridesFile(): Promise<ClaudeSessionOverridesFile> {
|
|
56
|
+
try {
|
|
57
|
+
ensureConfigDir();
|
|
58
|
+
if (!existsSync(CLAUDE_SESSION_OVERRIDES_PATH)) {
|
|
59
|
+
const initial = defaultOverrides();
|
|
60
|
+
await writeOverridesFile(initial);
|
|
61
|
+
return initial;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const raw = await readFile(CLAUDE_SESSION_OVERRIDES_PATH, 'utf-8');
|
|
65
|
+
const parsed = parse(raw, null, false) as unknown;
|
|
66
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
67
|
+
return defaultOverrides();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const file = parsed as Record<string, unknown>;
|
|
71
|
+
const sessions = Array.isArray(file.sessions)
|
|
72
|
+
? file.sessions.map(normalizeEntry).filter((entry): entry is ClaudeSessionOverrideEntry => entry !== null)
|
|
73
|
+
: [];
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
version: typeof file.version === 'number' ? file.version : 1,
|
|
77
|
+
sessions,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return defaultOverrides();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function writeOverridesFile(file: ClaudeSessionOverridesFile): Promise<void> {
|
|
85
|
+
ensureConfigDir();
|
|
86
|
+
const tempPath = `${CLAUDE_SESSION_OVERRIDES_PATH}.tmp`;
|
|
87
|
+
await writeFile(tempPath, stringify(file, null, 2), 'utf-8');
|
|
88
|
+
await rename(tempPath, CLAUDE_SESSION_OVERRIDES_PATH);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function listClaudeSessionOverrides(): Promise<ClaudeSessionOverrideEntry[]> {
|
|
92
|
+
const file = await readOverridesFile();
|
|
93
|
+
return file.sessions;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function getClaudeSessionOverride(sessionId: string): Promise<ClaudeSessionOverrideEntry | null> {
|
|
97
|
+
const file = await readOverridesFile();
|
|
98
|
+
return file.sessions.find((entry) => entry.sessionId === sessionId) ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function upsertClaudeSessionOverride(
|
|
102
|
+
sessionId: string,
|
|
103
|
+
mutate: (current: ClaudeSessionOverrideEntry | null, now: number) => ClaudeSessionOverrideEntry | null,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const run = async () => {
|
|
106
|
+
const file = await readOverridesFile();
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const current = file.sessions.find((entry) => entry.sessionId === sessionId) ?? null;
|
|
109
|
+
const next = mutate(current, now);
|
|
110
|
+
const withoutCurrent = file.sessions.filter((entry) => entry.sessionId !== sessionId);
|
|
111
|
+
file.sessions = next ? [...withoutCurrent, next] : withoutCurrent;
|
|
112
|
+
await writeOverridesFile(file);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const queued = claudeOverrideWriteQueue.then(run, run);
|
|
116
|
+
claudeOverrideWriteQueue = queued.then(() => undefined, () => undefined);
|
|
117
|
+
await queued;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function markClaudeSessionArchived(sessionId: string, archivedAt: number = Date.now()): Promise<void> {
|
|
121
|
+
await upsertClaudeSessionOverride(sessionId, (current, now) => ({
|
|
122
|
+
sessionId,
|
|
123
|
+
archivedAt,
|
|
124
|
+
deletedAt: current?.deletedAt,
|
|
125
|
+
restoredAt: undefined,
|
|
126
|
+
updatedAt: now,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function markClaudeSessionDeleted(sessionId: string, deletedAt: number = Date.now()): Promise<void> {
|
|
131
|
+
await upsertClaudeSessionOverride(sessionId, (_current, now) => ({
|
|
132
|
+
sessionId,
|
|
133
|
+
deletedAt,
|
|
134
|
+
restoredAt: undefined,
|
|
135
|
+
updatedAt: now,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function clearClaudeSessionDeleted(sessionId: string): Promise<void> {
|
|
140
|
+
await upsertClaudeSessionOverride(sessionId, (current, now) => {
|
|
141
|
+
if (!current) return null;
|
|
142
|
+
if (current.archivedAt === undefined) return null;
|
|
143
|
+
return {
|
|
144
|
+
sessionId,
|
|
145
|
+
archivedAt: current.archivedAt,
|
|
146
|
+
restoredAt: current.restoredAt,
|
|
147
|
+
updatedAt: now,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function clearClaudeSessionArchived(sessionId: string): Promise<void> {
|
|
153
|
+
await upsertClaudeSessionOverride(sessionId, (current, now) => {
|
|
154
|
+
if (!current) return null;
|
|
155
|
+
if (current.deletedAt !== undefined) {
|
|
156
|
+
return {
|
|
157
|
+
sessionId,
|
|
158
|
+
deletedAt: current.deletedAt,
|
|
159
|
+
restoredAt: now,
|
|
160
|
+
updatedAt: now,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
sessionId,
|
|
165
|
+
restoredAt: now,
|
|
166
|
+
updatedAt: now,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|