From 846c4b7400d8137f8b9d3f3a3d1252b7660a983a Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Sun, 28 Sep 2025 13:39:28 +0200 Subject: [PATCH] CM-53314 - Add "Apply AI suggested fix" with the state tracker --- .../scanResult/ScanDetectionDetailsBase.kt | 1 + .../common/actions/CardActions.kt | 85 ++++++++++++++++++- .../components/actions/IacActions.kt | 51 +++++++++-- .../components/actions/SastActions.kt | 51 +++++++++-- .../components/actions/ScaActions.kt | 23 +++-- .../components/actions/SecretActions.kt | 25 ++++-- .../cycode/plugin/services/CycodeService.kt | 8 +- .../messages/CycodeBundle.properties | 3 + 8 files changed, 217 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/com/cycode/plugin/cli/models/scanResult/ScanDetectionDetailsBase.kt b/src/main/kotlin/com/cycode/plugin/cli/models/scanResult/ScanDetectionDetailsBase.kt index c79b25b..7c9f972 100644 --- a/src/main/kotlin/com/cycode/plugin/cli/models/scanResult/ScanDetectionDetailsBase.kt +++ b/src/main/kotlin/com/cycode/plugin/cli/models/scanResult/ScanDetectionDetailsBase.kt @@ -2,6 +2,7 @@ package com.cycode.plugin.cli.models.scanResult interface ScanDetectionDetailsBase { fun getFilepath(): String + /** * Gets the line number. * @return The 1-based line number (first line is 1, not 0) diff --git a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/common/actions/CardActions.kt b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/common/actions/CardActions.kt index 5e64f1f..40250a1 100644 --- a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/common/actions/CardActions.kt +++ b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/common/actions/CardActions.kt @@ -7,10 +7,18 @@ import java.awt.GridBagConstraints import javax.swing.JButton import javax.swing.JComponent import javax.swing.JPanel +import javax.swing.SwingUtilities open class CardActions { private val gbc = GridBagConstraints() private val panel: JPanel = JPanel(FlowLayout(FlowLayout.RIGHT)) + private val buttons: MutableMap = mutableMapOf() + + private data class ButtonInfo( + val button: JButton, + val originalText: String, + val inProgressText: String + ) init { gbc.insets = JBUI.insets(2) @@ -21,10 +29,79 @@ open class CardActions { ) } - fun addActionButton(text: String, onClick: () -> Unit) { - panel.add(JButton(text).apply { - addActionListener { onClick() } - }, gbc) + fun addActionButton( + id: String, + text: String, + onClick: () -> Unit, + async: Boolean = false, + inProgressText: String = "$text..." + ): JButton { + val button = JButton(text).apply { + addActionListener { + disableButton(id) + if (async) { + // For async operations, onClick handles its own threading and re-enabling + onClick() + } else { + // For sync operations, run in a background thread to avoid blocking EDT + Thread { + try { + onClick() + } finally { + SwingUtilities.invokeLater { + enableButton(id) + } + } + }.start() + } + } + } + buttons[id] = ButtonInfo(button, text, inProgressText) + panel.add(button, gbc) + return button + } + + fun removeButton(id: String) { + buttons[id]?.let { buttonInfo -> + panel.remove(buttonInfo.button) + buttons.remove(id) + panel.revalidate() + panel.repaint() + } + } + + fun showButton(id: String) { + buttons[id]?.button?.isVisible = true + panel.revalidate() + panel.repaint() + } + + fun hideButton(id: String) { + buttons[id]?.button?.isVisible = false + panel.revalidate() + panel.repaint() + } + + fun getButton(id: String): JButton? { + return buttons[id]?.button + } + + fun enableButton(id: String) { + buttons[id]?.let { buttonInfo -> + SwingUtilities.invokeLater { + buttonInfo.button.isEnabled = true + buttonInfo.button.text = buttonInfo.originalText + } + } + } + + fun disableButton(id: String) { + buttons[id]?.let { buttonInfo -> + SwingUtilities.invokeLater { + buttonInfo.button.isEnabled = false + buttonInfo.button.text = buttonInfo.inProgressText + } + } } fun getContent(): JComponent { diff --git a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/iacViolationCardContentTab/components/actions/IacActions.kt b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/iacViolationCardContentTab/components/actions/IacActions.kt index ba24085..e23d4f0 100644 --- a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/iacViolationCardContentTab/components/actions/IacActions.kt +++ b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/iacViolationCardContentTab/components/actions/IacActions.kt @@ -8,14 +8,55 @@ import com.cycode.plugin.components.toolWindow.components.violationCardContentTa import com.cycode.plugin.services.cycode import com.intellij.openapi.project.Project import javax.swing.JComponent +import javax.swing.SwingUtilities class IacActions(val project: Project) : CardActions() { + + companion object { + private const val GENERATE_AI_REMEDIATION_ID = "generate_ai_remediation" + private const val APPLY_AI_REMEDIATION_ID = "apply_ai_remediation" + } + fun addContent(detection: IacDetection, aiRemediationComponent: CardHtmlSummary): JComponent { - addActionButton(CycodeBundle.message("generateAiRemediationBtn"), onClick = { - cycode(project).getAiRemediation(detection.id) { remediationResult -> - aiRemediationComponent.setHtmlContent(convertMarkdownToHtml(remediationResult.remediation)) - } - }) + val generateButtonText = CycodeBundle.message("generateAiRemediationBtn") + val applyButtonText = CycodeBundle.message("applyAiRemediationBtn") + + addActionButton( + id = GENERATE_AI_REMEDIATION_ID, + text = generateButtonText, + onClick = { + cycode(project).getAiRemediation( + detectionId = detection.id, + onSuccess = { remediationResult -> + aiRemediationComponent.setHtmlContent(convertMarkdownToHtml(remediationResult.remediation)) + + SwingUtilities.invokeLater { + hideButton(GENERATE_AI_REMEDIATION_ID) + + if (remediationResult.isFixAvailable) { + showButton(APPLY_AI_REMEDIATION_ID) + } + } + }, + onFailure = { + enableButton(GENERATE_AI_REMEDIATION_ID) + } + ) + }, + async = true, + inProgressText = CycodeBundle.message("generateAiRemediationBtnInProgress") + ) + + addActionButton( + id = APPLY_AI_REMEDIATION_ID, + text = applyButtonText, + onClick = { + // TODO: Implement actual apply fix logic + Thread.sleep(3000) + }, + inProgressText = CycodeBundle.message("applyAiRemediationBtnInProgress") + ) + hideButton(APPLY_AI_REMEDIATION_ID) return getContent() } diff --git a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/sastViolationCardContentTab/components/actions/SastActions.kt b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/sastViolationCardContentTab/components/actions/SastActions.kt index db6fca1..2ab784b 100644 --- a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/sastViolationCardContentTab/components/actions/SastActions.kt +++ b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/sastViolationCardContentTab/components/actions/SastActions.kt @@ -8,14 +8,55 @@ import com.cycode.plugin.components.toolWindow.components.violationCardContentTa import com.cycode.plugin.services.cycode import com.intellij.openapi.project.Project import javax.swing.JComponent +import javax.swing.SwingUtilities class SastActions(val project: Project) : CardActions() { + + companion object { + private const val GENERATE_AI_REMEDIATION_ID = "generate_ai_remediation" + private const val APPLY_AI_REMEDIATION_ID = "apply_ai_remediation" + } + fun addContent(detection: SastDetection, aiRemediationComponent: CardHtmlSummary): JComponent { - addActionButton(CycodeBundle.message("generateAiRemediationBtn"), onClick = { - cycode(project).getAiRemediation(detection.id) { remediationResult -> - aiRemediationComponent.setHtmlContent(convertMarkdownToHtml(remediationResult.remediation)) - } - }) + val generateButtonText = CycodeBundle.message("generateAiRemediationBtn") + val applyButtonText = CycodeBundle.message("applyAiRemediationBtn") + + addActionButton( + id = GENERATE_AI_REMEDIATION_ID, + text = generateButtonText, + onClick = { + cycode(project).getAiRemediation( + detectionId = detection.id, + onSuccess = { remediationResult -> + aiRemediationComponent.setHtmlContent(convertMarkdownToHtml(remediationResult.remediation)) + + SwingUtilities.invokeLater { + hideButton(GENERATE_AI_REMEDIATION_ID) + + if (remediationResult.isFixAvailable) { + showButton(APPLY_AI_REMEDIATION_ID) + } + } + }, + onFailure = { + enableButton(GENERATE_AI_REMEDIATION_ID) + } + ) + }, + async = true, + inProgressText = CycodeBundle.message("generateAiRemediationBtnInProgress") + ) + + addActionButton( + id = APPLY_AI_REMEDIATION_ID, + text = applyButtonText, + onClick = { + // TODO: Implement actual apply fix logic + Thread.sleep(3000) + }, + inProgressText = CycodeBundle.message("applyAiRemediationBtnInProgress") + ) + hideButton(APPLY_AI_REMEDIATION_ID) return getContent() } diff --git a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/scaViolationCardContentTab/components/actions/ScaActions.kt b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/scaViolationCardContentTab/components/actions/ScaActions.kt index 9b365af..981ec01 100644 --- a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/scaViolationCardContentTab/components/actions/ScaActions.kt +++ b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/scaViolationCardContentTab/components/actions/ScaActions.kt @@ -10,15 +10,24 @@ import com.intellij.openapi.project.Project import javax.swing.JComponent class ScaActions(val project: Project) : CardActions() { + + companion object { + private const val IGNORE_VIOLATION_ID = "ignore_violation" + } + fun addContent(detection: ScaDetection): JComponent { if (detection.detectionDetails.alert?.cveIdentifier != null) { - addActionButton(CycodeBundle.message("violationCardIgnoreViolationBtn"), onClick = { - cycode(project).applyIgnoreFromFileAnnotation( - CliScanType.Sca, - CliIgnoreType.CVE, - detection.detectionDetails.alert.cveIdentifier - ) - }) + addActionButton( + id = IGNORE_VIOLATION_ID, + text = CycodeBundle.message("violationCardIgnoreViolationBtn"), + onClick = { + cycode(project).applyIgnoreFromFileAnnotation( + CliScanType.Sca, + CliIgnoreType.CVE, + detection.detectionDetails.alert.cveIdentifier + ) + } + ) } return getContent() diff --git a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/secretViolationCardContentTab/components/actions/SecretActions.kt b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/secretViolationCardContentTab/components/actions/SecretActions.kt index ff20278..bb8457d 100644 --- a/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/secretViolationCardContentTab/components/actions/SecretActions.kt +++ b/src/main/kotlin/com/cycode/plugin/components/toolWindow/components/violationCardContentTab/secretViolationCardContentTab/components/actions/SecretActions.kt @@ -10,16 +10,25 @@ import com.intellij.openapi.project.Project import javax.swing.JComponent class SecretActions(val project: Project) : CardActions() { + + companion object { + private const val IGNORE_VIOLATION_ID = "ignore_violation" + } + fun addContent(detection: SecretDetection): JComponent { - addActionButton(CycodeBundle.message("violationCardIgnoreViolationBtn"), onClick = { - if (detection.detectionDetails.detectedValue != null) { - cycode(project).applyIgnoreFromFileAnnotation( - CliScanType.Secret, - CliIgnoreType.VALUE, - detection.detectionDetails.detectedValue!! - ) + addActionButton( + id = IGNORE_VIOLATION_ID, + text = CycodeBundle.message("violationCardIgnoreViolationBtn"), + onClick = { + if (detection.detectionDetails.detectedValue != null) { + cycode(project).applyIgnoreFromFileAnnotation( + CliScanType.Secret, + CliIgnoreType.VALUE, + detection.detectionDetails.detectedValue!! + ) + } } - }) + ) return getContent() } diff --git a/src/main/kotlin/com/cycode/plugin/services/CycodeService.kt b/src/main/kotlin/com/cycode/plugin/services/CycodeService.kt index a8fcd80..6d84da7 100755 --- a/src/main/kotlin/com/cycode/plugin/services/CycodeService.kt +++ b/src/main/kotlin/com/cycode/plugin/services/CycodeService.kt @@ -103,7 +103,11 @@ class CycodeService(val project: Project) : Disposable { } } - fun getAiRemediation(detectionId: String, onSuccess: (AiRemediationResultData) -> Unit) { + fun getAiRemediation( + detectionId: String, + onSuccess: (AiRemediationResultData) -> Unit, + onFailure: () -> Unit = {} + ) { runBackgroundTask(CycodeBundle.message("aiRemediationGenerating")) { indicator -> thisLogger().debug("[AI REMEDIATION] Start generating remediation for $detectionId") val aiRemediation = cliService.getAiRemediation(detectionId) @@ -111,6 +115,8 @@ class CycodeService(val project: Project) : Disposable { if (aiRemediation != null) { onSuccess(aiRemediation) + } else { + onFailure() } } } diff --git a/src/main/resources/messages/CycodeBundle.properties b/src/main/resources/messages/CycodeBundle.properties index 7263220..797fbe3 100755 --- a/src/main/resources/messages/CycodeBundle.properties +++ b/src/main/resources/messages/CycodeBundle.properties @@ -105,6 +105,9 @@ violationCardCompanyGuidelinesTitle=Company Guidelines violationCardCycodeGuidelinesTitle=Cycode Guidelines violationCardAiRemediationTitle=AI Remediation generateAiRemediationBtn=Generate AI Remediation +applyAiRemediationBtn=Apply AI suggested fix +generateAiRemediationBtnInProgress=Generating... +applyAiRemediationBtnInProgress=Applying... violationCardIgnoreViolationBtn=Ignore this violation # sca violation card scaViolationCardShortSummary={0} | {1}