-
Notifications
You must be signed in to change notification settings - Fork 273
feat(amazonq): add support for CodeReview and DisplayFindings tools, alter behavior of explain and fix buttons #5970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 30 commits
f76d69d
b05c4f1
99d8433
ec2d4fd
19f51a9
d28f486
ea41226
d37106e
fe668ee
f21c028
683189f
45b7fe2
2588777
7d4404d
5e878f9
b26778c
5d2c6f5
8ed39dd
b0ebef0
3725505
e913055
f8e45e8
9819471
685db1a
7baa5f2
683944b
98e8b3c
9debeac
2a007ad
718153b
f675828
f9c0976
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"type" : "feature", | ||
"description" : "Enable agentic code review" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,9 +10,13 @@ | |
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_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,13 +113,21 @@ | |
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 | ||
import kotlin.collections.get | ||
|
||
class BrowserConnector( | ||
private val serializer: MessageSerializer = MessageSerializer.getInstance(), | ||
|
@@ -568,6 +582,32 @@ | |
} | ||
} | ||
|
||
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<String>, | ||
val severity: String, | ||
val recommendation: Recommendation, | ||
val suggestedFixes: List<SuggestedFix>, | ||
val scanJobId: String, | ||
val language: String, | ||
val autoDetected: Boolean, | ||
val filePath: String, | ||
val findingContext: String, | ||
) | ||
|
||
data class AggregatedCodeScanIssue( | ||
val filePath: String, | ||
val issues: List<FlareCodeScanIssue>, | ||
) | ||
|
||
private fun showResult( | ||
result: CompletableFuture<String>, | ||
partialResultToken: String, | ||
|
@@ -581,10 +621,14 @@ | |
throw error | ||
} | ||
chatCommunicationManager.removePartialChatMessage(partialResultToken) | ||
val decryptedMessage = Gson().fromJson(value?.let { encryptionManager?.decrypt(it) }.orEmpty(), Map::class.java) | ||
as Map<String, *> | ||
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 +642,85 @@ | |
} | ||
} | ||
|
||
fun parseFindingsMessages(messagesMap: Map<String, *>) { | ||
try { | ||
val additionalMessages = messagesMap["additionalMessages"] as? MutableList<Map<String, Any>> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this any? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The typing is very complex and only the shape of additionalMessages matters. Even then I am not confident in my ability to know the shape of every possible entry here. Only the shape of the findings message matters. Introducing forced shapes would be too risky in my opinion |
||
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<VirtualFile>() | ||
if (findingsMessages != null) { | ||
for (findingsMessage in findingsMessages) { | ||
additionalMessages.remove(findingsMessage) | ||
val gson = Gson() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: would recommend using the same instance of Gson throughout the class There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will add in follow up PR |
||
val jsonFindings = gson.fromJson(findingsMessage["body"] as String, List::class.java) | ||
val mappedFindings = mutableListOf<CodeWhispererCodeScanIssue>() | ||
for (aggregatedIssueUnformatted in jsonFindings) { | ||
val aggregatedIssue = gson.fromJson(gson.toJson(aggregatedIssueUnformatted), AggregatedCodeScanIssue::class.java) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are receiving a List of AggregatedCodeScanIssues as a string from FLARE message, this is formatting them and putting them into a List object. |
||
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) | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -17,34 +17,56 @@ | |||||
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 { | ||||||
Check warning on line 23 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/HandleIssueCommandAction.kt
|
||||||
|
||||||
val contextDataKey = DataKey.create<MutableMap<String, String>>("amazonq.codescan.handleIssueCommandContext") | ||||||
val actionDataKey = DataKey.create<String>("amazonq.codescan.handleIssueCommandAction") | ||||||
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will add in follow up PR |
||||||
|
||||||
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, String>): 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<MutableMap<String, String>>("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, | ||||||
|
Uh oh!
There was an error while loading. Please reload this page.