diff --git a/mpp-idea/build.gradle.kts b/mpp-idea/build.gradle.kts index 9e808eed86..288ba004d2 100644 --- a/mpp-idea/build.gradle.kts +++ b/mpp-idea/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { } repositories { + mavenLocal() // For locally published mpp-ui and mpp-core artifacts mavenCentral() google() // Required for mpp-ui's webview dependencies (jogamp) @@ -41,7 +42,9 @@ dependencies { // Depend on mpp-ui and mpp-core JVM targets for shared UI components and ConfigManager // For KMP projects, we need to depend on the JVM target specifically // IMPORTANT: Exclude ALL transitive dependencies that conflict with IntelliJ's bundled libraries - implementation("cc.unitmesh.devins:mpp-ui-jvm") { + // Note: For KMP projects, the module is published as "group:artifact-jvm" but the project + // dependency substitution should map "group:artifact" to the project ":artifact" + implementation("AutoDev-Intellij:mpp-ui:$mppVersion") { // Exclude all Compose dependencies - IntelliJ provides its own via bundledModules exclude(group = "org.jetbrains.compose") exclude(group = "org.jetbrains.compose.runtime") @@ -78,7 +81,7 @@ dependencies { // Exclude SQLDelight - not needed in IntelliJ plugin exclude(group = "app.cash.sqldelight") } - implementation("cc.unitmesh.devins:mpp-core-jvm") { + implementation("cc.unitmesh:mpp-core:$mppVersion") { // Exclude Compose dependencies from mpp-core as well exclude(group = "org.jetbrains.compose") exclude(group = "org.jetbrains.compose.runtime") diff --git a/mpp-idea/settings.gradle.kts b/mpp-idea/settings.gradle.kts index a205d98f32..c8a23317c4 100644 --- a/mpp-idea/settings.gradle.kts +++ b/mpp-idea/settings.gradle.kts @@ -18,11 +18,19 @@ pluginManagement { } // Include mpp-ui from parent project for shared UI components and ConfigManager -// For KMP projects, we substitute the JVM target artifacts +// For KMP projects, we substitute the Maven coordinates with local project dependencies +// Note: The group IDs must match what's defined in the respective build.gradle.kts files: +// - mpp-ui: uses root project name "AutoDev-Intellij" as group +// - mpp-core: group = "cc.unitmesh" +// - mpp-codegraph: uses root project name +// - mpp-viewer: group = "cc.unitmesh.viewer" includeBuild("..") { dependencySubstitution { - substitute(module("cc.unitmesh.devins:mpp-ui-jvm")).using(project(":mpp-ui")) - substitute(module("cc.unitmesh.devins:mpp-core-jvm")).using(project(":mpp-core")) + // Substitute Maven coordinates with project dependencies + substitute(module("AutoDev-Intellij:mpp-ui")).using(project(":mpp-ui")).because("Using local project") + substitute(module("cc.unitmesh:mpp-core")).using(project(":mpp-core")).because("Using local project") + substitute(module("AutoDev-Intellij:mpp-codegraph")).using(project(":mpp-codegraph")).because("Using local project") + substitute(module("cc.unitmesh.viewer:mpp-viewer")).using(project(":mpp-viewer")).because("Using local project") } } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewContent.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewContent.kt index ca3cc1fd8a..44537f732e 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewContent.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewContent.kt @@ -16,6 +16,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cc.unitmesh.agent.diff.ChangeType +import cc.unitmesh.devins.ui.compose.agent.codereview.AIAnalysisProgress +import cc.unitmesh.devins.ui.compose.agent.codereview.AnalysisStage +import cc.unitmesh.devins.ui.compose.agent.codereview.CommitInfo +import cc.unitmesh.devins.ui.compose.agent.codereview.DiffFileInfo import cc.unitmesh.devins.ui.compose.theme.AutoDevColors import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.Orientation @@ -36,7 +40,7 @@ fun IdeaCodeReviewContent(viewModel: IdeaCodeReviewViewModel) { selectedIndices = state.selectedCommitIndices, isLoading = state.isLoading, onCommitSelect = { index -> - viewModel.selectCommits(setOf(index)) + viewModel.selectCommit(index) }, modifier = Modifier.width(280.dp).fillMaxHeight() ) @@ -67,7 +71,7 @@ fun IdeaCodeReviewContent(viewModel: IdeaCodeReviewViewModel) { @Composable private fun CommitListPanel( - commits: List, + commits: List, selectedIndices: Set, isLoading: Boolean, onCommitSelect: (Int) -> Unit, @@ -122,7 +126,7 @@ private fun CommitListPanel( @Composable private fun CommitItem( - commit: IdeaCommitInfo, + commit: CommitInfo, isSelected: Boolean, onClick: () -> Unit ) { @@ -172,7 +176,7 @@ private fun CommitItem( @Composable private fun DiffViewerPanel( - diffFiles: List, + diffFiles: List, selectedFileIndex: Int, isLoading: Boolean, onFileSelect: (Int) -> Unit, @@ -251,7 +255,7 @@ private fun DiffViewerPanel( } @Composable -private fun DiffContent(file: IdeaDiffFileInfo) { +private fun DiffContent(file: DiffFileInfo) { val scrollState = rememberScrollState() Column( @@ -311,7 +315,7 @@ private fun DiffContent(file: IdeaDiffFileInfo) { @Composable private fun AIAnalysisPanel( - progress: IdeaAIAnalysisProgress, + progress: AIAnalysisProgress, error: String?, onStartAnalysis: () -> Unit, onCancelAnalysis: () -> Unit, @@ -333,7 +337,7 @@ private fun AIAnalysisPanel( ) when (progress.stage) { - IdeaAnalysisStage.IDLE, IdeaAnalysisStage.COMPLETED, IdeaAnalysisStage.ERROR -> { + AnalysisStage.IDLE, AnalysisStage.COMPLETED, AnalysisStage.ERROR -> { DefaultButton(onClick = onStartAnalysis) { Text("Start Analysis") } @@ -355,18 +359,19 @@ private fun AIAnalysisPanel( verticalAlignment = Alignment.CenterVertically ) { val (statusText, statusColor) = when (progress.stage) { - IdeaAnalysisStage.IDLE -> "Ready" to JewelTheme.globalColors.text.info - IdeaAnalysisStage.RUNNING_LINT -> "Running lint..." to AutoDevColors.Amber.c400 - IdeaAnalysisStage.ANALYZING -> "Analyzing code..." to AutoDevColors.Blue.c400 - IdeaAnalysisStage.GENERATING_PLAN -> "Generating plan..." to AutoDevColors.Blue.c400 - IdeaAnalysisStage.GENERATING_FIX -> "Generating fixes..." to AutoDevColors.Blue.c400 - IdeaAnalysisStage.COMPLETED -> "Completed" to AutoDevColors.Green.c400 - IdeaAnalysisStage.ERROR -> "Error" to AutoDevColors.Red.c400 + AnalysisStage.IDLE -> "Ready" to JewelTheme.globalColors.text.info + AnalysisStage.RUNNING_LINT -> "Running lint..." to AutoDevColors.Amber.c400 + AnalysisStage.ANALYZING_LINT -> "Analyzing code..." to AutoDevColors.Blue.c400 + AnalysisStage.GENERATING_PLAN -> "Generating plan..." to AutoDevColors.Blue.c400 + AnalysisStage.WAITING_FOR_USER_INPUT -> "Waiting for input..." to AutoDevColors.Amber.c400 + AnalysisStage.GENERATING_FIX -> "Generating fixes..." to AutoDevColors.Blue.c400 + AnalysisStage.COMPLETED -> "Completed" to AutoDevColors.Green.c400 + AnalysisStage.ERROR -> "Error" to AutoDevColors.Red.c400 } - if (progress.stage != IdeaAnalysisStage.IDLE && - progress.stage != IdeaAnalysisStage.COMPLETED && - progress.stage != IdeaAnalysisStage.ERROR) { + if (progress.stage != AnalysisStage.IDLE && + progress.stage != AnalysisStage.COMPLETED && + progress.stage != AnalysisStage.ERROR) { CircularProgressIndicator() } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewModels.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewModels.kt deleted file mode 100644 index ffc383d424..0000000000 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewModels.kt +++ /dev/null @@ -1,71 +0,0 @@ -package cc.unitmesh.devins.idea.toolwindow.codereview - -import cc.unitmesh.agent.diff.ChangeType -import cc.unitmesh.agent.diff.DiffHunk - -/** - * State for Code Review UI in IntelliJ IDEA plugin. - * Adapted from mpp-ui's CodeReviewState. - */ -data class IdeaCodeReviewState( - val isLoading: Boolean = false, - val isLoadingDiff: Boolean = false, - val error: String? = null, - val commitHistory: List = emptyList(), - val selectedCommitIndices: Set = emptySet(), - val diffFiles: List = emptyList(), - val selectedFileIndex: Int = 0, - val aiProgress: IdeaAIAnalysisProgress = IdeaAIAnalysisProgress(), - val hasMoreCommits: Boolean = false, - val isLoadingMore: Boolean = false, - val totalCommitCount: Int? = null, - val originDiff: String? = null -) - -/** - * Information about a commit. - */ -data class IdeaCommitInfo( - val hash: String, - val shortHash: String, - val author: String, - val timestamp: Long, - val date: String, - val message: String -) - -/** - * Information about a file in the diff. - */ -data class IdeaDiffFileInfo( - val path: String, - val oldPath: String? = null, - val changeType: ChangeType = ChangeType.EDIT, - val hunks: List = emptyList(), - val language: String? = null -) - -/** - * AI analysis progress for streaming display. - */ -data class IdeaAIAnalysisProgress( - val stage: IdeaAnalysisStage = IdeaAnalysisStage.IDLE, - val currentFile: String? = null, - val analysisOutput: String = "", - val planOutput: String = "", - val fixOutput: String = "" -) - -/** - * Stages of AI analysis. - */ -enum class IdeaAnalysisStage { - IDLE, - RUNNING_LINT, - ANALYZING, - GENERATING_PLAN, - GENERATING_FIX, - COMPLETED, - ERROR -} - diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewViewModel.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewViewModel.kt index b97307e38d..e4880a2afc 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewViewModel.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/codereview/IdeaCodeReviewViewModel.kt @@ -1,345 +1,59 @@ package cc.unitmesh.devins.idea.toolwindow.codereview -import cc.unitmesh.agent.CodeReviewAgent -import cc.unitmesh.agent.ReviewTask -import cc.unitmesh.agent.ReviewType -import cc.unitmesh.agent.config.McpToolConfigService -import cc.unitmesh.agent.config.ToolConfigFile -import cc.unitmesh.agent.diff.ChangeType -import cc.unitmesh.agent.diff.DiffParser -import cc.unitmesh.agent.language.LanguageDetector -import cc.unitmesh.agent.platform.GitOperations import cc.unitmesh.devins.idea.renderer.JewelRenderer -import cc.unitmesh.devins.ui.config.ConfigManager -import cc.unitmesh.devins.workspace.GitFileStatus -import cc.unitmesh.llm.KoogLLMService +import cc.unitmesh.devins.ui.compose.agent.codereview.CodeReviewViewModel +import cc.unitmesh.devins.workspace.DefaultWorkspace +import cc.unitmesh.devins.workspace.Workspace import com.intellij.openapi.Disposable +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import java.text.SimpleDateFormat -import java.util.* +import kotlinx.coroutines.CoroutineScope /** * ViewModel for Code Review in IntelliJ IDEA plugin. - * Adapted from mpp-ui's CodeReviewViewModel for IntelliJ platform. * - * Uses mpp-core's GitOperations (JVM implementation) for git operations - * and JewelRenderer for UI rendering. + * This class extends the common CodeReviewViewModel from mpp-ui, + * adapting it for the IntelliJ platform by: + * - Creating a Workspace from IntelliJ Project + * - Using JewelRenderer for native IntelliJ theme integration + * - Implementing Disposable for proper resource cleanup + * + * All core functionality (git operations, analysis, plan generation, fix generation) + * is inherited from the base CodeReviewViewModel. */ class IdeaCodeReviewViewModel( private val project: Project, private val coroutineScope: CoroutineScope -) : Disposable { - - private val projectPath: String = project.basePath ?: "" - private val gitOps = GitOperations(projectPath) - - // Renderer for agent output - val renderer = JewelRenderer() - - // State - private val _state = MutableStateFlow(IdeaCodeReviewState()) - val state: StateFlow = _state.asStateFlow() - - // Control execution - private var currentJob: Job? = null - private var codeReviewAgent: CodeReviewAgent? = null - private var agentInitialized = false - - init { - if (projectPath.isEmpty()) { - updateState { it.copy(error = "No project path available") } - } else { - coroutineScope.launch { - try { - loadCommitHistory() - } catch (e: Exception) { - updateState { it.copy(error = "Failed to initialize: ${e.message}") } - } - } - } - } - - /** - * Load recent git commits - */ - suspend fun loadCommitHistory(count: Int = 50) { - updateState { it.copy(isLoading = true, error = null) } - - try { - val totalCount = gitOps.getTotalCommitCount() - val gitCommits = gitOps.getRecentCommits(count) - - val hasMore = totalCount?.let { it > gitCommits.size } ?: false - val commits = gitCommits.map { git -> - IdeaCommitInfo( - hash = git.hash, - shortHash = git.shortHash, - author = git.author, - timestamp = git.date, - date = formatDate(git.date), - message = git.message - ) - } - - updateState { - it.copy( - isLoading = false, - commitHistory = commits, - selectedCommitIndices = if (commits.isNotEmpty()) setOf(0) else emptySet(), - hasMoreCommits = hasMore, - totalCommitCount = totalCount, - error = null - ) - } - - if (commits.isNotEmpty()) { - loadCommitDiff(setOf(0)) - } - } catch (e: Exception) { - updateState { - it.copy(isLoading = false, error = "Failed to load commits: ${e.message}") - } - } - } +) : CodeReviewViewModel( + workspace = createWorkspaceFromProject(project) +), Disposable { - /** - * Select commits and load their diff - */ - fun selectCommits(indices: Set) { - coroutineScope.launch { - loadCommitDiff(indices) - } - } + private val logger = Logger.getInstance(IdeaCodeReviewViewModel::class.java) - /** - * Load diff for selected commits - */ - private suspend fun loadCommitDiff(selectedIndices: Set) { - if (selectedIndices.isEmpty()) { - updateState { - it.copy( - isLoadingDiff = false, - selectedCommitIndices = emptySet(), - diffFiles = emptyList(), - error = null - ) - } - return - } - - updateState { - it.copy(isLoadingDiff = true, selectedCommitIndices = selectedIndices, error = null) - } + // JewelRenderer for IntelliJ native theme + val jewelRenderer = JewelRenderer() - try { - val sortedIndices = selectedIndices.sorted() - val newestIndex = sortedIndices.first() - val oldestIndex = sortedIndices.last() + companion object { + /** + * Create a Workspace from an IntelliJ Project + */ + private fun createWorkspaceFromProject(project: Project): Workspace { + val projectPath = project.basePath + val projectName = project.name - val currentState = _state.value - val newestCommit = currentState.commitHistory[newestIndex] - val oldestCommit = currentState.commitHistory[oldestIndex] - - val gitDiff = if (newestIndex == oldestIndex) { - gitOps.getCommitDiff(newestCommit.hash) + return if (projectPath != null) { + DefaultWorkspace.create(projectName, projectPath) } else { - val hasParent = gitOps.hasParent(oldestCommit.hash) - if (hasParent) { - gitOps.getDiff("${oldestCommit.hash}^", newestCommit.hash) - } else { - gitOps.getDiff("4b825dc642cb6eb9a060e54bf8d69288fbee4904", newestCommit.hash) - } - } - - if (gitDiff == null) { - updateState { it.copy(isLoadingDiff = false, error = "No diff available") } - return - } - - val diffFiles = gitDiff.files.map { file -> - val parsedDiff = DiffParser.parse(file.diff) - val hunks = parsedDiff.firstOrNull()?.hunks ?: emptyList() - - IdeaDiffFileInfo( - path = file.path, - oldPath = file.oldPath, - changeType = when (file.status) { - GitFileStatus.ADDED -> ChangeType.CREATE - GitFileStatus.DELETED -> ChangeType.DELETE - GitFileStatus.MODIFIED -> ChangeType.EDIT - GitFileStatus.RENAMED -> ChangeType.RENAME - GitFileStatus.COPIED -> ChangeType.EDIT - }, - hunks = hunks, - language = LanguageDetector.detectLanguage(file.path) - ) - } - - updateState { - it.copy( - isLoadingDiff = false, - diffFiles = diffFiles, - selectedFileIndex = 0, - error = null, - originDiff = gitDiff.originDiff - ) - } - } catch (e: Exception) { - updateState { it.copy(isLoadingDiff = false, error = "Failed to load diff: ${e.message}") } - } - } - - /** - * Select a file from the diff list - */ - fun selectFile(index: Int) { - updateState { it.copy(selectedFileIndex = index) } - } - - /** - * Start AI analysis on the selected commits - */ - fun startAnalysis() { - val currentState = _state.value - if (currentState.diffFiles.isEmpty()) { - updateState { it.copy(error = "No files to analyze") } - return - } - - currentJob?.cancel() - currentJob = coroutineScope.launch { - try { - updateState { - it.copy( - aiProgress = IdeaAIAnalysisProgress(stage = IdeaAnalysisStage.RUNNING_LINT), - error = null - ) - } - - val agent = initializeCodeReviewAgent() - val filePaths = currentState.diffFiles.map { it.path } - - val additionalContext = buildString { - val selectedCommits = currentState.selectedCommitIndices - .mapNotNull { currentState.commitHistory.getOrNull(it) } - - if (selectedCommits.isNotEmpty()) { - appendLine("## Selected Commits") - selectedCommits.forEach { commit -> - appendLine("- ${commit.shortHash}: ${commit.message.lines().firstOrNull()}") - } - appendLine() - } - } - - val reviewTask = ReviewTask( - filePaths = filePaths, - reviewType = ReviewType.COMPREHENSIVE, - projectPath = projectPath, - patch = currentState.originDiff, - lintResults = emptyList(), - additionalContext = additionalContext - ) - - updateState { - it.copy(aiProgress = it.aiProgress.copy( - stage = IdeaAnalysisStage.ANALYZING, - analysisOutput = "Starting code review analysis...\n" - )) - } - - val analysisOutputBuilder = StringBuilder() - try { - agent.execute(reviewTask) { progressMessage -> - analysisOutputBuilder.append(progressMessage) - updateState { - it.copy(aiProgress = it.aiProgress.copy( - analysisOutput = analysisOutputBuilder.toString() - )) - } - } - - updateState { - it.copy(aiProgress = it.aiProgress.copy(stage = IdeaAnalysisStage.COMPLETED)) - } - } catch (e: Exception) { - analysisOutputBuilder.append("\nError: ${e.message}") - updateState { - it.copy(aiProgress = it.aiProgress.copy( - stage = IdeaAnalysisStage.ERROR, - analysisOutput = analysisOutputBuilder.toString() - )) - } - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - updateState { - it.copy( - aiProgress = it.aiProgress.copy(stage = IdeaAnalysisStage.ERROR), - error = "Analysis failed: ${e.message}" - ) - } + DefaultWorkspace.createEmpty(projectName) } } } /** - * Cancel current analysis - */ - fun cancelAnalysis() { - currentJob?.cancel() - updateState { it.copy(aiProgress = IdeaAIAnalysisProgress(stage = IdeaAnalysisStage.IDLE)) } - } - - /** - * Initialize the CodeReviewAgent + * Dispose resources when the ViewModel is no longer needed */ - private suspend fun initializeCodeReviewAgent(): CodeReviewAgent { - if (codeReviewAgent != null && agentInitialized) { - return codeReviewAgent!! - } - - val toolConfig = ToolConfigFile.default() - val configWrapper = ConfigManager.load() - val modelConfig = configWrapper.getActiveModelConfig() - ?: error("No active model configuration found. Please configure a model in settings.") - - val llmService = KoogLLMService.create(modelConfig) - val mcpToolConfigService = McpToolConfigService(toolConfig) - - codeReviewAgent = CodeReviewAgent( - projectPath = projectPath, - llmService = llmService, - maxIterations = 50, - renderer = renderer, - mcpToolConfigService = mcpToolConfigService, - enableLLMStreaming = true - ) - agentInitialized = true - - return codeReviewAgent!! - } - - private fun updateState(update: (IdeaCodeReviewState) -> IdeaCodeReviewState) { - _state.value = update(_state.value) - } - - private fun formatDate(timestamp: Long): String { - return try { - val date = Date(timestamp * 1000) - SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(date) - } catch (e: Exception) { - "Unknown" - } - } - override fun dispose() { - currentJob?.cancel() + logger.info("Disposing IdeaCodeReviewViewModel") + // The parent class cleanup will happen when the scope is cancelled } } -