diff --git a/src/crates/assembly/core/src/service/workspace/service.rs b/src/crates/assembly/core/src/service/workspace/service.rs index 61bd993e49..9dd4e505ed 100644 --- a/src/crates/assembly/core/src/service/workspace/service.rs +++ b/src/crates/assembly/core/src/service/workspace/service.rs @@ -2138,7 +2138,26 @@ impl WorkspaceService { let assistant_root = self.path_manager.assistant_workspace_base_dir(None); if path.parent()? != assistant_root { - return None; + // BitFun → OpenBitFun rebranding left pre-migration assistant + // workspaces rooted at the legacy hidden directory. Keep them + // recognizable so delete/reset commands can still operate on them. + let legacy_root = self + .path_manager + .product_home_dir() + .parent() + .map(|home| home.join(".bitfun").join("personal_assistant"))?; + if path.parent()? != legacy_root { + return None; + } + // The pre-rebranding default assistant workspace (no id suffix) + // is also recognizable so it can be deleted/reset like named ones. + if path == legacy_root.join("workspace") { + return Some(AssistantWorkspaceDescriptor { + path: path.to_path_buf(), + assistant_id: None, + display_name: Self::assistant_display_name(None), + }); + } } let file_name = path.file_name()?.to_string_lossy(); @@ -3789,6 +3808,30 @@ mod tests { Some("Legacy TypeScript implementation".to_string()) ); } + + #[tokio::test] + async fn legacy_bitfun_assistant_workspace_paths_are_still_recognized() { + let env = TestEnvironment::new(); + let service = build_test_workspace_service(env.path_manager.clone()).await; + + // New-brand assistant root remains recognized. + let new_root = env.path_manager.assistant_workspace_base_dir(None); + assert!(service.is_assistant_workspace_path(&new_root.join("workspace-abc"))); + assert!(service.is_assistant_workspace_path(&new_root.join("workspace"))); + + // Pre-rebranding assistant workspaces rooted at ~/.bitfun are recognized + // so delete/reset commands can still operate on them. + let product_home = env.path_manager.product_home_dir(); + let home = product_home.parent().unwrap(); + let legacy_root = home.join(".bitfun").join("personal_assistant"); + assert!(service.is_assistant_workspace_path(&legacy_root.join("workspace-44e0e781"))); + assert!(service.is_assistant_workspace_path(&legacy_root.join("workspace"))); + + // Unrelated paths are still rejected. + assert!(!service.is_assistant_workspace_path(&home.join("personal_assistant").join("workspace-xyz"))); + assert!(!service.is_assistant_workspace_path(&home.join(".bitfun").join("workspace-xyz"))); + assert!(!service.is_assistant_workspace_path(&home.join(".bitfun").join("personal_assistant").join("other"))); + } } fn workspace_catalog_revision() -> &'static tokio::sync::watch::Sender { diff --git a/src/web-ui/src/flow_chat/store/FlowChatStore.test.ts b/src/web-ui/src/flow_chat/store/FlowChatStore.test.ts index 5860f7ed50..c96ad9de1e 100644 --- a/src/web-ui/src/flow_chat/store/FlowChatStore.test.ts +++ b/src/web-ui/src/flow_chat/store/FlowChatStore.test.ts @@ -986,6 +986,20 @@ describe('FlowChatStore session removal active selection', () => { expect(flowChatStore.getState().activeSessionId).toBeNull(); expect(Array.from(flowChatStore.getState().sessions.keys())).toEqual(['session-keep']); }); + + it('deletes a session whose workspace directory is unavailable (ID-first backend)', async () => { + const session = createSession({ + sessionId: 'session-legacy', + title: 'Legacy assistant session', + workspacePath: undefined, + projectWorkspacePath: undefined, + }); + flowChatStore.setState(() => ({ sessions: new Map([[session.sessionId, session]]), activeSessionId: session.sessionId })); + await flowChatStore.deleteSession(session.sessionId, { nextActiveSessionId: null }); + expect(apiMocks.deleteSession).toHaveBeenCalledExactlyOnceWith(session.sessionId, session.workspaceId); + expect(flowChatStore.getState().sessions.has(session.sessionId)).toBe(false); + expect(flowChatStore.getState().activeSessionId).toBeNull(); + }); }); describe('FlowChatStore token usage', () => { diff --git a/src/web-ui/src/flow_chat/store/FlowChatStore.ts b/src/web-ui/src/flow_chat/store/FlowChatStore.ts index fcab35017c..0a402e6cf6 100644 --- a/src/web-ui/src/flow_chat/store/FlowChatStore.ts +++ b/src/web-ui/src/flow_chat/store/FlowChatStore.ts @@ -5379,9 +5379,9 @@ export class FlowChatStore { scope.assertCurrent('delete session'); const sess = this.state.sessions.get(id); if (sess?.draft?.phase !== 'editing') { - if (!sess || !sessionProjectWorkspacePath(sess)) { - throw new Error(`Workspace path not found for session ${id}`); - } + // The backend resolves session storage by workspace ID, not by path, + // so deletion must still be attempted when the workspace directory is + // unavailable (e.g. pre-migration assistant workspaces under ~/.bitfun). await agentAPI.deleteSession(id, this.sessionHistoryWorkspaceId(id)); scope.assertCurrent('complete session deletion'); }