package cc.unitmesh.devins.idea.toolwindow.changes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cc.unitmesh.agent.diff.ChangeType import cc.unitmesh.agent.diff.FileChange import cc.unitmesh.agent.diff.FileChangeTracker import cc.unitmesh.devins.idea.compose.IdeaLaunchedEffect import cc.unitmesh.devins.idea.theme.IdeaAutoDevColors import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.icons.AllIconsKeys private val logger = Logger.getInstance("IdeaFileChangeSummary") /** * File Change Summary Component for IntelliJ IDEA using Jewel components. * * Displays a collapsible summary of all file changes made by the AI Agent. * Uses Jewel theming and components for native IntelliJ look and feel. */ @Composable fun IdeaFileChangeSummary( project: Project, modifier: Modifier = Modifier ) { // Use manual state collection to avoid ClassLoader conflicts with collectAsState() var changes by remember { mutableStateOf(emptyList()) } IdeaLaunchedEffect(Unit) { FileChangeTracker.changes.collect { changes = it } } var isExpanded by remember { mutableStateOf(false) } var selectedChange by remember { mutableStateOf(null) } // Only show if there are changes if (changes.isEmpty()) { return } // Handle diff dialog display when a file is selected IdeaLaunchedEffect(selectedChange) { selectedChange?.let { change -> ApplicationManager.getApplication().invokeLater { IdeaFileChangeDiffDialogWrapper.show( project = project, change = change, onUndo = { undoChange(project, change) FileChangeTracker.removeChange(change) }, onKeep = { FileChangeTracker.removeChange(change) }, onDismiss = {} ) } selectedChange = null } } Column( modifier = modifier .fillMaxWidth() .background( JewelTheme.globalColors.panelBackground, RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp) ) .clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp)) ) { // Collapsed header IdeaChangeSummaryHeader( changeCount = changes.size, isExpanded = isExpanded, onExpandToggle = { isExpanded = !isExpanded }, onUndoAll = { changes.forEach { change -> undoChange(project, change) } FileChangeTracker.clearChanges() }, onKeepAll = { FileChangeTracker.clearChanges() } ) // Expanded content AnimatedVisibility( visible = isExpanded, enter = expandVertically(), exit = shrinkVertically() ) { Column(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier .fillMaxWidth() .height(1.dp) .background(JewelTheme.globalColors.borders.normal) ) LazyColumn( modifier = Modifier .fillMaxWidth() .heightIn(max = 200.dp) .padding(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { items(changes, key = { "${it.filePath}_${it.timestamp}" }) { change -> IdeaFileChangeItem( change = change, onClick = { selectedChange = change }, onUndo = { undoChange(project, change) FileChangeTracker.removeChange(change) }, onKeep = { FileChangeTracker.removeChange(change) } ) } } } } } } @Composable private fun IdeaChangeSummaryHeader( changeCount: Int, isExpanded: Boolean, onExpandToggle: () -> Unit, onUndoAll: () -> Unit, onKeepAll: () -> Unit ) { Row( modifier = Modifier .fillMaxWidth() .clickable { onExpandToggle() } .padding(horizontal = 8.dp, vertical = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { // Left side: expand arrow and title Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { // Expand arrow Icon( key = if (isExpanded) AllIconsKeys.General.ArrowDown else AllIconsKeys.General.ArrowRight, contentDescription = if (isExpanded) "Collapse" else "Expand", modifier = Modifier.size(12.dp), tint = IdeaAutoDevColors.Neutral.c400 ) // Title Text( text = "$changeCount file${if (changeCount > 1) "s" else ""} changed", style = JewelTheme.defaultTextStyle.copy( fontSize = 12.sp, fontWeight = FontWeight.Medium ), maxLines = 1, overflow = TextOverflow.Ellipsis ) } // Right side: action buttons Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { // Undo All button IconButton( onClick = onUndoAll, modifier = Modifier.size(24.dp) ) { Icon( key = AllIconsKeys.Actions.Rollback, contentDescription = "Undo All", modifier = Modifier.size(14.dp), tint = IdeaAutoDevColors.Red.c400 ) } // Keep All button IconButton( onClick = onKeepAll, modifier = Modifier.size(24.dp) ) { Icon( key = AllIconsKeys.Actions.Checked, contentDescription = "Keep All", modifier = Modifier.size(14.dp), tint = IdeaAutoDevColors.Green.c400 ) } } } } /** * Undo a file change using IntelliJ's RollbackWorker for proper VCS integration. * Falls back to manual revert if RollbackWorker fails. */ private fun undoChange(project: Project, fileChange: FileChange) { ApplicationManager.getApplication().invokeLater { try { // Convert FileChange to IntelliJ Change and use RollbackWorker val change = FileChangeConverter.toChange(project, fileChange) val rollbackWorker = com.intellij.openapi.vcs.changes.ui.RollbackWorker(project) rollbackWorker.doRollback(listOf(change), false) } catch (e: Exception) { logger.warn("RollbackWorker failed, falling back to manual revert", e) // Fallback to manual revert performManualUndo(project, fileChange) } } } /** * Manual undo fallback when RollbackWorker fails. */ private fun performManualUndo(project: Project, change: FileChange) { runWriteAction { try { when (change.changeType) { ChangeType.CREATE -> { // For created files, delete or clear the content val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath) virtualFile?.let { vf -> val document = FileDocumentManager.getInstance().getDocument(vf) document?.setText("") } } ChangeType.EDIT, ChangeType.RENAME -> { // Restore original content change.originalContent?.let { original -> val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath) virtualFile?.let { vf -> val document = FileDocumentManager.getInstance().getDocument(vf) document?.setText(original) } } } ChangeType.DELETE -> { // For deleted files, we would need to recreate them change.originalContent?.let { original -> val parentPath = change.filePath.substringBeforeLast('/') val fileName = change.filePath.substringAfterLast('/') val parentDir = LocalFileSystem.getInstance().findFileByPath(parentPath) parentDir?.let { dir -> val newFile = dir.createChildData(project, fileName) val document = FileDocumentManager.getInstance().getDocument(newFile) document?.setText(original) } } } } } catch (e: Exception) { logger.error("Failed to undo change for ${change.filePath}", e) } } }