diff --git a/.changes/next-release/feature-fa373e43-dd37-4f6f-a226-7476f5866e9a.json b/.changes/next-release/feature-fa373e43-dd37-4f6f-a226-7476f5866e9a.json new file mode 100644 index 00000000000..8492c48768d --- /dev/null +++ b/.changes/next-release/feature-fa373e43-dd37-4f6f-a226-7476f5866e9a.json @@ -0,0 +1,4 @@ +{ + "type" : "feature", + "description" : "Enable agentic code review" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6c7361face9..a5cb2a89f39 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ mockitoKotlin = "5.4.1-SNAPSHOT" mockk = "1.13.17" nimbus-jose-jwt = "9.40" node-gradle = "7.0.2" -telemetryGenerator = "1.0.322" +telemetryGenerator = "1.0.329" testLogger = "4.0.0" testRetry = "1.5.10" # test-only; platform provides slf4j transitively at runtime diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index 0f994cfb35e..d2172d1f6f9 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -10,9 +10,13 @@ import com.google.gson.Gson import com.intellij.ide.BrowserUtil import com.intellij.ide.util.RunOnceUtil import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.jcef.JBCefJSQuery.Response import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred @@ -71,6 +75,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_BAR_ACTIONS import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_CHANGE import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_REMOVE +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CODE_REVIEW_FINDINGS_SUFFIX +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DISPLAY_FINDINGS_SUFFIX import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD @@ -107,10 +113,17 @@ import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestA import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Description +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Recommendation +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.SuggestedFix import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.settings.MeetQSettings import software.aws.toolkits.telemetry.MetricResult import software.aws.toolkits.telemetry.Telemetry +import java.nio.file.Path import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException import java.util.function.Function @@ -568,6 +581,32 @@ class BrowserConnector( } } + data class FlareCodeScanIssue( + val startLine: Int, + val endLine: Int, + val comment: String?, + val title: String, + val description: Description, + val detectorId: String, + val detectorName: String, + val findingId: String, + val ruleId: String?, + val relatedVulnerabilities: List, + val severity: String, + val recommendation: Recommendation, + val suggestedFixes: List, + val scanJobId: String, + val language: String, + val autoDetected: Boolean, + val filePath: String, + val findingContext: String, + ) + + data class AggregatedCodeScanIssue( + val filePath: String, + val issues: List, + ) + private fun showResult( result: CompletableFuture, partialResultToken: String, @@ -581,10 +620,14 @@ class BrowserConnector( throw error } chatCommunicationManager.removePartialChatMessage(partialResultToken) + val decryptedMessage = Gson().fromJson(value?.let { encryptionManager?.decrypt(it) }.orEmpty(), Map::class.java) + as Map + parseFindingsMessages(decryptedMessage) + val messageToChat = ChatCommunicationManager.convertToJsonToSendToChat( SEND_CHAT_COMMAND_PROMPT, tabId, - value?.let { encryptionManager?.decrypt(it) }.orEmpty(), + Gson().toJson(decryptedMessage), isPartialResult = false ) browser.postChat(messageToChat) @@ -598,6 +641,85 @@ class BrowserConnector( } } + fun parseFindingsMessages(messagesMap: Map) { + try { + val additionalMessages = messagesMap["additionalMessages"] as? MutableList> + val findingsMessages = additionalMessages?.filter { message -> + if (message.contains("messageId")) { + (message["messageId"] as String).endsWith(CODE_REVIEW_FINDINGS_SUFFIX) || + (message["messageId"] as String).endsWith(DISPLAY_FINDINGS_SUFFIX) + } else { + false + } + } + val scannedFiles = mutableListOf() + if (findingsMessages != null) { + for (findingsMessage in findingsMessages) { + additionalMessages.remove(findingsMessage) + val gson = Gson() + val jsonFindings = gson.fromJson(findingsMessage["body"] as String, List::class.java) + val mappedFindings = mutableListOf() + for (aggregatedIssueUnformatted in jsonFindings) { + val aggregatedIssue = gson.fromJson(gson.toJson(aggregatedIssueUnformatted), AggregatedCodeScanIssue::class.java) + val file = LocalFileSystem.getInstance().findFileByIoFile( + Path.of(aggregatedIssue.filePath).toFile() + ) + if (file?.isDirectory == false) { + scannedFiles.add(file) + runReadAction { + FileDocumentManager.getInstance().getDocument(file) + }?.let { document -> + for (issue in aggregatedIssue.issues) { + val endLineInDocument = minOf(maxOf(0, issue.endLine - 1), document.lineCount - 1) + val endCol = document.getLineEndOffset(endLineInDocument) - document.getLineStartOffset(endLineInDocument) + 1 + val isIssueIgnored = CodeWhispererCodeScanManager.getInstance(project) + .isIgnoredIssue(issue.title, document, file, issue.startLine - 1) + if (isIssueIgnored) { + continue + } + mappedFindings.add( + CodeWhispererCodeScanIssue( + startLine = issue.startLine, + startCol = 1, + endLine = issue.endLine, + endCol = endCol, + file = file, + project = project, + title = issue.title, + description = issue.description, + detectorId = issue.detectorId, + detectorName = issue.detectorName, + findingId = issue.findingId, + ruleId = issue.ruleId, + relatedVulnerabilities = issue.relatedVulnerabilities, + severity = issue.severity, + recommendation = issue.recommendation, + suggestedFixes = issue.suggestedFixes, + codeSnippet = emptyList(), + isVisible = true, + autoDetected = issue.autoDetected, + scanJobId = issue.scanJobId, + ), + ) + } + } + } + } + + CodeWhispererCodeScanManager.getInstance(project) + .addOnDemandIssues( + mappedFindings, + scannedFiles, + CodeWhispererConstants.CodeAnalysisScope.AGENTIC + ) + CodeWhispererCodeScanManager.getInstance(project).showCodeScanUI() + } + } + } catch (e: Exception) { + LOG.error { "Failed to parse findings message $e" } + } + } + private suspend fun updateQuickActionsInBrowser(browser: Browser) { val isFeatureDevAvailable = isFeatureDevAvailable(project) val isCodeTransformAvailable = isCodeTransformAvailable(project) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt index 33a9cc50baa..b2630a0f9f7 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/EditorContextCommand.kt @@ -34,8 +34,8 @@ enum class EditorContextCommand( verb = "sendToPrompt", actionId = "aws.amazonq.sendToPrompt", ), - ExplainCodeScanIssue( - verb = "ExplainIssue", - actionId = "aws.amazonq.explainCodeScanIssue", + HandleCodeScanIssue( + verb = "HandleCodeScanIssue", + actionId = "aws.amazonq.handleCodeScanIssueCommand", ), } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/HandleIssueCommandAction.kt similarity index 57% rename from plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt rename to plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/HandleIssueCommandAction.kt index 33a8bcf857c..2e0fb0693f1 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/HandleIssueCommandAction.kt @@ -17,34 +17,56 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatP import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_TO_PROMPT import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendToPromptParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TriggerType +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl + +class HandleIssueCommandAction : AnAction(), DumbAware { + val contextDataKey = DataKey.create>("amazonq.codescan.handleIssueCommandContext") + val actionDataKey = DataKey.create("amazonq.codescan.handleIssueCommandAction") -class ExplainCodeIssueAction : AnAction(), DumbAware { override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun update(e: AnActionEvent) { e.presentation.isEnabledAndVisible = e.project != null } + fun createLineRangeText(issueContext: MutableMap): String { + val startLine = issueContext["startLine"] + val endLine = issueContext["endLine"] + return if (startLine.equals(endLine)) { + "[$startLine]" + } else { + "[$startLine, $endLine]" + } + } override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return - val issueDataKey = DataKey.create>("amazonq.codescan.explainissue") - val issueContext = e.getData(issueDataKey) ?: return + val context = e.getData(contextDataKey) ?: return + val action = e.getData(actionDataKey) ?: return + + // Emit telemetry event + TelemetryHelper.recordTelemetryIssueCommandAction( + context["findingId"].orEmpty(), + context["detectorId"].orEmpty(), + context["ruleId"].orEmpty(), + context["autoDetected"].orEmpty(), + getStartUrl(project).orEmpty(), + action, // The action name (explainIssue or applyFix) + "Succeeded" + ) ActionManager.getInstance().getAction("q.openchat").actionPerformed(e) ApplicationManager.getApplication().executeOnPooledThread { runBlocking { // https://github.com/aws/aws-toolkit-vscode/blob/master/packages/amazonq/src/lsp/chat/commands.ts#L30 - val codeSelection = "\n```\n${issueContext["code"]?.trimIndent()?.trim()}\n```\n" + val codeSelection = "\n```\n${context["code"]?.trimIndent()?.trim()}\n```\n" + val actionString = if (action == "explainIssue") "Explain" else "Fix" - val prompt = "Explain the issue \n\n " + - "Issue: \"${issueContext["title"]}\" \n" + - "Code: $codeSelection" + val prompt = "$actionString ${context["title"]} issue in ${context["fileName"]} at ${createLineRangeText(context)}" - val modelPrompt = "Explain the issue ${issueContext["title"]} \n\n " + - "Issue: \"${issueContext["title"]}\" \n" + - "Description: ${issueContext["description"]} \n" + - "Code: $codeSelection and generate the code demonstrating the fix" + val modelPrompt = "$actionString ${context["title"]} issue in ${context["fileName"]} at ${createLineRangeText(context)}" + + "Issue: \"${context}\" \n" val params = SendToPromptParams( selection = codeSelection, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index 4d7cff9667f..be5f83fa2e2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -441,6 +441,26 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: .credentialStartUrl(startUrl) } } + + fun recordTelemetryIssueCommandAction( + findingId: String, + detectorId: String, + ruleId: String, + autoDetected: String, + startUrl: String, + metricName: String, + result: String, + ) { + Telemetry.amazonq.codeReviewTool.use { + it.reason(metricName) + .setAttribute("findingId", findingId) + .setAttribute("detectorId", detectorId) + .setAttribute("ruleId", ruleId) + .setAttribute("credentialStartUrl", startUrl) + .setAttribute("autoDetected", autoDetected) + .setAttribute("result", result) + } + } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt index 6d910aa729d..640c594795b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt @@ -15,7 +15,7 @@ class UserIntentRecognizer { EditorContextCommand.Refactor -> UserIntent.SUGGEST_ALTERNATE_IMPLEMENTATION EditorContextCommand.Fix -> UserIntent.APPLY_COMMON_BEST_PRACTICES EditorContextCommand.Optimize -> UserIntent.IMPROVE_CODE - EditorContextCommand.ExplainCodeScanIssue -> UserIntent.EXPLAIN_CODE_SELECTION + EditorContextCommand.HandleCodeScanIssue -> UserIntent.EXPLAIN_CODE_SELECTION EditorContextCommand.GenerateUnitTests -> UserIntent.GENERATE_UNIT_TESTS EditorContextCommand.SendToPrompt -> null } diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnectorTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnectorTest.kt new file mode 100644 index 00000000000..2a7f87440fa --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnectorTest.kt @@ -0,0 +1,214 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.webview + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.testFramework.replaceService +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import software.aws.toolkits.jetbrains.services.amazonq.AmazonQTestBase +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Description +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Recommendation +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.SuggestedFix +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants + +class BrowserConnectorTest : AmazonQTestBase() { + private lateinit var browserConnector: BrowserConnector + private lateinit var mockCodeScanManager: CodeWhispererCodeScanManager + private lateinit var mockLocalFileSystem: LocalFileSystem + private lateinit var mockFileDocumentManager: FileDocumentManager + private lateinit var fixture: CodeInsightTestFixture + + @Before + override fun setup() { + super.setup() + fixture = projectRule.fixture + + mockCodeScanManager = mock() + mockLocalFileSystem = mock() + mockFileDocumentManager = mock() + + project.replaceService(CodeWhispererCodeScanManager::class.java, mockCodeScanManager, disposableRule.disposable) + + browserConnector = spy(BrowserConnector(project = project)) + } + + @Test + fun `parseFindingsMessages should handle empty additionalMessages`() { + val messagesMap = mapOf() + + browserConnector.parseFindingsMessages(messagesMap) + + verify(mockCodeScanManager, never()).addOnDemandIssues(any(), any(), any()) + } + + @Test + fun `parseFindingsMessages should handle null additionalMessages`() { + val messagesMap = mapOf("additionalMessages" to null) + + browserConnector.parseFindingsMessages(messagesMap) + + verify(mockCodeScanManager, never()).addOnDemandIssues(any(), any(), any()) + } + + @Test + fun `parseFindingsMessages should filter messages with CODE_REVIEW_FINDINGS_SUFFIX`() { + val findingsMessage = mapOf( + "messageId" to "test_codeReviewFindings", + "body" to """[{"filePath": "/test/file.kt", "issues": []}]""" + ) + val otherMessage = mapOf( + "messageId" to "other_message", + "body" to "other content" + ) + val additionalMessages = mutableListOf>(findingsMessage, otherMessage) + val messagesMap = mapOf("additionalMessages" to additionalMessages) + + browserConnector.parseFindingsMessages(messagesMap) + + assertThat(additionalMessages.size == 1) + assertThat(additionalMessages[0] == otherMessage) + } + + @Test + fun `parseFindingsMessages should filter messages with DISPLAY_FINDINGS_SUFFIX`() { + val findingsMessage = mapOf( + "messageId" to "test_displayFindings", + "body" to """[{"filePath": "/test/file.kt", "issues": []}]""" + ) + val additionalMessages = mutableListOf>(findingsMessage) + val messagesMap = mapOf("additionalMessages" to additionalMessages) + + browserConnector.parseFindingsMessages(messagesMap) + + assertThat(additionalMessages).isEmpty() + } + + @Test + fun `parseFindingsMessages should process valid findings and verify mappedFindings populated`() { + val mockVirtualFile = mock { + on { isDirectory } doReturn false + } + val mockDocument = mock { + on { lineCount } doReturn 5 + on { getLineStartOffset(0) } doReturn 0 + on { getLineEndOffset(0) } doReturn 10 + } + + mockStatic(LocalFileSystem::class.java).use { localFileSystemMock -> + localFileSystemMock.`when` { LocalFileSystem.getInstance() }.thenReturn(mockLocalFileSystem) + whenever(mockLocalFileSystem.findFileByIoFile(any())) doReturn mockVirtualFile + + mockStatic(FileDocumentManager::class.java).use { fileDocumentManagerMock -> + fileDocumentManagerMock.`when` { FileDocumentManager.getInstance() } doReturn mockFileDocumentManager + whenever(mockFileDocumentManager.getDocument(mockVirtualFile)) doReturn mockDocument + whenever(mockCodeScanManager.isIgnoredIssue(any(), any(), any(), any())) doReturn false + + val issue = BrowserConnector.FlareCodeScanIssue( + startLine = 1, endLine = 1, comment = "Test comment", title = "Test Issue", + description = Description("Test description", "Test text"), detectorId = "test-detector", + detectorName = "Test Detector", findingId = "test-finding-id", ruleId = "test-rule", + relatedVulnerabilities = listOf("CVE-2023-1234"), severity = "HIGH", + recommendation = Recommendation("Fix this", "https://example.com"), + suggestedFixes = listOf(SuggestedFix("Fix code", "Fixed code")), + scanJobId = "test-job-id", language = "kotlin", autoDetected = false, + filePath = "/test/file.kt", findingContext = "test context" + ) + + val aggregatedIssue = BrowserConnector.AggregatedCodeScanIssue("/test/file.kt", listOf(issue)) + val findingsMessage = mapOf( + "messageId" to "test_codeReviewFindings", + "body" to jacksonObjectMapper().writeValueAsString(listOf(aggregatedIssue)) + ) + val additionalMessages = mutableListOf>(findingsMessage) + val messagesMap = mapOf("additionalMessages" to additionalMessages) + + browserConnector.parseFindingsMessages(messagesMap) + + val issuesCaptor = argumentCaptor>() + verify(mockCodeScanManager).addOnDemandIssues( + issuesCaptor.capture(), + any(), + eq(CodeWhispererConstants.CodeAnalysisScope.AGENTIC) + ) + + assertThat(additionalMessages).isEmpty() + assertThat(issuesCaptor.firstValue.isNotEmpty()) + assertThat(issuesCaptor.firstValue[0].title == "Test Issue") + } + } + } + + @Test + fun `parseFindingsMessages should skip directory files and not populate mappedFindings`() { + val mockDirectoryFile = mock { on { isDirectory } doReturn true } + + mockStatic(LocalFileSystem::class.java).use { localFileSystemMock -> + localFileSystemMock.`when` { LocalFileSystem.getInstance() } doReturn mockLocalFileSystem + whenever(mockLocalFileSystem.findFileByIoFile(any())) doReturn mockDirectoryFile + + val issue = BrowserConnector.FlareCodeScanIssue( + startLine = 1, endLine = 1, comment = null, title = "Test Issue", + description = Description("Test description", "Test text"), detectorId = "test-detector", + detectorName = "Test Detector", findingId = "test-finding-id", ruleId = null, + relatedVulnerabilities = emptyList(), severity = "MEDIUM", + recommendation = Recommendation("Fix this", "https://example.com"), suggestedFixes = emptyList(), + scanJobId = "test-job-id", language = "kotlin", autoDetected = true, + filePath = "/test/directory", findingContext = "test context" + ) + + val aggregatedIssue = BrowserConnector.AggregatedCodeScanIssue("/test/directory", listOf(issue)) + val findingsMessage = mapOf( + "messageId" to "test_displayFindings", + "body" to jacksonObjectMapper().writeValueAsString(listOf(aggregatedIssue)) + ) + val additionalMessages = mutableListOf>(findingsMessage) + val messagesMap = mapOf("additionalMessages" to additionalMessages) + + browserConnector.parseFindingsMessages(messagesMap) + + val issuesCaptor = argumentCaptor>() + verify(mockCodeScanManager).addOnDemandIssues( + issuesCaptor.capture(), + any(), + eq(CodeWhispererConstants.CodeAnalysisScope.AGENTIC) + ) + + assertThat(issuesCaptor.firstValue.isEmpty()) + assertThat(additionalMessages).isEmpty() + } + } + + @Test + fun `parseFindingsMessages should handle invalid JSON gracefully`() { + val findingsMessage = mapOf( + "messageId" to "test_codeReviewFindings", + "body" to "invalid json" + ) + val additionalMessages = mutableListOf>(findingsMessage) + val messagesMap = mapOf("additionalMessages" to additionalMessages) + + browserConnector.parseFindingsMessages(messagesMap) + + assertThat(additionalMessages).isEmpty() + } +} diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml index 24de37b55d9..88a52fd16bc 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml +++ b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml @@ -108,8 +108,8 @@ + id="aws.amazonq.handleCodeScanIssueCommand" + class="software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions.HandleIssueCommandAction"/> diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanIssueDetailsPanel.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanIssueDetailsPanel.kt index 8d5eb99d90d..0423128fe3d 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanIssueDetailsPanel.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanIssueDetailsPanel.kt @@ -15,14 +15,10 @@ import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.project.Project import com.intellij.ui.components.JBScrollPane import com.intellij.util.Alarm -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.context.CodeScanIssueDetailsDisplayType import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.additionBackgroundColor import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.additionForegroundColor -import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.applySuggestedFix +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.applyFix import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBackgroundColor import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBorderColor import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockForegroundColor @@ -34,15 +30,12 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.get import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.metaBackgroundColor import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.metaForegroundColor import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.openDiff -import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.sendCodeFixGeneratedTelemetryToServiceAPI import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.truncateIssueTitle import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.getHexString -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.resources.message import software.aws.toolkits.telemetry.Component -import software.aws.toolkits.telemetry.MetricResult import java.awt.BorderLayout import java.awt.Dimension import java.awt.datatransfer.StringSelection @@ -57,116 +50,14 @@ import javax.swing.ScrollPaneConstants import javax.swing.event.HyperlinkEvent import javax.swing.text.html.HTMLEditorKit -private val logger = getLogger() internal class CodeWhispererCodeScanIssueDetailsPanel( private val project: Project, issue: CodeWhispererCodeScanIssue, - private val defaultScope: CoroutineScope, ) : JPanel(BorderLayout()) { private val kit = HTMLEditorKit() private val doc = kit.createDefaultDocument() - private val amazonQCodeFixSession = AmazonQCodeFixSession(project) private val codeScanManager = CodeWhispererCodeScanManager.getInstance(project) - private suspend fun handleGenerateFix(issue: CodeWhispererCodeScanIssue, isRegenerate: Boolean = false) { - if (issue.ruleId == "sbom-software-assurance-services") { - logger.warn { "GenerateFix is not available for SAS findings." } - return - } - editorPane.text = getCodeScanIssueDetailsHtml( - issue, CodeScanIssueDetailsDisplayType.DetailsPane, CodeWhispererConstants.FixGenerationState.GENERATING, - project = project - ) - editorPane.revalidate() - editorPane.repaint() - runInEdt { - editorPane.scrollToReference("fixLoadingSection") - } - - val codeFixResponse: AmazonQCodeFixSession.CodeFixResponse = amazonQCodeFixSession.runCodeFixWorkflow(issue) - if (codeFixResponse.failureResponse != null) { - editorPane.apply { - text = getCodeScanIssueDetailsHtml( - issue, CodeScanIssueDetailsDisplayType.DetailsPane, CodeWhispererConstants.FixGenerationState.FAILED, - project = project - ) - revalidate() - repaint() - runInEdt { - scrollToReference("fixFailureSection") - } - } - CodeWhispererTelemetryService.getInstance().sendCodeScanIssueGenerateFix( - Component.Webview, - issue, - isRegenerate, - MetricResult.Failed, - codeFixResponse.failureResponse - ) - } else { - val isReferenceAllowed = CodeWhispererSettings.getInstance().isIncludeCodeWithReference() - var suggestedFix = SuggestedFix( - code = "", - description = "" - ) - codeFixResponse.getCodeFixJobResponse?.suggestedFix()?.let { - it.codeDiff()?.let { codeDiff -> - // TODO: enable later - if (it.references() == null || it.references()?.isEmpty() == true) { - suggestedFix = SuggestedFix( - code = codeDiff.split("\n").drop(2).joinToString("\n"), // drop first two comment lines - description = it.description(), - codeFixJobId = codeFixResponse.jobId, - references = it.references(), - ) - } - } - } - - val showReferenceWarning = !isReferenceAllowed && suggestedFix.references.isNotEmpty() - if (suggestedFix.code.isNotEmpty() && !showReferenceWarning) { - issue.suggestedFixes = listOf(suggestedFix) - codeScanManager.updateIssue(issue) - } - - editorPane.apply { - text = getCodeScanIssueDetailsHtml( - issue, CodeScanIssueDetailsDisplayType.DetailsPane, project = project, - showReferenceWarning = showReferenceWarning - ) - revalidate() - repaint() - runInEdt { - scrollToReference("codeFixActions") - } - } - - buttonPane.apply { - removeAll() - if (issue.suggestedFixes.isNotEmpty()) add(applyFixButton) - add(regenerateFixButton) - add(explainIssueButton) - add(ignoreIssueButton) - add(ignoreIssuesButton) - add(Box.createHorizontalGlue()) - revalidate() - repaint() - } - ApplicationManager.getApplication().executeOnPooledThread { - if (suggestedFix.code.isNotBlank()) { - sendCodeFixGeneratedTelemetryToServiceAPI(issue, false) - } - CodeWhispererTelemetryService.getInstance().sendCodeScanIssueGenerateFix( - Component.Webview, - issue, - isRegenerate, - MetricResult.Succeeded, - includesFix = suggestedFix.code.isNotBlank() - ) - } - } - } - private val editorPane = JEditorPane().apply { contentType = "text/html" putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true) @@ -235,26 +126,8 @@ internal class CodeWhispererCodeScanIssueDetailsPanel( font = font.deriveFont(16f) } private val applyFixButton = JButton(message("codewhisperer.codescan.apply_fix_button_label")).apply { - putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) addActionListener { - applySuggestedFix(project, issue) - } - } - private val generateFixButton = JButton(message("codewhisperer.codescan.generate_fix_button_label")).apply { - putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) - isEnabled = issue.ruleId != "sbom-software-assurance-services" - addActionListener { - defaultScope.launch { - handleGenerateFix(issue) - } - } - } - private val regenerateFixButton = JButton(message("codewhisperer.codescan.regenerate_fix_button_label")).apply { - putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) - addActionListener { - defaultScope.launch { - handleGenerateFix(issue, isRegenerate = true) - } + applyFix(issue) } } private val explainIssueButton = JButton(message("codewhisperer.codescan.explain_button_label")).apply { @@ -298,9 +171,8 @@ internal class CodeWhispererCodeScanIssueDetailsPanel( private val buttonPane = JPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) preferredSize = Dimension(this.width, 30) - if (issue.suggestedFixes.isNotEmpty()) add(applyFixButton) - if (issue.suggestedFixes.isNotEmpty()) add(regenerateFixButton) else add(generateFixButton) add(explainIssueButton) + add(applyFixButton) add(ignoreIssueButton) add(ignoreIssuesButton) add(Box.createHorizontalGlue()) @@ -333,10 +205,5 @@ internal class CodeWhispererCodeScanIssueDetailsPanel( add(BorderLayout.SOUTH, buttonPane) isVisible = true revalidate() - if (issue.suggestedFixes.isEmpty()) { - defaultScope.launch { - handleGenerateFix(issue) - } - } } } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanManager.kt index 839c0c3b3e8..b00a4c0d124 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanManager.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanManager.kt @@ -110,7 +110,7 @@ private val LOG = getLogger() class CodeWhispererCodeScanManager(val project: Project) { private val defaultScope = projectCoroutineScope(project) private val codeScanResultsPanel by lazy { - CodeWhispererCodeScanResultsView(project, defaultScope) + CodeWhispererCodeScanResultsView(project) } private val codeScanIssuesContent by lazy { val contentManager = getProblemsWindow().contentManager @@ -537,6 +537,16 @@ class CodeWhispererCodeScanManager(val project: Project) { ondemandScanIssues = ondemandScanIssues.filter { it.findingId != issue.findingId } } + fun addOnDemandIssues(issues: List, scannedFiles: List, scope: CodeWhispererConstants.CodeAnalysisScope) = + defaultScope.launch { + ondemandScanIssues = ondemandScanIssues + issues + renderResponseOnUIThread( + getCombinedScanIssues(), + scannedFiles, + scope + ) + } + fun removeIssueByFindingId(issue: CodeWhispererCodeScanIssue, findingId: String) { scanNodesLookup[issue.file]?.forEach { node -> val issueNode = node.userObject as CodeWhispererCodeScanIssue diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt index d05e24907f7..3de0cd85152 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanResultsView.kt @@ -20,7 +20,6 @@ import com.intellij.ui.components.ActionLink import com.intellij.ui.treeStructure.Tree import com.intellij.util.ui.JBUI import icons.AwsIcons -import kotlinx.coroutines.CoroutineScope import software.aws.toolkits.core.utils.error import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueGroupingStrategy @@ -48,7 +47,7 @@ import javax.swing.tree.TreePath /** * Create a Code Scan results view that displays the code scan results. */ -internal class CodeWhispererCodeScanResultsView(private val project: Project, private val defaultScope: CoroutineScope) : JPanel(BorderLayout()) { +internal class CodeWhispererCodeScanResultsView(private val project: Project) : JPanel(BorderLayout()) { private fun isGroupedBySeverity() = CodeWhispererCodeScanManager.getInstance(project).getGroupingStrategySelected() == IssueGroupingStrategy.SEVERITY @@ -59,7 +58,7 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr val issueNode = e.path.lastPathComponent as? DefaultMutableTreeNode val issue = issueNode?.userObject as? CodeWhispererCodeScanIssue ?: return@addTreeSelectionListener - showIssueDetails(issue, defaultScope) + showIssueDetails(issue) synchronized(issueNode) { if (issueNode.userObject !is CodeWhispererCodeScanIssue) return@addTreeSelectionListener @@ -300,8 +299,8 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project, pr } } - private fun showIssueDetails(issue: CodeWhispererCodeScanIssue, defaultScope: CoroutineScope) { - val issueDetailsViewPanel = CodeWhispererCodeScanIssueDetailsPanel(project, issue, defaultScope) + private fun showIssueDetails(issue: CodeWhispererCodeScanIssue) { + val issueDetailsViewPanel = CodeWhispererCodeScanIssueDetailsPanel(project, issue) issueDetailsViewPanel.apply { isVisible = true revalidate() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt index ef6d75d4fda..e4730aa6af4 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt @@ -25,6 +25,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhisp import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.context.CodeScanIssueDetailsDisplayType import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.additionBackgroundColor import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.additionForegroundColor +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.applyFix import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.applySuggestedFix import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBackgroundColor import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBorderColor @@ -182,12 +183,21 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec val explainButton = JButton( message("codewhisperer.codescan.explain_button_label") ).apply { - toolTipText = message("codewhisperer.codescan.apply_fix_button_tooltip") + toolTipText = message("codewhisperer.codescan.explain_button_tooltip") addActionListener { hidePopup() explainIssue(issue) } } + val applyFixButton = JButton( + message("codewhisperer.codescan.apply_fix_button_label") + ).apply { + toolTipText = message("codewhisperer.codescan.apply_fix_button_tooltip") + addActionListener { + hidePopup() + applyFix(issue) + } + } val titlePane = JPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) @@ -204,6 +214,7 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec add(button) } add(explainButton) + add(applyFixButton) // Add glue before and after label to center it add(Box.createHorizontalGlue()) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/sessionconfig/CodeScanSessionConfig.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/sessionconfig/CodeScanSessionConfig.kt index 7cb698cd69f..2ae5ca13dd9 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/sessionconfig/CodeScanSessionConfig.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/sessionconfig/CodeScanSessionConfig.kt @@ -107,6 +107,7 @@ class CodeScanSessionConfig( null -> getProjectPayloadMetadata() else -> when (scope) { CodeAnalysisScope.PROJECT -> getProjectPayloadMetadata() + CodeAnalysisScope.AGENTIC -> getProjectPayloadMetadata() CodeAnalysisScope.FILE -> if (selectedFile.isWithin(projectRoot)) { getFilePayloadMetadata(selectedFile, true) } else { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt index 148ef85ab87..31444fec641 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.intellij.diff.DiffContentFactory import com.intellij.diff.DiffManager import com.intellij.diff.requests.SimpleDiffRequest @@ -63,7 +64,8 @@ val metaBackgroundColor = JBColor.namedColor("FileColor.Blue", JBColor(0xeaf6ff, val metaForegroundColor = JBColor.namedColor("Label.infoForeground", JBColor(0x808080, 0x8C8C8C)) private val LOG = getLogger() -private val explainIssueDataKey = DataKey.create>("amazonq.codescan.explainissue") +private val hanldeIssueCommandContextDataKey = DataKey.create>("amazonq.codescan.handleIssueCommandContext") +private val hanldeIssueCommandActionDataKey = DataKey.create("amazonq.codescan.handleIssueCommandAction") enum class IssueSeverity(val displayName: String) { CRITICAL("Critical"), @@ -78,6 +80,11 @@ enum class IssueGroupingStrategy(val displayName: String) { FILE_LOCATION("File Location"), } +private enum class IssueCommandAction(val displayName: String) { + EXPLAIN_ISSUE("explainIssue"), + APPLY_FIX("applyFix"), +} + fun getCodeScanIssueDetailsHtml( issue: CodeWhispererCodeScanIssue, display: CodeScanIssueDetailsDisplayType, @@ -228,18 +235,40 @@ private fun createSuggestedFixSection(issue: CodeWhispererCodeScanIssue, suggest } fun explainIssue(issue: CodeWhispererCodeScanIssue) { - val explainIssueContext = mutableMapOf( + handleIssueCommand(issue, IssueCommandAction.EXPLAIN_ISSUE) +} + +fun applyFix(issue: CodeWhispererCodeScanIssue) { + handleIssueCommand(issue, IssueCommandAction.APPLY_FIX) +} + +private fun handleIssueCommand(issue: CodeWhispererCodeScanIssue, action: IssueCommandAction) { + val handleIssueCommandContext = mutableMapOf( "title" to issue.title, "description" to issue.description.markdown, - "code" to issue.codeText + "code" to issue.codeText, + "fileName" to issue.file.name, + "startLine" to issue.startLine.toString(), + "endLine" to issue.endLine.toString(), + "recommendation" to jacksonObjectMapper().writeValueAsString(issue.recommendation), + "suggestedFixes" to jacksonObjectMapper().writeValueAsString(issue.suggestedFixes), + "codeSnippet" to jacksonObjectMapper().writeValueAsString(issue.codeSnippet), + "findingId" to issue.findingId, + "ruleId" to issue.ruleId.orEmpty(), + "detectorId" to issue.detectorId, + "autoDetected" to issue.autoDetected.toString(), ) val actionEvent = AnActionEvent.createFromInputEvent( null, ToolkitPlaces.EDITOR_PSI_REFERENCE, null, - SimpleDataContext.builder().add(explainIssueDataKey, explainIssueContext).add(CommonDataKeys.PROJECT, issue.project).build() + SimpleDataContext.builder() + .add(hanldeIssueCommandContextDataKey, handleIssueCommandContext) + .add(CommonDataKeys.PROJECT, issue.project) + .add(hanldeIssueCommandActionDataKey, action.displayName) + .build() ) - ActionManager.getInstance().getAction("aws.amazonq.explainCodeScanIssue").actionPerformed(actionEvent) + ActionManager.getInstance().getAction("aws.amazonq.handleCodeScanIssueCommand").actionPerformed(actionEvent) } fun openDiff(issue: CodeWhispererCodeScanIssue) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt index 4d034d0a987..897df646a1f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt @@ -148,6 +148,7 @@ class CodeWhispererTelemetryService(private val cs: CoroutineScope) { } } CodeWhispererConstants.CodeAnalysisScope.PROJECT -> CodewhispererCodeScanScope.PROJECT + CodeWhispererConstants.CodeAnalysisScope.AGENTIC -> CodewhispererCodeScanScope.PROJECT } fun sendSecurityScanEvent(codeScanEvent: CodeScanTelemetryEvent, project: Project? = null) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt index c8da6b85a69..98252f08aa7 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt @@ -120,6 +120,7 @@ object CodeWhispererConstants { enum class CodeAnalysisScope(val value: String) { FILE("FILE"), PROJECT("PROJECT"), + AGENTIC("AGENTIC"), } enum class FeatureName(val value: String) { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt index a4481219033..0fbb1553a21 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt @@ -25,7 +25,9 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LSPAny import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.AuthFollowUpClickedParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.AuthFollowupType import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_ERROR_PARAMS +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CODE_REVIEW_FINDINGS_SUFFIX import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatMessage +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DISPLAY_FINDINGS_SUFFIX import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ErrorParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSerializedChatResult import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT @@ -141,6 +143,18 @@ class ChatCommunicationManager(private val project: Project, private val cs: Cor Gson().fromJson(partialChatResult, Map::class.java) } ?: partialChatResult + if (partialResultMap is Map<*, *>) { + val additionalMessages = partialResultMap["additionalMessages"] as? MutableList> + additionalMessages?.removeAll { + val messageId = it["messageId"] as? String + messageId != null && + ( + messageId.endsWith(CODE_REVIEW_FINDINGS_SUFFIX) || + messageId.endsWith(DISPLAY_FINDINGS_SUFFIX) + ) + } + } + notifyUi( FlareUiMessage( command = SEND_CHAT_COMMAND_PROMPT, diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/ExtendedClientMetadata.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/ExtendedClientMetadata.kt index 938b69cbc85..cabb37492ad 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/ExtendedClientMetadata.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/ExtendedClientMetadata.kt @@ -27,6 +27,8 @@ data class QCapabilities( val pinnedContextEnabled: Boolean, val imageContextEnabled: Boolean, val reroute: Boolean, + val codeReviewInChat: Boolean, + val displayFindings: Boolean, val workspaceFilePath: String?, ) @@ -71,6 +73,8 @@ fun createExtendedClientMetadata(project: Project): ExtendedClientMetadata { pinnedContextEnabled = true, imageContextEnabled = true, reroute = true, + codeReviewInChat = true, + displayFindings = true, workspaceFilePath = project.workspaceFile?.path, ), window = WindowCapabilities( diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/AwsServerCapabilities.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/AwsServerCapabilities.kt index 46d5412f4f8..2de0e0abcef 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/AwsServerCapabilities.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/AwsServerCapabilities.kt @@ -15,6 +15,8 @@ data class ChatOptions( val export: Boolean, val mcpServers: Boolean, val reroute: Boolean, + val codeReviewInChat: Boolean, + val displayFindings: Boolean, val showLogs: Boolean, ) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatMessage.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatMessage.kt index b22268c4ea4..521d4607db7 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatMessage.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatMessage.kt @@ -109,3 +109,6 @@ enum class MessageType(@JsonValue val repr: String) { DIRECTIVE("directive"), TOOL("tool"), } + +const val CODE_REVIEW_FINDINGS_SUFFIX = "_codeReviewFindings" +const val DISPLAY_FINDINGS_SUFFIX = "_displayFindings" diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 122ee65b787..fad46030a77 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -878,8 +878,8 @@ codewhisperer.actions.view_documentation.title=View Documentation codewhisperer.codefix.code_fix_job_timed_out=Amazon Q: Timed out generating code fix codewhisperer.codefix.create_code_fix_error=Amazon Q: Failed to generate fix for the issue codewhisperer.codefix.invalid_zip_error=Amazon Q: Failed to create valid zip -codewhisperer.codescan.apply_fix_button_label=Apply fix -codewhisperer.codescan.apply_fix_button_tooltip=Apply suggested fix +codewhisperer.codescan.apply_fix_button_label=Fix +codewhisperer.codescan.apply_fix_button_tooltip=Generate and apply fix codewhisperer.codescan.build_artifacts_not_found=Cannot find build artifacts for the project. Try rebuilding the Java project in IDE or specify compilation output path in File | Project Structure... | Project | Compiler output: codewhisperer.codescan.cancelled_by_user_exception=Code review job cancelled by user. codewhisperer.codescan.cannot_read_file=Amazon Q encountered an error while parsing a file. @@ -887,6 +887,7 @@ codewhisperer.codescan.clear_filters=Clear Filters codewhisperer.codescan.cwe_label=Common Weakness Enumeration (CWE) codewhisperer.codescan.detector_library_label=Detector library codewhisperer.codescan.explain_button_label=Explain +codewhisperer.codescan.explain_button_tooltip=Explain Issue codewhisperer.codescan.file_ext_not_supported=File extension {0} is not supported for the Amazon Q Code Review feature. Please try again with a valid file format - java, python, javascript, typescript, csharp, yaml, json, tf, hcl, ruby, go. codewhisperer.codescan.file_name_issues_count= {0} {1} {2, choice, 1#1 issue|2#{2,number} issues} codewhisperer.codescan.file_not_found=For file path {0} with error message: {0}