From d3ba908496e82f4ab9ce257cfd91d2b813ed3868 Mon Sep 17 00:00:00 2001
From: samgst-amazon
Date: Tue, 3 Dec 2024 06:15:47 -0800
Subject: [PATCH] reinvent 2024
---
...-21701bb3-5189-474e-868b-9ec46ecde6ee.json | 4 -
...-44636712-ede6-4e2a-b4d6-ca2b98da003d.json | 4 +
...-5c2fae3e-c794-438b-8af5-2c31c00ab000.json | 4 +
...-8fc6f4c9-6976-4a4a-88f0-bd4e6081a4b9.json | 4 +
.run/Run Amazon Q - Ultimate [2024.1].run.xml | 2 +-
.../kotlin/toolkit-generate-sdks.gradle.kts | 2 +
gradle/libs.versions.toml | 2 +-
.../resources/META-INF/plugin-chat.xml | 3 +
.../clients/AmazonQCodeGenerateClient.kt | 153 ++
.../session/ConversationNotStartedState.kt | 21 +
.../jetbrains/common/session/SessionState.kt | 17 +
.../common/session/SessionStateTypes.kt | 24 +
.../common/util/AmazonQCodeGenService.kt | 214 +++
.../jetbrains/common/util/FileUtils.kt | 82 ++
.../services/amazonq/QOpenPanelAction.kt | 5 +
.../amazonq/toolwindow/AmazonQToolWindow.kt | 33 +-
.../services/amazonq/webview/Browser.kt | 31 +-
.../amazonq/webview/BrowserConnector.kt | 28 +-
.../amazonqCodeScan/CodeScanChatApp.kt | 175 +++
.../amazonqCodeScan/CodeScanChatAppFactory.kt | 12 +
.../amazonqCodeScan/CodeScanChatItems.kt | 164 +++
.../amazonqCodeScan/CodeScanConstants.kt | 6 +
.../InboundAppMessagesHandler.kt | 33 +
.../amazonqCodeScan/auth/CodeScanAuthUtils.kt | 11 +
.../commands/CodeScanActionMessage.kt | 16 +
.../commands/CodeScanCommand.kt | 8 +
.../commands/CodeScanMessageListener.kt | 21 +
.../controller/CodeScanChatController.kt | 197 +++
.../controller/CodeScanChatHelper.kt | 81 ++
.../messages/CodeScanMessage.kt | 185 +++
.../amazonqCodeScan/session/Session.kt | 9 +
.../storage/ChatSessionStorage.kt | 33 +
.../amazonqCodeTest/CodeTestChatApp.kt | 87 ++
.../amazonqCodeTest/CodeTestChatAppFactory.kt | 11 +
.../amazonqCodeTest/CodeTestChatItems.kt | 38 +
.../amazonqCodeTest/CodeTestConstants.kt | 19 +
.../CodeWhispererCodeTestSession.kt | 149 ++
.../CodeWhispererUTGChatManager.kt | 560 ++++++++
.../InboundAppMessagesHandler.kt | 24 +
.../amazonqCodeTest/auth/CodeTestAuthUtils.kt | 11 +
.../controller/CodeTestChatController.kt | 1252 +++++++++++++++++
.../controller/CodeTestChatHelper.kt | 156 ++
.../messages/CodeTestMessage.kt | 241 ++++
.../model/BuildAndExecuteStatusIcon.kt | 39 +
.../model/PreviousUTGIterationContext.kt | 13 +
.../amazonqCodeTest/model/ShortAnswer.kt | 41 +
.../amazonqCodeTest/session/Session.kt | 69 +
.../storage/ChatSessionStorage.kt | 25 +
.../amazonqCodeTest/utils/UTGChatUtil.kt | 200 +++
.../jetbrains/services/amazonqDoc/DocApp.kt | 99 ++
.../services/amazonqDoc/DocAppFactory.kt | 11 +
.../services/amazonqDoc/DocChatItems.kt | 45 +
.../services/amazonqDoc/DocConstants.kt | 27 +
.../services/amazonqDoc/DocExceptions.kt | 25 +
.../amazonqDoc/InboundAppMessagesHandler.kt | 21 +
.../services/amazonqDoc/auth/DocAuthUtils.kt | 16 +
.../amazonqDoc/controller/DocController.kt | 1114 +++++++++++++++
.../controller/DocControllerExtensions.kt | 123 ++
.../controller/DocGenerationTask.kt | 62 +
.../amazonqDoc/messages/DocMessage.kt | 287 ++++
.../messages/DocMessagePublisherExtensions.kt | 215 +++
.../amazonqDoc/session/DocGenerationState.kt | 288 ++++
.../services/amazonqDoc/session/DocSession.kt | 235 ++++
.../session/PrepareDocGenerationState.kt | 105 ++
.../amazonqDoc/session/SessionStateTypes.kt | 27 +
.../amazonqDoc/storage/ChatSessionStorage.kt | 26 +
.../amazonqDoc/util/DocControllerUtil.kt | 33 +
.../amazonqFeatureDev/FeatureDevApp.kt | 8 +-
.../controller/FeatureDevController.kt | 2 +-
.../messages/FeatureDevMessage.kt | 3 +
.../session/CodeGenerationState.kt | 8 +-
.../session/ConversationNotStartedState.kt | 2 +-
.../session/PrepareCodeGenerationState.kt | 2 +-
.../amazonqFeatureDev/session/Session.kt | 4 +-
.../amazonqFeatureDev/session/SessionState.kt | 2 +-
.../session/SessionStateTypes.kt | 4 +-
.../cwc/clients/chat/model/Responses.kt | 3 +
.../cwc/commands/GenerateUnitTestsAction.kt | 18 +-
.../actions/CodeScanCompleteAction.kt | 21 +
.../services/cwc/controller/ChatController.kt | 31 +-
.../chat/userIntent/UserIntentRecognizer.kt | 8 +-
.../cwc/inline/InlineChatController.kt | 2 +-
.../services/cwc/messages/CwcMessage.kt | 5 +
.../controller/FeatureDevControllerTest.kt | 8 +-
.../amazonqFeatureDev/session/SessionTest.kt | 6 +-
.../codemodernizer/CodeTransformChatApp.kt | 9 +-
.../messages/CodeTransformMessage.kt | 3 +
.../CodeWhispererIntegrationTestBase.kt | 3 +-
.../META-INF/plugin-codewhisperer.xml | 17 +-
.../codescan/AmazonQCodeFixSession.kt | 235 ++++
.../CodeWhispererCodeScanException.kt | 2 +
...WhispererCodeScanHighlightingFilesPanel.kt | 2 +-
.../CodeWhispererCodeScanIssueDetailsPanel.kt | 306 ++++
.../codescan/CodeWhispererCodeScanManager.kt | 613 +++++---
.../CodeWhispererCodeScanResultsView.kt | 103 +-
.../codescan/CodeWhispererCodeScanSession.kt | 246 +---
.../CodeWhispererCodeScanFilterGroup.kt | 39 +
.../actions/CodeWhispererCodeScanRunAction.kt | 15 +-
.../CodeWhispererStopCodeScanAction.kt | 30 -
.../CodeScanIssueDetailsDisplayType.kt | 8 +
.../CodeWhispererCodeScanDocumentListener.kt | 1 +
...spererCodeScanEditorMouseMotionListener.kt | 303 +---
.../sessionconfig/CodeScanSessionConfig.kt | 121 +-
.../utils/AmazonQCodeReviewGitUtils.kt | 141 ++
.../utils/CodeWhispererCodeScanIssueUtils.kt | 441 ++++++
.../sessionconfig/CodeTestSessionConfig.kt | 247 ++++
.../credentials/CodeWhispererClientAdaptor.kt | 205 +++
.../explorer/QStatusBarLoggedInActionGroup.kt | 4 +-
.../explorer/actions/ActionFactory.kt | 9 +-
.../CodeWhispererProgrammingLanguage.kt | 6 +
.../language/languages/CodeWhispererJson.kt | 6 +
.../languages/CodeWhispererPlainText.kt | 6 +
.../language/languages/CodeWhispererPython.kt | 6 +
.../language/languages/CodeWhispererRuby.kt | 6 +
.../language/languages/CodeWhispererShell.kt | 6 +
.../language/languages/CodeWhispererTf.kt | 2 +
.../languages/CodeWhispererUnknownLanguage.kt | 6 +
.../language/languages/CodeWhispererYaml.kt | 6 +
.../codewhisperer/model/CodeWhispererModel.kt | 8 +-
.../settings/CodeWhispererConfigurable.kt | 14 +
...WhispererProjectStartupSettingsListener.kt | 7 +-
.../CodeWhispererTelemetryService.kt | 43 +-
.../util/CodeWhispererConstants.kt | 25 +-
.../codewhisperer/util/CodeWhispererUtil.kt | 16 +
.../util/CodeWhispererZipUploadManager.kt | 156 ++
.../CodeWhispererConfigurableTest.kt | 5 +-
.../codescan/CodeWhispererCodeFileScanTest.kt | 55 +-
.../codescan/CodeWhispererCodeScanTest.kt | 53 +-
.../codescan/CodeWhispererCodeScanTestBase.kt | 102 +-
.../CodeWhispererProjectCodeScanTest.kt | 4 +-
.../ui/apps/amazonqCommonsConnector.ts | 67 +-
.../mynah-ui/ui/apps/codeScanChatConnector.ts | 183 +++
.../mynah-ui/ui/apps/codeTestChatConnector.ts | 584 ++++++++
.../ui/apps/codeTransformChatConnector.ts | 18 +-
.../src/mynah-ui/ui/apps/docChatConnector.ts | 397 ++++++
.../ui/apps/featureDevChatConnector.ts | 19 +-
.../mynah-ui/src/mynah-ui/ui/commands.ts | 14 +
.../mynah-ui/src/mynah-ui/ui/connector.ts | 174 ++-
.../src/mynah-ui/ui/followUps/generator.ts | 26 +
.../src/mynah-ui/ui/followUps/handler.ts | 1 +
.../src/mynah-ui/ui/forms/constants.ts | 33 +
.../amazonq/mynah-ui/src/mynah-ui/ui/main.ts | 229 ++-
.../src/mynah-ui/ui/messages/controller.ts | 6 +
.../src/mynah-ui/ui/quickActions/generator.ts | 89 +-
.../src/mynah-ui/ui/quickActions/handler.ts | 218 ++-
.../src/mynah-ui/ui/storages/tabsStorage.ts | 33 +-
.../src/mynah-ui/ui/tabs/generator.ts | 22 +-
.../src/mynah-ui/ui/telemetry/actions.ts | 38 +
.../src/mynah-ui/ui/texts/constants.ts | 6 +
.../src/mynah-ui/ui/walkthrough/agent.ts | 194 +++
.../src/mynah-ui/ui/walkthrough/welcome.ts | 46 +
.../settings/CodeWhispererSettings.kt | 17 +
.../src/main/resources/META-INF/plugin.xml | 55 +-
.../severity-initial-critical.svg | 4 +
.../codewhisperer/severity-initial-high.svg | 4 +
.../codewhisperer/severity-initial-info.svg | 4 +
.../codewhisperer/severity-initial-low.svg | 4 +
.../codewhisperer/severity-initial-medium.svg | 4 +
.../resources/telemetryOverride.json | 156 ++
.../jetbrains-community/src/icons/AwsIcons.kt | 14 +
.../resources/MessagesBundle.properties | 174 ++-
.../codewhispererruntime/service-2.json | 3 +-
.../codewhispererstreaming/service-2.json | 25 +-
163 files changed, 13016 insertions(+), 901 deletions(-)
delete mode 100644 .changes/next-release/.changes/next-release/bugfix-21701bb3-5189-474e-868b-9ec46ecde6ee.json
create mode 100644 .changes/next-release/feature-44636712-ede6-4e2a-b4d6-ca2b98da003d.json
create mode 100644 .changes/next-release/feature-5c2fae3e-c794-438b-8af5-2c31c00ab000.json
create mode 100644 .changes/next-release/feature-8fc6f4c9-6976-4a4a-88f0-bd4e6081a4b9.json
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/ConversationNotStartedState.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionState.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionStateTypes.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/AmazonQCodeGenService.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/FileUtils.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatAppFactory.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatItems.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanConstants.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/InboundAppMessagesHandler.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/auth/CodeScanAuthUtils.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanActionMessage.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanCommand.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanMessageListener.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatController.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/messages/CodeScanMessage.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/session/Session.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatAppFactory.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatItems.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestConstants.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererCodeTestSession.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/InboundAppMessagesHandler.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/auth/CodeTestAuthUtils.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatHelper.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/messages/CodeTestMessage.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/BuildAndExecuteStatusIcon.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/PreviousUTGIterationContext.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/ShortAnswer.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/session/Session.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/utils/UTGChatUtil.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocAppFactory.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocChatItems.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocConstants.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocExceptions.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/InboundAppMessagesHandler.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/auth/DocAuthUtils.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocControllerExtensions.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocGenerationTask.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessage.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessagePublisherExtensions.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocGenerationState.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocSession.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/SessionStateTypes.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/util/DocControllerUtil.kt
create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanCompleteAction.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/AmazonQCodeFixSession.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanIssueDetailsPanel.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanFilterGroup.kt
delete mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererStopCodeScanAction.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/context/CodeScanIssueDetailsDisplayType.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/AmazonQCodeReviewGitUtils.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt
create mode 100644 plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt
create mode 100644 plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeScanChatConnector.ts
create mode 100644 plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTestChatConnector.ts
create mode 100644 plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/docChatConnector.ts
create mode 100644 plugins/amazonq/mynah-ui/src/mynah-ui/ui/telemetry/actions.ts
create mode 100644 plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/agent.ts
create mode 100644 plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts
create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-critical.svg
create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-high.svg
create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-info.svg
create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-low.svg
create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-medium.svg
diff --git a/.changes/next-release/.changes/next-release/bugfix-21701bb3-5189-474e-868b-9ec46ecde6ee.json b/.changes/next-release/.changes/next-release/bugfix-21701bb3-5189-474e-868b-9ec46ecde6ee.json
deleted file mode 100644
index 8d2e0694ab..0000000000
--- a/.changes/next-release/.changes/next-release/bugfix-21701bb3-5189-474e-868b-9ec46ecde6ee.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type" : "bugfix",
- "description" : "Feature Development: Fix file rejections for files outside of src/"
-}
diff --git a/.changes/next-release/feature-44636712-ede6-4e2a-b4d6-ca2b98da003d.json b/.changes/next-release/feature-44636712-ede6-4e2a-b4d6-ca2b98da003d.json
new file mode 100644
index 0000000000..587728529e
--- /dev/null
+++ b/.changes/next-release/feature-44636712-ede6-4e2a-b4d6-ca2b98da003d.json
@@ -0,0 +1,4 @@
+{
+ "type" : "feature",
+ "description" : "`/review` in Q chat to scan your code for vulnerabilities and quality issues, and generate fixes"
+}
diff --git a/.changes/next-release/feature-5c2fae3e-c794-438b-8af5-2c31c00ab000.json b/.changes/next-release/feature-5c2fae3e-c794-438b-8af5-2c31c00ab000.json
new file mode 100644
index 0000000000..695a952916
--- /dev/null
+++ b/.changes/next-release/feature-5c2fae3e-c794-438b-8af5-2c31c00ab000.json
@@ -0,0 +1,4 @@
+{
+ "type" : "feature",
+ "description" : "`/test` in Q chat to generate unit tests for java and python"
+}
diff --git a/.changes/next-release/feature-8fc6f4c9-6976-4a4a-88f0-bd4e6081a4b9.json b/.changes/next-release/feature-8fc6f4c9-6976-4a4a-88f0-bd4e6081a4b9.json
new file mode 100644
index 0000000000..e40d4e4416
--- /dev/null
+++ b/.changes/next-release/feature-8fc6f4c9-6976-4a4a-88f0-bd4e6081a4b9.json
@@ -0,0 +1,4 @@
+{
+ "type" : "feature",
+ "description" : "`/doc` in Q chat to generate and update documentation for your project"
+}
diff --git a/.run/Run Amazon Q - Ultimate [2024.1].run.xml b/.run/Run Amazon Q - Ultimate [2024.1].run.xml
index 4296908019..bae51e0465 100644
--- a/.run/Run Amazon Q - Ultimate [2024.1].run.xml
+++ b/.run/Run Amazon Q - Ultimate [2024.1].run.xml
@@ -22,4 +22,4 @@
false
-
\ No newline at end of file
+
diff --git a/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts b/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts
index b051f7e913..314ff99426 100644
--- a/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts
+++ b/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts
@@ -38,6 +38,8 @@ java {
tasks.withType().configureEach {
options.encoding = "UTF-8"
+ sourceCompatibility = "17"
+ targetCompatibility = "17"
}
val generateTask = tasks.register("generateSdks")
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index de7d410a66..49574a7802 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -27,7 +27,7 @@ mockitoKotlin = "5.4.0"
mockk = "1.13.10"
nimbus-jose-jwt = "9.40"
node-gradle = "7.0.2"
-telemetryGenerator = "1.0.278"
+telemetryGenerator = "1.0.284"
testLogger = "4.0.0"
testRetry = "1.5.10"
# test-only; platform provides slf4j transitively at runtime
diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml
index d08356231f..6949f4ba39 100644
--- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml
+++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml
@@ -26,6 +26,9 @@
+
+
+
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt
new file mode 100644
index 0000000000..7741758d49
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt
@@ -0,0 +1,153 @@
+// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.common.clients
+
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.SystemInfo
+import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient
+import software.amazon.awssdk.services.codewhispererruntime.model.ArtifactType
+import software.amazon.awssdk.services.codewhispererruntime.model.ContentChecksumType
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateTaskAssistConversationRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateTaskAssistConversationResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationEvent
+import software.amazon.awssdk.services.codewhispererruntime.model.GetTaskAssistCodeGenerationResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory
+import software.amazon.awssdk.services.codewhispererruntime.model.OperatingSystem
+import software.amazon.awssdk.services.codewhispererruntime.model.OptOutPreference
+import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryEventResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.StartTaskAssistCodeGenerationResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.TaskAssistPlanningUploadContext
+import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext
+import software.amazon.awssdk.services.codewhispererruntime.model.UploadIntent
+import software.amazon.awssdk.services.codewhispererruntime.model.UserContext
+import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.jetbrains.common.session.Intent
+import software.aws.toolkits.jetbrains.core.awsClient
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.services.amazonq.clients.AmazonQStreamingClient
+import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_EVALUATION_PRODUCT_NAME
+import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency
+import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
+import software.aws.toolkits.jetbrains.settings.AwsSettings
+import java.time.Instant
+import software.amazon.awssdk.services.codewhispererruntime.model.ChatTriggerType as SyncChatTriggerType
+
+@Service(Service.Level.PROJECT)
+class AmazonQCodeGenerateClient(private val project: Project) {
+ private fun getTelemetryOptOutPreference() =
+ if (AwsSettings.getInstance().isTelemetryEnabled) {
+ OptOutPreference.OPTIN
+ } else {
+ OptOutPreference.OPTOUT
+ }
+
+ private val docGenerationUserContext = ClientMetadata.getDefault().let {
+ val osForFeatureDev: OperatingSystem =
+ when {
+ SystemInfo.isWindows -> OperatingSystem.WINDOWS
+ SystemInfo.isMac -> OperatingSystem.MAC
+ // For now, categorize everything else as "Linux" (Linux/FreeBSD/Solaris/etc.)
+ else -> OperatingSystem.LINUX
+ }
+
+ UserContext.builder()
+ .ideCategory(IdeCategory.JETBRAINS)
+ .operatingSystem(osForFeatureDev)
+ .product(FEATURE_EVALUATION_PRODUCT_NAME)
+ .clientId(it.clientId)
+ .ideVersion(it.awsVersion)
+ .build()
+ }
+
+ fun connection() = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
+ ?: error("Attempted to use connection while one does not exist")
+
+ fun bearerClient() = connection().getConnectionSettings().awsClient()
+
+ private val amazonQStreamingClient
+ get() = AmazonQStreamingClient.getInstance(project)
+
+ fun sendDocGenerationTelemetryEvent(
+ docGenerationEvent: DocGenerationEvent,
+ ): SendTelemetryEventResponse =
+ bearerClient().sendTelemetryEvent { requestBuilder ->
+ requestBuilder.telemetryEvent { telemetryEventBuilder ->
+ telemetryEventBuilder.docGenerationEvent(docGenerationEvent)
+ }
+ requestBuilder.optOutPreference(getTelemetryOptOutPreference())
+ requestBuilder.userContext(docGenerationUserContext)
+ }
+
+ fun createTaskAssistConversation(): CreateTaskAssistConversationResponse = bearerClient().createTaskAssistConversation(
+ CreateTaskAssistConversationRequest.builder().build()
+ )
+
+ fun createTaskAssistUploadUrl(conversationId: String, contentChecksumSha256: String, contentLength: Long): CreateUploadUrlResponse =
+ bearerClient().createUploadUrl {
+ it.contentChecksumType(ContentChecksumType.SHA_256)
+ .contentChecksum(contentChecksumSha256)
+ .contentLength(contentLength)
+ .artifactType(ArtifactType.SOURCE_CODE)
+ .uploadIntent(UploadIntent.TASK_ASSIST_PLANNING)
+ .uploadContext(
+ UploadContext.builder()
+ .taskAssistPlanningUploadContext(
+ TaskAssistPlanningUploadContext.builder()
+ .conversationId(conversationId)
+ .build()
+ )
+ .build()
+ )
+ }
+
+ fun startTaskAssistCodeGeneration(conversationId: String, uploadId: String, userMessage: String, intent: Intent): StartTaskAssistCodeGenerationResponse =
+ bearerClient()
+ .startTaskAssistCodeGeneration { request ->
+ request
+ .conversationState {
+ it
+ .conversationId(conversationId)
+ .chatTriggerType(SyncChatTriggerType.MANUAL)
+ .currentMessage { cm -> cm.userInputMessage { um -> um.content(userMessage) } }
+ }
+ .workspaceState {
+ it
+ .programmingLanguage { pl -> pl.languageName("javascript") }
+ .uploadId(uploadId)
+ }
+ .intent(intent.name)
+ }
+
+ fun getTaskAssistCodeGeneration(conversationId: String, codeGenerationId: String): GetTaskAssistCodeGenerationResponse = bearerClient()
+ .getTaskAssistCodeGeneration {
+ it
+ .conversationId(conversationId)
+ .codeGenerationId(codeGenerationId)
+ }
+
+ suspend fun exportTaskAssistResultArchive(conversationId: String): MutableList = amazonQStreamingClient.exportResultArchive(
+ conversationId,
+ ExportIntent.TASK_ASSIST,
+ null,
+ { e ->
+ LOG.error(e) { "TaskAssist - ExportResultArchive stream exportId=$conversationId exportIntent=${ExportIntent.TASK_ASSIST} Failed: ${e.message} " }
+ },
+ { startTime ->
+ LOG.info { "TaskAssist - ExportResultArchive latency: ${calculateTotalLatency(startTime, Instant.now())}" }
+ }
+ )
+
+ companion object {
+ private val LOG = getLogger()
+
+ fun getInstance(project: Project) = project.service()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/ConversationNotStartedState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/ConversationNotStartedState.kt
new file mode 100644
index 0000000000..ad300449df
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/ConversationNotStartedState.kt
@@ -0,0 +1,21 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.common.session
+
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.SessionStateInteraction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStateAction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
+
+class ConversationNotStartedState(
+ override var approach: String,
+ override val tabID: String,
+ override var token: CancellationTokenSource?,
+) : SessionState {
+ override val phase = SessionStatePhase.INIT
+
+ override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
+ error("Illegal transition between states, restart the conversation")
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionState.kt
new file mode 100644
index 0000000000..1585096382
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionState.kt
@@ -0,0 +1,17 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.common.session
+
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.SessionStateInteraction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStateAction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
+
+interface SessionState {
+ val tabID: String
+ val phase: SessionStatePhase?
+ var token: CancellationTokenSource?
+ var approach: String
+ suspend fun interact(action: SessionStateAction): SessionStateInteraction
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionStateTypes.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionStateTypes.kt
new file mode 100644
index 0000000000..3b50f10ca9
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionStateTypes.kt
@@ -0,0 +1,24 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.common.session
+
+import software.aws.toolkits.jetbrains.common.util.AmazonQCodeGenService
+import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
+
+open class SessionStateConfig(
+ open val conversationId: String,
+ open val repoContext: FeatureDevSessionContext,
+ open val amazonQCodeGenService: AmazonQCodeGenService,
+)
+
+data class SessionStateConfigData(
+ override val conversationId: String,
+ override val repoContext: FeatureDevSessionContext,
+ override val amazonQCodeGenService: AmazonQCodeGenService,
+) : SessionStateConfig(conversationId, repoContext, amazonQCodeGenService)
+
+enum class Intent {
+ DEV,
+ DOC,
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/AmazonQCodeGenService.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/AmazonQCodeGenService.kt
new file mode 100644
index 0000000000..afe9da01b2
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/AmazonQCodeGenService.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.common.util
+
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.intellij.openapi.project.Project
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.GetTaskAssistCodeGenerationResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.ServiceQuotaExceededException
+import software.amazon.awssdk.services.codewhispererruntime.model.StartTaskAssistCodeGenerationResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException
+import software.amazon.awssdk.services.codewhispererruntime.model.ValidationException
+import software.amazon.awssdk.services.codewhispererstreaming.model.CodeWhispererStreamingException
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.common.clients.AmazonQCodeGenerateClient
+import software.aws.toolkits.jetbrains.common.session.Intent
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.DocGenerationStreamResult
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.ExportDocTaskAssistResultArchiveStreamResult
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ContentLengthException
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ExportParseException
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevOperation
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ZipFileCorruptedException
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
+import software.aws.toolkits.telemetry.AmazonqTelemetry
+import software.aws.toolkits.telemetry.MetricResult
+
+class AmazonQCodeGenService(val proxyClient: AmazonQCodeGenerateClient, val project: Project) {
+ fun createConversation(): String {
+ val startTime = System.currentTimeMillis()
+ var failureReason: String? = null
+ var failureReasonDesc: String? = null
+ var result: MetricResult = MetricResult.Succeeded
+ var conversationId: String? = null
+ try {
+ logger.debug { "Executing createTaskAssistConversation" }
+ val taskAssistConversationResult = proxyClient.createTaskAssistConversation()
+ conversationId = taskAssistConversationResult.conversationId()
+ logger.debug {
+ "$FEATURE_NAME: Created conversation: {conversationId: $conversationId, requestId: ${
+ taskAssistConversationResult.responseMetadata().requestId()
+ }"
+ }
+
+ return conversationId
+ } catch (e: Exception) {
+ logger.warn(e) { "$FEATURE_NAME: Failed to start conversation: ${e.message}" }
+ result = MetricResult.Failed
+ failureReason = e.javaClass.simpleName
+ if (e is FeatureDevException) {
+ failureReason = e.reason()
+ failureReasonDesc = e.reasonDesc()
+ }
+ var errMssg = e.message
+ if (e is CodeWhispererRuntimeException) {
+ errMssg = e.awsErrorDetails().errorMessage()
+ logger.warn(e) { "Start conversation failed for request: ${e.requestId()}" }
+
+ if (e is ServiceQuotaExceededException) {
+ throw MonthlyConversationLimitError(errMssg, operation = FeatureDevOperation.CreateConversation.toString(), desc = null, cause = e.cause)
+ }
+ }
+ throw FeatureDevException(errMssg, operation = FeatureDevOperation.CreateConversation.toString(), desc = null, e.cause)
+ } finally {
+ AmazonqTelemetry.startConversationInvoke(
+ amazonqConversationId = conversationId,
+ result = result,
+ reason = failureReason,
+ reasonDesc = failureReasonDesc,
+ duration = (System.currentTimeMillis() - startTime).toDouble(),
+ credentialStartUrl = getStartUrl(project = this.project),
+ )
+ }
+ }
+
+ fun createUploadUrl(conversationId: String, contentChecksumSha256: String, contentLength: Long, uploadId: String):
+ CreateUploadUrlResponse {
+ try {
+ logger.debug { "Executing createUploadUrl with conversationId $conversationId" }
+ val uploadUrlResponse = proxyClient.createTaskAssistUploadUrl(
+ conversationId,
+ contentChecksumSha256,
+ contentLength,
+ )
+ logger.debug {
+ "$FEATURE_NAME: Created upload url: {uploadId: $uploadId, requestId: ${uploadUrlResponse.responseMetadata().requestId()}}"
+ }
+ return uploadUrlResponse
+ } catch (e: Exception) {
+ logger.warn(e) { "$FEATURE_NAME: Failed to generate presigned url: ${e.message}" }
+
+ var errMssg = e.message
+ if (e is CodeWhispererRuntimeException) {
+ errMssg = e.awsErrorDetails().errorMessage()
+ logger.warn(e) { "Create UploadUrl failed for request: ${e.requestId()}" }
+
+ if (e is ValidationException && e.message?.contains("Invalid contentLength") == true) {
+ throw ContentLengthException(operation = FeatureDevOperation.CreateUploadUrl.toString(), desc = null, cause = e.cause)
+ }
+ }
+ throw FeatureDevException(errMssg, operation = FeatureDevOperation.CreateUploadUrl.toString(), desc = null, e.cause)
+ }
+ }
+
+ open fun startTaskAssistCodeGeneration(conversationId: String, uploadId: String, message: String, intent: Intent):
+ StartTaskAssistCodeGenerationResponse {
+ try {
+ logger.debug { "Executing startTaskAssistCodeGeneration with conversationId: $conversationId , uploadId: $uploadId" }
+ val startCodeGenerationResponse = proxyClient.startTaskAssistCodeGeneration(
+ conversationId,
+ uploadId,
+ message,
+ intent
+ )
+
+ logger.debug { "$FEATURE_NAME: Started code generation with requestId: ${startCodeGenerationResponse.responseMetadata().requestId()}" }
+ return startCodeGenerationResponse
+ } catch (e: Exception) {
+ logger.warn(e) { "$FEATURE_NAME: Failed to execute startTaskAssistCodeGeneration ${e.message}" }
+
+ var errMssg = e.message
+ if (e is CodeWhispererRuntimeException) {
+ errMssg = e.awsErrorDetails().errorMessage()
+ logger.warn(e) { "StartTaskAssistCodeGeneration failed for request: ${e.requestId()}" }
+
+ // API Front-end will throw Throttling if conversation limit is reached. API Front-end monitors StartCodeGeneration for throttling
+ if (e is software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException &&
+ e.message?.contains("StartTaskAssistCodeGeneration reached for this month.") == true
+ ) {
+ throw MonthlyConversationLimitError(errMssg, operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause)
+ }
+
+ if (e is ServiceQuotaExceededException || (
+ e is ThrottlingException && e.message?.contains(
+ "limit for number of iterations on a code generation"
+ ) == true
+ )
+ ) {
+ throw CodeIterationLimitException(operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause)
+ } else if (e is ValidationException && e.message?.contains("repo size is exceeding the limits") == true) {
+ throw ContentLengthException(operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, cause = e.cause)
+ } else if (e is ValidationException && e.message?.contains("zipped file is corrupted") == true) {
+ throw ZipFileCorruptedException(operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause)
+ }
+ }
+ throw FeatureDevException(errMssg, operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause)
+ }
+ }
+
+ fun getTaskAssistCodeGeneration(conversationId: String, codeGenerationId: String): GetTaskAssistCodeGenerationResponse {
+ try {
+ logger.debug { "Executing GetTaskAssistCodeGeneration with conversationId: $conversationId , codeGenerationId: $codeGenerationId" }
+ val getCodeGenerationResponse = proxyClient.getTaskAssistCodeGeneration(conversationId, codeGenerationId)
+
+ logger.debug {
+ "$FEATURE_NAME: Received code generation status $getCodeGenerationResponse with requestId ${
+ getCodeGenerationResponse.responseMetadata()
+ .requestId()
+ }"
+ }
+ return getCodeGenerationResponse
+ } catch (e: Exception) {
+ logger.warn(e) { "$FEATURE_NAME: Failed to execute GetTaskAssistCodeGeneration ${e.message}" }
+
+ var errMssg = e.message
+ if (e is CodeWhispererRuntimeException) {
+ errMssg = e.awsErrorDetails().errorMessage()
+ logger.warn(e) { "GetTaskAssistCodeGeneration failed for request: ${e.requestId()}" }
+ }
+ throw FeatureDevException(errMssg, operation = FeatureDevOperation.GetTaskAssistCodeGeneration.toString(), desc = null, e.cause)
+ }
+ }
+
+ suspend fun exportTaskAssistArchiveResult(conversationId: String): DocGenerationStreamResult {
+ val exportResponse: MutableList
+ try {
+ exportResponse = proxyClient.exportTaskAssistResultArchive(conversationId)
+ logger.debug { "$FEATURE_NAME: Received export task assist result archive response" }
+ } catch (e: Exception) {
+ logger.warn(e) { "$FEATURE_NAME: Failed to export archive result: ${e.message}" }
+
+ var errMssg = e.message
+ if (e is CodeWhispererStreamingException) {
+ errMssg = e.awsErrorDetails().errorMessage()
+ logger.warn(e) { "ExportTaskAssistArchiveResult failed for request: ${e.requestId()}" }
+ }
+ throw FeatureDevException(errMssg, operation = FeatureDevOperation.ExportTaskAssistArchiveResult.toString(), desc = null, e.cause)
+ }
+
+ val parsedResult: ExportDocTaskAssistResultArchiveStreamResult
+ try {
+ val result = exportResponse.reduce { acc, next -> acc + next } // To map the result it is needed to combine the full byte array
+ parsedResult = jacksonObjectMapper().readValue(result)
+ } catch (e: Exception) {
+ logger.error(e) { "Failed to parse downloaded code results" }
+ throw ExportParseException(operation = FeatureDevOperation.ExportTaskAssistArchiveResult.toString(), desc = null, e.cause)
+ }
+
+ return parsedResult.codeGenerationResult
+ }
+
+ companion object {
+ private val logger = getLogger()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/FileUtils.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/FileUtils.kt
new file mode 100644
index 0000000000..6cf8cc2476
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/FileUtils.kt
@@ -0,0 +1,82 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.common.util
+
+import com.github.difflib.DiffUtils
+import com.github.difflib.patch.DeltaType
+import com.intellij.openapi.fileChooser.FileChooser
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.CharsetToolkit
+import com.intellij.openapi.vfs.VirtualFile
+import java.io.File
+import java.nio.charset.Charset
+import java.nio.file.Path
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.writeBytes
+
+fun resolveAndCreateOrUpdateFile(projectRootPath: Path, relativeFilePath: String, fileContent: String) {
+ val filePath = projectRootPath.resolve(relativeFilePath)
+ filePath.parent.createDirectories() // Create directories if needed
+ filePath.writeBytes(fileContent.toByteArray(Charsets.UTF_8))
+}
+
+fun resolveAndDeleteFile(projectRootPath: Path, relativePath: String) {
+ val filePath = projectRootPath.resolve(relativePath)
+ filePath.deleteIfExists()
+}
+
+fun selectFolder(project: Project, openOn: VirtualFile): VirtualFile? {
+ val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
+ return FileChooser.chooseFile(fileChooserDescriptor, project, openOn)
+}
+
+fun readFileToString(file: File): String {
+ val charsetToolkit = CharsetToolkit(file.readBytes(), Charset.forName("UTF-8"), false)
+ val charset = charsetToolkit.guessEncoding(4096)
+ return file.readText(charset)
+}
+
+/**
+ * Calculates the number of added characters and lines between existing content and LLM response
+ *
+ * @param existingContent The original text content before changes
+ * @param llmResponse The new text content from the LLM
+ * @return A Map containing:
+ * - "addedChars": Total number of new characters added
+ * - "addedLines": Total number of new lines added
+ */
+data class DiffResult(val addedChars: Int, val addedLines: Int)
+
+fun getDiffCharsAndLines(
+ existingContent: String,
+ llmResponse: String,
+): DiffResult {
+ var addedChars = 0
+ var addedLines = 0
+
+ val existingLines = existingContent.lines()
+ val llmLines = llmResponse.lines()
+
+ val patch = DiffUtils.diff(existingLines, llmLines)
+
+ for (delta in patch.deltas) {
+ when (delta.type) {
+ DeltaType.INSERT -> {
+ addedChars += delta.target.lines.sumOf { it.length }
+ addedLines += delta.target.lines.size
+ }
+
+ DeltaType.CHANGE -> {
+ addedChars += delta.target.lines.sumOf { it.length }
+ addedLines += delta.target.lines.size
+ }
+
+ else -> {} // Do nothing for DELETE
+ }
+ }
+
+ return DiffResult(addedChars, addedLines)
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt
index 95fbabd3a4..de01e99bad 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt
@@ -9,6 +9,8 @@ import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.wm.ToolWindowManager
import icons.AwsIcons
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID
+import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.runScanKey
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.UiTelemetry
@@ -19,5 +21,8 @@ class QOpenPanelAction : AnAction(message("action.q.openchat.text"), null, AwsIc
val project = e.getRequiredData(CommonDataKeys.PROJECT)
UiTelemetry.click(project, "q_openChat")
ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, true)
+ if (e.getData(runScanKey) == true) {
+ AmazonQToolWindow.openScanTab(project)
+ }
}
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt
index e5673cc6da..7235a9c370 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt
@@ -27,6 +27,10 @@ import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPag
import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector
import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.runCodeScanMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
+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 javax.swing.JComponent
@@ -72,6 +76,14 @@ class AmazonQToolWindow private constructor(
}
}
+ private fun sendMessageAppToUi(message: AmazonQMessage, tabType: String) {
+ appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach {
+ scope.launch {
+ it.messagesFromAppToUi.publish(message)
+ }
+ }
+ }
+
private fun initConnections() {
val apps = appSource.getApps(project)
apps.forEach { app ->
@@ -110,7 +122,10 @@ class AmazonQToolWindow private constructor(
chatBrowser.init(
isCodeTransformAvailable = isCodeTransformAvailable(project),
- isFeatureDevAvailable = isFeatureDevAvailable(project)
+ isFeatureDevAvailable = isFeatureDevAvailable(project),
+ isCodeScanAvailable = isCodeScanAvailable(project),
+ isCodeTestAvailable = isCodeTestAvailable(project),
+ isDocAvailable = isDocAvailable(project)
)
scope.launch {
@@ -134,17 +149,25 @@ class AmazonQToolWindow private constructor(
companion object {
fun getInstance(project: Project): AmazonQToolWindow = project.service()
+ private fun showChatWindow(project: Project) = runInEdt {
+ val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID)
+ toolWindow?.show()
+ }
+
fun getStarted(project: Project) {
// Make sure the window is shown
- runInEdt {
- val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID)
- toolWindow?.show()
- }
+ showChatWindow(project)
// Send the interaction message
val window = getInstance(project)
window.sendMessage(OnboardingPageInteraction(OnboardingPageInteractionType.CwcButtonClick), "cwc")
}
+
+ fun openScanTab(project: Project) {
+ showChatWindow(project)
+ val window = getInstance(project)
+ window.sendMessageAppToUi(runCodeScanMessage, tabType = "codescan")
+ }
}
override fun dispose() {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt
index 9a4f30222d..9bd49041e4 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt
@@ -18,7 +18,13 @@ class Browser(parent: Disposable) : Disposable {
val receiveMessageQuery = JBCefJSQuery.create(jcefBrowser)
- fun init(isCodeTransformAvailable: Boolean, isFeatureDevAvailable: Boolean) {
+ fun init(
+ isCodeTransformAvailable: Boolean,
+ isFeatureDevAvailable: Boolean,
+ isDocAvailable: Boolean,
+ isCodeScanAvailable: Boolean,
+ isCodeTestAvailable: Boolean,
+ ) {
// register the scheme handler to route http://mynah/ URIs to the resources/assets directory on classpath
CefApp.getInstance()
.registerSchemeHandlerFactory(
@@ -27,7 +33,7 @@ class Browser(parent: Disposable) : Disposable {
AssetResourceHandler.AssetResourceHandlerFactory(),
)
- loadWebView(isCodeTransformAvailable, isFeatureDevAvailable)
+ loadWebView(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable)
}
override fun dispose() {
@@ -42,19 +48,31 @@ class Browser(parent: Disposable) : Disposable {
.executeJavaScript("window.postMessage(JSON.stringify($message))", jcefBrowser.cefBrowser.url, 0)
// Load the chat web app into the jcefBrowser
- private fun loadWebView(isCodeTransformAvailable: Boolean, isFeatureDevAvailable: Boolean) {
+ private fun loadWebView(
+ isCodeTransformAvailable: Boolean,
+ isFeatureDevAvailable: Boolean,
+ isDocAvailable: Boolean,
+ isCodeScanAvailable: Boolean,
+ isCodeTestAvailable: Boolean,
+ ) {
// setup empty state. The message request handlers use this for storing state
// that's persistent between page loads.
jcefBrowser.setProperty("state", "")
// load the web app
- jcefBrowser.loadHTML(getWebviewHTML(isCodeTransformAvailable, isFeatureDevAvailable))
+ jcefBrowser.loadHTML(getWebviewHTML(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable))
}
/**
* Generate index.html for the web view
* @return HTML source
*/
- private fun getWebviewHTML(isCodeTransformAvailable: Boolean, isFeatureDevAvailable: Boolean): String {
+ private fun getWebviewHTML(
+ isCodeTransformAvailable: Boolean,
+ isFeatureDevAvailable: Boolean,
+ isDocAvailable: Boolean,
+ isCodeScanAvailable: Boolean,
+ isCodeTestAvailable: Boolean,
+ ): String {
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")
val jsScripts = """
@@ -69,6 +87,9 @@ class Browser(parent: Disposable) : Disposable {
},
$isFeatureDevAvailable, // whether /dev is available
$isCodeTransformAvailable, // whether /transform is available
+ $isDocAvailable, // whether /doc is available
+ $isCodeScanAvailable, // whether /scan is available
+ $isCodeTestAvailable // whether /test is available
);
}
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 b2571123b7..6fac4c8940 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
@@ -3,6 +3,7 @@
package software.aws.toolkits.jetbrains.services.amazonq.webview
+import com.intellij.ide.BrowserUtil
import com.intellij.ui.jcef.JBCefJSQuery.Response
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.awaitClose
@@ -21,6 +22,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.util.command
import software.aws.toolkits.jetbrains.services.amazonq.util.tabType
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.AmazonQTheme
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.ThemeBrowserAdapter
+import software.aws.toolkits.telemetry.Telemetry
import java.util.function.Function
class BrowserConnector(
@@ -37,9 +39,31 @@ class BrowserConnector(
addMessageHook(browser)
.onEach { json ->
val node = serializer.toNode(json)
- if (node.command == "ui-is-ready") {
- uiReady.complete(true)
+ when (node.command) {
+ "ui-is-ready" -> uiReady.complete(true)
+
+ // some weird issue preventing deserialization from working
+ "open-user-guide" -> {
+ BrowserUtil.browse(node.get("userGuideLink").asText())
+ }
+ "send-telemetry" -> {
+ val source = node.get("source")
+ val module = node.get("module")
+ val trigger = node.get("trigger")
+
+ if (source != null) {
+ Telemetry.ui.click.use {
+ it.elementId(source.asText())
+ }
+ } else if (module != null && trigger != null) {
+ Telemetry.toolkit.openModule.use {
+ it.module(module.asText())
+ it.source(trigger.asText())
+ }
+ }
+ }
}
+
val tabType = node.tabType ?: return@onEach
connections.filter { connection -> connection.app.tabTypes.contains(tabType) }.forEach { connection ->
launch {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt
new file mode 100644
index 0000000000..166223d623
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt
@@ -0,0 +1,175 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.launch
+import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanActionMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanMessageListener
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.controller.CodeScanChatController
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.AuthenticationNeededExceptionMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.AuthenticationUpdateMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CODE_SCAN_TAB_NAME
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.IncomingCodeScanMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
+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 java.util.concurrent.atomic.AtomicBoolean
+
+private enum class CodeScanMessageTypes(val type: String) {
+ ClearChat("clear"),
+ Help("help"),
+ TabCreated("new-tab-was-created"),
+ TabRemoved("tab-was-removed"),
+ Scan("scan"),
+ StartProjectScan("codescan_start_project_scan"),
+ StartFileScan("codescan_start_file_scan"),
+ StopFileScan("codescan_stop_file_scan"),
+ StopProjectScan("codescan_stop_project_scan"),
+ OpenIssuesPanel("codescan_open_issues"),
+ ResponseBodyLinkClicked("response-body-link-click"),
+}
+
+class CodeScanChatApp(private val scope: CoroutineScope) : AmazonQApp {
+ private val isProcessingAuthChanged = AtomicBoolean(false)
+ override val tabTypes = listOf(CODE_SCAN_TAB_NAME)
+ override fun init(context: AmazonQAppInitContext) {
+ val chatSessionStorage = ChatSessionStorage()
+ val inboundAppMessagesHandler: InboundAppMessagesHandler = CodeScanChatController(context, chatSessionStorage)
+
+ context.messageTypeRegistry.register(
+ CodeScanMessageTypes.ClearChat.type to IncomingCodeScanMessage.ClearChat::class,
+ CodeScanMessageTypes.Help.type to IncomingCodeScanMessage.Help::class,
+ CodeScanMessageTypes.TabCreated.type to IncomingCodeScanMessage.TabCreated::class,
+ CodeScanMessageTypes.TabRemoved.type to IncomingCodeScanMessage.TabRemoved::class,
+ CodeScanMessageTypes.Scan.type to IncomingCodeScanMessage.Scan::class,
+ CodeScanMessageTypes.StartProjectScan.type to IncomingCodeScanMessage.StartProjectScan::class,
+ CodeScanMessageTypes.StartFileScan.type to IncomingCodeScanMessage.StartFileScan::class,
+ CodeScanMessageTypes.StopProjectScan.type to IncomingCodeScanMessage.StopProjectScan::class,
+ CodeScanMessageTypes.StopFileScan.type to IncomingCodeScanMessage.StopFileScan::class,
+ CodeScanMessageTypes.ResponseBodyLinkClicked.type to IncomingCodeScanMessage.ResponseBodyLinkClicked::class,
+ CodeScanMessageTypes.OpenIssuesPanel.type to IncomingCodeScanMessage.OpenIssuesPanel::class
+ )
+
+ scope.launch {
+ merge(service().flow, context.messagesFromUiToApp.flow).collect { message ->
+ // Launch a new coroutine to handle each message
+ scope.launch { handleMessage(message, inboundAppMessagesHandler) }
+ }
+ }
+
+ fun authChanged() {
+ val isAnotherThreadProcessing = !isProcessingAuthChanged.compareAndSet(false, true)
+ if (isAnotherThreadProcessing) return
+ scope.launch {
+ val authController = AuthController()
+ val credentialState = authController.getAuthNeededStates(context.project).amazonQ
+ if (credentialState == null) {
+ // Notify tabs about restoring authentication
+ context.messagesFromAppToUi.publish(
+ AuthenticationUpdateMessage(
+ featureDevEnabled = isFeatureDevAvailable(context.project),
+ codeTransformEnabled = isCodeTransformAvailable(context.project),
+ codeScanEnabled = isCodeScanAvailable(context.project),
+ codeTestEnabled = isCodeTestAvailable(context.project),
+ docEnabled = isDocAvailable(context.project),
+ authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabId }
+ )
+ )
+
+ chatSessionStorage.changeAuthenticationNeeded(false)
+ chatSessionStorage.changeAuthenticationNeededNotified(false)
+ } else {
+ chatSessionStorage.changeAuthenticationNeeded(true)
+
+ // Ask for reauth
+ chatSessionStorage.getAuthenticatingSessions().filter { !it.authNeededNotified }.forEach {
+ context.messagesFromAppToUi.publish(
+ AuthenticationNeededExceptionMessage(
+ tabId = it.tabId,
+ authType = credentialState.authType,
+ message = credentialState.message
+ )
+ )
+ }
+
+ // Prevent multiple calls to activeConnectionChanged
+ chatSessionStorage.changeAuthenticationNeededNotified(true)
+ }
+ isProcessingAuthChanged.set(false)
+ }
+ }
+
+ ApplicationManager.getApplication().messageBus.connect(this).subscribe(
+ BearerTokenProviderListener.TOPIC,
+ object : BearerTokenProviderListener {
+ override fun onChange(providerId: String, newScopes: List?) {
+ val qProvider = getQTokenProvider(context.project)
+ val isQ = qProvider?.id == providerId
+ val isAuthorized = qProvider?.state() == BearerTokenAuthState.AUTHORIZED
+ if (!isQ || !isAuthorized) return
+ authChanged()
+ }
+ }
+ )
+
+ ApplicationManager.getApplication().messageBus.connect(this).subscribe(
+ ToolkitConnectionManagerListener.TOPIC,
+ object : ToolkitConnectionManagerListener {
+ override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
+ authChanged()
+ }
+ }
+ )
+ }
+
+ private fun getQTokenProvider(project: Project) = (
+ ToolkitConnectionManager
+ .getInstance(project)
+ .activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
+ )
+ ?.getConnectionSettings()
+ ?.tokenProvider
+ ?.delegate as? BearerTokenProvider
+
+ private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
+ when (message) {
+ is IncomingCodeScanMessage.ClearChat -> inboundAppMessagesHandler.processClearQuickAction(message)
+ is IncomingCodeScanMessage.Help -> inboundAppMessagesHandler.processHelpQuickAction(message)
+ is IncomingCodeScanMessage.TabCreated -> inboundAppMessagesHandler.processTabCreated(message)
+ is IncomingCodeScanMessage.TabRemoved -> inboundAppMessagesHandler.processTabRemoved(message)
+ is IncomingCodeScanMessage.Scan -> inboundAppMessagesHandler.processScanQuickAction(message)
+ is IncomingCodeScanMessage.StartProjectScan -> inboundAppMessagesHandler.processStartProjectScan(message)
+ is IncomingCodeScanMessage.StartFileScan -> inboundAppMessagesHandler.processStartFileScan(message)
+ is CodeScanActionMessage -> inboundAppMessagesHandler.processCodeScanCommand(message)
+ is IncomingCodeScanMessage.StopProjectScan -> inboundAppMessagesHandler.processStopProjectScan(message)
+ is IncomingCodeScanMessage.StopFileScan -> inboundAppMessagesHandler.processStopFileScan(message)
+ is IncomingCodeScanMessage.ResponseBodyLinkClicked -> inboundAppMessagesHandler.processResponseBodyLinkClicked(message)
+ is IncomingCodeScanMessage.OpenIssuesPanel -> inboundAppMessagesHandler.processOpenIssuesPanel(message)
+ }
+ }
+
+ override fun dispose() {
+ // nothing to do
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatAppFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatAppFactory.kt
new file mode 100644
index 0000000000..2ac70b38b0
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatAppFactory.kt
@@ -0,0 +1,12 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan
+
+import com.intellij.openapi.project.Project
+import kotlinx.coroutines.CoroutineScope
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppFactory
+
+class CodeScanChatAppFactory(private val cs: CoroutineScope) : AmazonQAppFactory {
+ override fun createApp(project: Project) = CodeScanChatApp(cs)
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatItems.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatItems.kt
new file mode 100644
index 0000000000..63479dcc97
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatItems.kt
@@ -0,0 +1,164 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.Button
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanButtonId
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessageContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.ProgressField
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.PromptProgressMessage
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticPrompt
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticTextResponse
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import software.aws.toolkits.resources.message
+import java.util.UUID
+
+private val projectScanButton = Button(
+ id = CodeScanButtonId.StartProjectScan.id,
+ text = message("codescan.chat.message.button.projectScan")
+)
+
+private val fileScanButton = Button(
+ id = CodeScanButtonId.StartFileScan.id,
+ text = message("codescan.chat.message.button.fileScan")
+)
+
+private val openIssuesPanelButton = Button(
+ id = CodeScanButtonId.OpenIssuesPanel.id,
+ text = message("codescan.chat.message.button.openIssues"),
+ keepCardAfterClick = true
+)
+
+fun buildStartNewScanChatContent() = CodeScanChatMessageContent(
+ type = ChatMessageType.Answer,
+ message = message("codescan.chat.new_scan.input.message"),
+ buttons = listOf(
+ fileScanButton,
+ projectScanButton
+ ),
+ canBeVoted = false
+)
+
+// TODO: Replace StaticPrompt and StaticTextResponse message according to Fnf
+fun buildHelpChatPromptContent() = CodeScanChatMessageContent(
+ type = ChatMessageType.Prompt,
+ message = StaticPrompt.Help.message,
+ canBeVoted = false
+)
+
+fun buildHelpChatAnswerContent() = CodeScanChatMessageContent(
+ type = ChatMessageType.Answer,
+ message = StaticTextResponse.Help.message,
+ canBeVoted = false
+)
+
+fun buildUserSelectionProjectScanChatContent() = CodeScanChatMessageContent(
+ type = ChatMessageType.Prompt,
+ message = message("codescan.chat.message.button.projectScan"),
+ canBeVoted = false
+)
+
+fun buildUserSelectionFileScanChatContent() = CodeScanChatMessageContent(
+ type = ChatMessageType.Prompt,
+ message = message("codescan.chat.message.button.fileScan"),
+ canBeVoted = false
+)
+
+fun buildNotInGitRepoChatContent() = CodeScanChatMessageContent(
+ type = ChatMessageType.Answer,
+ message = message("codescan.chat.message.not_git_repo"),
+ canBeVoted = false
+)
+
+fun buildScanInProgressChatContent(currentStep: Int, isProject: Boolean) = CodeScanChatMessageContent(
+ type = ChatMessageType.AnswerPart,
+ message = buildString {
+ appendLine(if (isProject) message("codescan.chat.message.scan_begin_project") else message("codescan.chat.message.scan_begin_file"))
+ appendLine("")
+ appendLine(message("codescan.chat.message.scan_begin_wait_time"))
+ appendLine("")
+ appendLine("${getIconForStep(0, currentStep)} " + message("codescan.chat.message.scan_step_1"))
+ appendLine("${getIconForStep(1, currentStep)} " + message("codescan.chat.message.scan_step_2"))
+ appendLine("${getIconForStep(2, currentStep)} " + message("codescan.chat.message.scan_step_3"))
+ }
+)
+
+val cancellingProgressField = ProgressField(
+ status = "warning",
+ text = message("general.canceling"),
+ value = -1,
+ actions = emptyList()
+)
+
+fun buildScanCompleteChatContent(issues: List, isProject: Boolean = false): CodeScanChatMessageContent {
+ val issueCountMap = IssueSeverity.entries.associate { it.displayName to 0 }.toMutableMap()
+ val aggregatedIssues = issues.groupBy { it.severity }
+ aggregatedIssues.forEach { (key, list) -> if (list.isNotEmpty()) issueCountMap[key] = list.size }
+
+ val message = buildString {
+ appendLine(if (isProject) message("codewhisperer.codescan.scan_complete_project") else message("codewhisperer.codescan.scan_complete_file"))
+ issueCountMap.entries.forEach { (severity, count) ->
+ appendLine(message("codewhisperer.codescan.scan_complete_count", severity, count))
+ }
+ }
+
+ return CodeScanChatMessageContent(
+ type = ChatMessageType.Answer,
+ message = message,
+ buttons = listOf(
+ openIssuesPanelButton
+ ),
+ )
+}
+
+fun buildPromptProgressMessage(tabId: String, isProject: Boolean = false, isCanceling: Boolean = false) = PromptProgressMessage(
+ progressField = when {
+ isCanceling -> cancellingProgressField
+ isProject -> projectScanProgressField
+ else -> fileScanProgressField
+ },
+ tabId = tabId
+)
+
+fun buildClearPromptProgressMessage(tabId: String) = PromptProgressMessage(
+ tabId = tabId
+)
+
+val runCodeScanMessage = CodeScanChatMessage(messageType = ChatMessageType.Prompt, command = "review", tabId = UUID.randomUUID().toString())
+
+val cancelFileScanButton = Button(
+ id = CodeScanButtonId.StopFileScan.id,
+ text = message("general.cancel"),
+ icon = "cancel"
+)
+
+val cancelProjectScanButton = cancelFileScanButton.copy(
+ id = CodeScanButtonId.StopProjectScan.id
+)
+
+val fileScanProgressField = ProgressField(
+ status = "default",
+ text = message("codescan.chat.message.scan_file_in_progress"),
+ value = -1,
+ actions = listOf(cancelFileScanButton)
+)
+
+val projectScanProgressField = fileScanProgressField.copy(
+ text = message("codescan.chat.message.scan_project_in_progress"),
+ actions = listOf(cancelProjectScanButton)
+)
+
+fun buildProjectScanFailedChatContent(errorMessage: String?) = CodeScanChatMessageContent(
+ type = ChatMessageType.Answer,
+ message = errorMessage ?: message("codescan.chat.message.project_scan_failed")
+)
+
+fun getIconForStep(targetStep: Int, currentStep: Int) = when {
+ currentStep == targetStep -> "☐"
+ currentStep > targetStep -> "☑"
+ else -> "☐"
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanConstants.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanConstants.kt
new file mode 100644
index 0000000000..c27b7ef416
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanConstants.kt
@@ -0,0 +1,6 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan
+
+const val FEATURE_NAME = "Amazon Q Code Scan"
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/InboundAppMessagesHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/InboundAppMessagesHandler.kt
new file mode 100644
index 0000000000..31ca09503d
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/InboundAppMessagesHandler.kt
@@ -0,0 +1,33 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanActionMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.IncomingCodeScanMessage
+
+interface InboundAppMessagesHandler {
+ suspend fun processScanQuickAction(message: IncomingCodeScanMessage.Scan)
+
+ suspend fun processStartProjectScan(message: IncomingCodeScanMessage.StartProjectScan)
+
+ suspend fun processStartFileScan(message: IncomingCodeScanMessage.StartFileScan)
+
+ suspend fun processStopProjectScan(message: IncomingCodeScanMessage.StopProjectScan)
+
+ suspend fun processStopFileScan(message: IncomingCodeScanMessage.StopFileScan)
+
+ suspend fun processTabCreated(message: IncomingCodeScanMessage.TabCreated)
+
+ suspend fun processClearQuickAction(message: IncomingCodeScanMessage.ClearChat)
+
+ suspend fun processHelpQuickAction(message: IncomingCodeScanMessage.Help)
+
+ suspend fun processTabRemoved(message: IncomingCodeScanMessage.TabRemoved)
+
+ suspend fun processCodeScanCommand(message: CodeScanActionMessage)
+
+ suspend fun processResponseBodyLinkClicked(message: IncomingCodeScanMessage.ResponseBodyLinkClicked)
+
+ suspend fun processOpenIssuesPanel(message: IncomingCodeScanMessage.OpenIssuesPanel)
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/auth/CodeScanAuthUtils.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/auth/CodeScanAuthUtils.kt
new file mode 100644
index 0000000000..2e5744b176
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/auth/CodeScanAuthUtils.kt
@@ -0,0 +1,11 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth
+
+import com.intellij.openapi.project.Project
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnection
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity
+
+fun isCodeScanAvailable(project: Project): Boolean = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q) is ActiveConnection.ValidBearer
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanActionMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanActionMessage.kt
new file mode 100644
index 0000000000..01532c154f
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanActionMessage.kt
@@ -0,0 +1,16 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands
+
+import com.intellij.openapi.project.Project
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+
+data class CodeScanActionMessage(
+ val command: CodeScanCommand,
+ val project: Project,
+ val scanResult: CodeScanResponse? = null,
+ val scope: CodeWhispererConstants.CodeAnalysisScope,
+) : AmazonQMessage
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanCommand.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanCommand.kt
new file mode 100644
index 0000000000..f7b16705b1
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanCommand.kt
@@ -0,0 +1,8 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands
+
+enum class CodeScanCommand {
+ ScanComplete,
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanMessageListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanMessageListener.kt
new file mode 100644
index 0000000000..832741a1fc
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanMessageListener.kt
@@ -0,0 +1,21 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands
+
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.project.Project
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+
+@Service
+class CodeScanMessageListener {
+ private val _messages by lazy { MutableSharedFlow(extraBufferCapacity = 10) }
+ val flow = _messages.asSharedFlow()
+
+ fun onScanResult(result: CodeScanResponse?, scope: CodeWhispererConstants.CodeAnalysisScope, project: Project) {
+ _messages.tryEmit(CodeScanActionMessage(CodeScanCommand.ScanComplete, scanResult = result, scope = scope, project = project))
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatController.kt
new file mode 100644
index 0000000000..59a69c7ce4
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatController.kt
@@ -0,0 +1,197 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.controller
+
+import com.intellij.ide.BrowserUtil
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.FEATURE_NAME
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.InboundAppMessagesHandler
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildHelpChatAnswerContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildHelpChatPromptContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildNotInGitRepoChatContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildProjectScanFailedChatContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildScanCompleteChatContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildScanInProgressChatContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildStartNewScanChatContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildUserSelectionFileScanChatContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildUserSelectionProjectScanChatContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanActionMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanCommand
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.AuthenticationNeededExceptionMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.IncomingCodeScanMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
+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.getAuthType
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import software.aws.toolkits.resources.message
+
+class CodeScanChatController(
+ private val context: AmazonQAppInitContext,
+ private val chatSessionStorage: ChatSessionStorage,
+ private val authController: AuthController = AuthController(),
+) : InboundAppMessagesHandler {
+
+ private val messenger = context.messagesFromAppToUi
+ private val codeScanManager = CodeWhispererCodeScanManager.getInstance(context.project)
+ private val codeScanChatHelper = CodeScanChatHelper(context.messagesFromAppToUi, chatSessionStorage)
+ private val scanInProgressMessageId = "scanProgressMessage"
+
+ override suspend fun processScanQuickAction(message: IncomingCodeScanMessage.Scan) {
+ // TODO: telemetry
+
+ if (!checkForAuth(message.tabId)) {
+ return
+ }
+ if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return
+
+ codeScanChatHelper.setActiveCodeScanTabId(message.tabId)
+ codeScanChatHelper.addNewMessage(buildStartNewScanChatContent())
+ codeScanChatHelper.sendChatInputEnabledMessage(false)
+ }
+
+ override suspend fun processStartProjectScan(message: IncomingCodeScanMessage.StartProjectScan) {
+ if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return
+ codeScanChatHelper.addNewMessage(buildUserSelectionProjectScanChatContent())
+ if (!codeScanManager.isInsideWorkTree()) {
+ codeScanChatHelper.addNewMessage(buildNotInGitRepoChatContent())
+ }
+ codeScanChatHelper.addNewMessage(buildScanInProgressChatContent(currentStep = 1, isProject = true), messageIdOverride = scanInProgressMessageId)
+ codeScanManager.runCodeScan(CodeWhispererConstants.CodeAnalysisScope.PROJECT, initiatedByChat = true)
+ codeScanChatHelper.updateProgress(isProject = true, isCanceling = false)
+ }
+
+ override suspend fun processStopProjectScan(message: IncomingCodeScanMessage.StopProjectScan) {
+ if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return
+ codeScanChatHelper.updateProgress(isProject = true, isCanceling = true)
+ codeScanManager.stopCodeScan(CodeWhispererConstants.CodeAnalysisScope.PROJECT)
+ }
+
+ override suspend fun processStopFileScan(message: IncomingCodeScanMessage.StopFileScan) {
+ if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return
+ codeScanChatHelper.updateProgress(isProject = false, isCanceling = true)
+ codeScanManager.stopCodeScan(CodeWhispererConstants.CodeAnalysisScope.FILE)
+ }
+
+ override suspend fun processStartFileScan(message: IncomingCodeScanMessage.StartFileScan) {
+ if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return
+ codeScanChatHelper.addNewMessage(buildUserSelectionFileScanChatContent())
+ codeScanChatHelper.addNewMessage(buildScanInProgressChatContent(currentStep = 1, isProject = false), messageIdOverride = scanInProgressMessageId)
+ codeScanManager.runCodeScan(CodeWhispererConstants.CodeAnalysisScope.FILE, initiatedByChat = true)
+ codeScanChatHelper.updateProgress(isProject = false, isCanceling = false)
+ }
+
+ override suspend fun processTabCreated(message: IncomingCodeScanMessage.TabCreated) {
+ logger.debug { "$FEATURE_NAME: New tab created: $message" }
+ codeScanChatHelper.setActiveCodeScanTabId(message.tabId)
+ CodeWhispererTelemetryService.getInstance().sendCodeScanNewTabEvent(getAuthType(context.project))
+ }
+
+ override suspend fun processClearQuickAction(message: IncomingCodeScanMessage.ClearChat) {
+ chatSessionStorage.deleteSession(message.tabId)
+ }
+
+ override suspend fun processHelpQuickAction(message: IncomingCodeScanMessage.Help) {
+ if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return
+ codeScanChatHelper.addNewMessage(buildHelpChatPromptContent())
+ codeScanChatHelper.addNewMessage(buildHelpChatAnswerContent())
+ }
+
+ override suspend fun processTabRemoved(message: IncomingCodeScanMessage.TabRemoved) {
+ chatSessionStorage.deleteSession(message.tabId)
+ }
+
+ override suspend fun processResponseBodyLinkClicked(message: IncomingCodeScanMessage.ResponseBodyLinkClicked) {
+ BrowserUtil.browse(message.link)
+ }
+
+ override suspend fun processCodeScanCommand(message: CodeScanActionMessage) {
+ if (message.project != context.project) return
+ val isProject = message.scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT
+ when (message.command) {
+ CodeScanCommand.ScanComplete -> {
+ codeScanChatHelper.addNewMessage(
+ buildScanInProgressChatContent(currentStep = 2, isProject = isProject),
+ messageIdOverride = scanInProgressMessageId
+ )
+ val result = message.scanResult
+ if (result != null) {
+ handleCodeScanResult(result, message.scope)
+ } else {
+ codeScanChatHelper.addNewMessage(buildProjectScanFailedChatContent("Cancelled"))
+ codeScanChatHelper.clearProgress()
+ }
+ }
+ }
+ }
+
+ private suspend fun handleCodeScanResult(result: CodeScanResponse, scope: CodeWhispererConstants.CodeAnalysisScope) {
+ val isProject = scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT
+ when (result) {
+ is CodeScanResponse.Success -> {
+ codeScanChatHelper.addNewMessage(
+ buildScanInProgressChatContent(currentStep = 3, isProject = isProject),
+ messageIdOverride = scanInProgressMessageId
+ )
+ codeScanChatHelper.addNewMessage(buildScanCompleteChatContent(result.issues, isProject = isProject))
+ codeScanChatHelper.clearProgress()
+ }
+ is CodeScanResponse.Failure -> {
+ codeScanChatHelper.addNewMessage(buildScanInProgressChatContent(3, isProject = isProject), messageIdOverride = scanInProgressMessageId)
+ codeScanChatHelper.addNewMessage(buildProjectScanFailedChatContent(result.failureReason.message))
+ codeScanChatHelper.clearProgress()
+ }
+ }
+ }
+
+ /**
+ * Return true if authenticated, else show authentication message and return false
+ * // TODO: Refactor this to avoid code duplication with other controllers
+ */
+ private suspend fun checkForAuth(tabId: String): Boolean {
+ try {
+ val session = chatSessionStorage.getSession(tabId)
+ logger.debug { "$FEATURE_NAME: Session created with id: ${session.tabId}" }
+
+ val credentialState = authController.getAuthNeededStates(context.project).amazonQ
+ if (credentialState != null) {
+ messenger.publish(
+ AuthenticationNeededExceptionMessage(
+ tabId = session.tabId,
+ authType = credentialState.authType,
+ message = credentialState.message
+ )
+ )
+ session.isAuthenticating = true
+ return false
+ }
+ } catch (err: Exception) {
+ messenger.publish(
+ CodeScanChatMessage(
+ tabId = tabId,
+ messageType = ChatMessageType.Answer,
+ message = message("codescan.chat.message.error_request")
+ )
+ )
+ return false
+ }
+
+ return true
+ }
+
+ override suspend fun processOpenIssuesPanel(message: IncomingCodeScanMessage.OpenIssuesPanel) {
+ if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return
+ codeScanManager.showCodeScanUI()
+ }
+
+ companion object {
+ private val logger = getLogger()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt
new file mode 100644
index 0000000000..6a9acbbfc0
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt
@@ -0,0 +1,81 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.controller
+
+import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildClearPromptProgressMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildPromptProgressMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.ChatInputEnabledMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessageContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.UpdatePlaceholderMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import java.util.UUID
+
+class CodeScanChatHelper(
+ private val messagePublisher: MessagePublisher,
+ private val chatSessionStorage: ChatSessionStorage,
+) {
+ private var activeCodeScanTabId: String? = null
+
+ fun setActiveCodeScanTabId(tabId: String) {
+ activeCodeScanTabId = tabId
+ }
+
+ fun getActiveCodeScanTabId(): String? = activeCodeScanTabId
+
+ private fun isInValidSession() = activeCodeScanTabId == null || chatSessionStorage.getSession(activeCodeScanTabId as String).isAuthenticating
+
+ suspend fun addNewMessage(
+ content: CodeScanChatMessageContent,
+ messageIdOverride: String? = null,
+ clearPreviousItemButtons: Boolean? = false,
+ ) {
+ if (isInValidSession()) return
+
+ messagePublisher.publish(
+ CodeScanChatMessage(
+ tabId = activeCodeScanTabId as String,
+ messageId = messageIdOverride ?: UUID.randomUUID().toString(),
+ messageType = content.type,
+ message = content.message,
+ buttons = content.buttons,
+ formItems = content.formItems,
+ followUps = content.followUps,
+ canBeVoted = content.canBeVoted,
+ isLoading = content.type == ChatMessageType.AnswerPart,
+ clearPreviousItemButtons = clearPreviousItemButtons as Boolean
+ )
+ )
+ }
+
+ suspend fun updateProgress(isProject: Boolean = false, isCanceling: Boolean = false) {
+ if (isInValidSession()) return
+ messagePublisher.publish(buildPromptProgressMessage(activeCodeScanTabId as String, isProject, isCanceling))
+ sendChatInputEnabledMessage(false)
+ }
+
+ suspend fun clearProgress() {
+ if (isInValidSession()) return
+ messagePublisher.publish(buildClearPromptProgressMessage(activeCodeScanTabId as String))
+ sendChatInputEnabledMessage(true)
+ }
+
+ suspend fun sendChatInputEnabledMessage(isEnabled: Boolean) {
+ if (isInValidSession()) return
+ messagePublisher.publish(ChatInputEnabledMessage(activeCodeScanTabId as String, enabled = isEnabled))
+ }
+
+ suspend fun updatePlaceholder(newPlaceholder: String) {
+ if (isInValidSession()) return
+
+ messagePublisher.publish(
+ UpdatePlaceholderMessage(
+ tabId = activeCodeScanTabId as String,
+ newPlaceholder = newPlaceholder
+ )
+ )
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/messages/CodeScanMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/messages/CodeScanMessage.kt
new file mode 100644
index 0000000000..062e974336
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/messages/CodeScanMessage.kt
@@ -0,0 +1,185 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthFollowUpType
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import software.aws.toolkits.jetbrains.services.cwc.messages.FollowUp
+import java.time.Instant
+import java.util.UUID
+
+const val CODE_SCAN_TAB_NAME = "codescan"
+
+enum class CodeScanButtonId(val id: String) {
+ StartProjectScan("codescan_start_project_scan"),
+ StartFileScan("codescan_start_file_scan"),
+ StopProjectScan("codescan_stop_project_scan"),
+ StopFileScan("codescan_stop_file_scan"),
+ OpenIssuesPanel("codescan_open_issues"),
+}
+
+data class Button(
+ val id: String,
+ val text: String,
+ val description: String? = null,
+ val icon: String? = null,
+ val keepCardAfterClick: Boolean? = false,
+ val disabled: Boolean? = false,
+ val waitMandatoryFormItems: Boolean? = false,
+)
+data class ProgressField(
+ val title: String? = null,
+ val value: Int? = null,
+ val status: String? = null,
+ val actions: List? = null,
+ val text: String? = null,
+)
+
+data class FormItemOption(
+ val label: String,
+ val value: String,
+)
+
+data class FormItem(
+ val id: String,
+ val type: String = "select",
+ val title: String,
+ val mandatory: Boolean = true,
+ val options: List = emptyList(),
+)
+
+sealed interface CodeScanBaseMessage : AmazonQMessage
+
+// === UI -> App Messages ===
+sealed interface IncomingCodeScanMessage : CodeScanBaseMessage {
+ data class Scan(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class StartProjectScan(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class StartFileScan(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class StopProjectScan(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class StopFileScan(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class OpenIssuesPanel(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class TabCreated(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class Help(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class ClearChat(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class TabRemoved(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeScanMessage
+
+ data class ResponseBodyLinkClicked(
+ @JsonProperty("tabID") val tabId: String,
+ val link: String,
+ ) : IncomingCodeScanMessage
+}
+
+// === App -> UI messages ===
+sealed class CodeScanUiMessage(
+ open val tabId: String?,
+ open val type: String,
+ open val messageId: String? = UUID.randomUUID().toString(),
+) : CodeScanBaseMessage {
+ val time = Instant.now().epochSecond
+ val sender = CODE_SCAN_TAB_NAME
+}
+
+data class PromptProgressMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val progressField: ProgressField? = null,
+) : CodeScanUiMessage(
+ tabId = tabId,
+ type = "updatePromptProgress",
+)
+
+data class ChatInputEnabledMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val enabled: Boolean,
+) : CodeScanUiMessage(
+ tabId = tabId,
+ type = "chatInputEnabledMessage"
+)
+
+data class AuthenticationUpdateMessage(
+ val authenticatingTabIDs: List,
+ val featureDevEnabled: Boolean,
+ val codeTransformEnabled: Boolean,
+ val codeScanEnabled: Boolean,
+ val codeTestEnabled: Boolean,
+ val docEnabled: Boolean,
+ val message: String? = null,
+) : CodeScanUiMessage(
+ null,
+ type = "authenticationUpdateMessage"
+)
+
+data class AuthenticationNeededExceptionMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val authType: AuthFollowUpType,
+ val message: String? = null,
+) : CodeScanUiMessage(
+ tabId = tabId,
+ type = "authNeededException"
+)
+
+data class CodeScanChatMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ override val messageId: String? = UUID.randomUUID().toString(),
+ val messageType: ChatMessageType,
+ val message: String? = null,
+ val buttons: List? = null,
+ val formItems: List? = null,
+ val followUps: List? = null,
+ val isLoading: Boolean = false,
+ val canBeVoted: Boolean = true,
+ val clearPreviousItemButtons: Boolean = true,
+ val command: String? = null,
+) : CodeScanUiMessage(
+ messageId = messageId,
+ tabId = tabId,
+ type = "chatMessage",
+)
+
+data class UpdatePlaceholderMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val newPlaceholder: String,
+) : CodeScanUiMessage(
+ tabId = tabId,
+ type = "updatePlaceholderMessage"
+)
+
+data class CodeScanChatMessageContent(
+ val message: String? = null,
+ val buttons: List? = null,
+ val formItems: List? = null,
+ val followUps: List? = null,
+ val type: ChatMessageType,
+ val canBeVoted: Boolean = true,
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/session/Session.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/session/Session.kt
new file mode 100644
index 0000000000..749e39fb24
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/session/Session.kt
@@ -0,0 +1,9 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.session
+
+data class Session(val tabId: String) {
+ var isAuthenticating: Boolean = false
+ var authNeededNotified: Boolean = false
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt
new file mode 100644
index 0000000000..fb05a7beda
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt
@@ -0,0 +1,33 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.session.Session
+
+class ChatSessionStorage {
+ private val sessions = mutableMapOf()
+
+ private fun createSession(tabId: String): Session {
+ val session = Session(tabId)
+ sessions[tabId] = session
+ return session
+ }
+
+ fun getSession(tabId: String): Session = sessions[tabId] ?: createSession(tabId)
+
+ fun deleteSession(tabId: String) {
+ sessions.remove(tabId)
+ }
+
+ // Find all sessions that are currently waiting to be authenticated
+ fun getAuthenticatingSessions(): List = this.sessions.values.filter { it.isAuthenticating }
+
+ fun changeAuthenticationNeeded(isAuthenticating: Boolean) {
+ sessions.keys.forEach { sessions[it]?.isAuthenticating = isAuthenticating }
+ }
+
+ fun changeAuthenticationNeededNotified(authNeededNotified: Boolean) {
+ sessions.keys.forEach { sessions[it]?.authNeededNotified = authNeededNotified }
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt
new file mode 100644
index 0000000000..036a048273
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt
@@ -0,0 +1,87 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest
+
+import com.intellij.openapi.application.ApplicationManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.controller.CodeTestChatController
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.AuthenticationUpdateMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.IncomingCodeTestMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.storage.ChatSessionStorage
+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
+
+class CodeTestChatApp(private val scope: CoroutineScope) : AmazonQApp {
+
+ override val tabTypes = listOf("codetest")
+
+ override fun init(context: AmazonQAppInitContext) {
+ val chatSessionStorage = ChatSessionStorage()
+ val inboundAppMessagesHandler =
+ CodeTestChatController(context, chatSessionStorage, cs = scope)
+
+ context.messageTypeRegistry.register(
+ "clear" to IncomingCodeTestMessage.ClearChat::class,
+ "help" to IncomingCodeTestMessage.Help::class,
+ "chat-prompt" to IncomingCodeTestMessage.ChatPrompt::class,
+ "new-tab-was-created" to IncomingCodeTestMessage.NewTabCreated::class,
+ "tab-was-removed" to IncomingCodeTestMessage.TabRemoved::class,
+ "start-test-gen" to IncomingCodeTestMessage.StartTestGen::class,
+ "response-body-link-click" to IncomingCodeTestMessage.ClickedLink::class,
+ "button-click" to IncomingCodeTestMessage.ButtonClicked::class
+ )
+
+ scope.launch {
+ context.messagesFromUiToApp.flow.collect { message ->
+ // Launch a new coroutine to handle each message
+ scope.launch { handleMessage(message, inboundAppMessagesHandler) }
+ }
+ }
+
+ ApplicationManager.getApplication().messageBus.connect(this).subscribe(
+ ToolkitConnectionManagerListener.TOPIC,
+ object : ToolkitConnectionManagerListener {
+ override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
+ scope.launch {
+ context.messagesFromAppToUi.publish(
+ AuthenticationUpdateMessage(
+ featureDevEnabled = isFeatureDevAvailable(context.project),
+ codeTransformEnabled = isCodeTransformAvailable(context.project),
+ codeScanEnabled = isCodeScanAvailable(context.project),
+ codeTestEnabled = isCodeTestAvailable(context.project),
+ docEnabled = isDocAvailable(context.project),
+ authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabId }
+ )
+ )
+ }
+ }
+ }
+ )
+ }
+
+ private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
+ when (message) {
+ is IncomingCodeTestMessage.ClearChat -> inboundAppMessagesHandler.processClearQuickAction(message)
+ is IncomingCodeTestMessage.Help -> inboundAppMessagesHandler.processHelpQuickAction(message)
+ is IncomingCodeTestMessage.ChatPrompt -> inboundAppMessagesHandler.processPromptChatMessage(message)
+ is IncomingCodeTestMessage.NewTabCreated -> inboundAppMessagesHandler.processNewTabCreatedMessage(message)
+ is IncomingCodeTestMessage.TabRemoved -> inboundAppMessagesHandler.processTabRemovedMessage(message)
+ is IncomingCodeTestMessage.StartTestGen -> inboundAppMessagesHandler.processStartTestGen(message)
+ is IncomingCodeTestMessage.ClickedLink -> inboundAppMessagesHandler.processLinkClick(message)
+ is IncomingCodeTestMessage.ButtonClicked -> inboundAppMessagesHandler.processButtonClickedMessage(message)
+ }
+ }
+
+ override fun dispose() {
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatAppFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatAppFactory.kt
new file mode 100644
index 0000000000..46ccd461d9
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatAppFactory.kt
@@ -0,0 +1,11 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest
+import com.intellij.openapi.project.Project
+import kotlinx.coroutines.CoroutineScope
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppFactory
+
+class CodeTestChatAppFactory(private val cs: CoroutineScope) : AmazonQAppFactory {
+ override fun createApp(project: Project) = CodeTestChatApp(cs)
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatItems.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatItems.kt
new file mode 100644
index 0000000000..4ed35e3506
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatItems.kt
@@ -0,0 +1,38 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.Button
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestButtonId
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.ProgressField
+import software.aws.toolkits.resources.message
+
+val cancellingProgressField = ProgressField(
+ status = "warning",
+ text = message("general.canceling"),
+ value = -1,
+ actions = emptyList()
+)
+
+// TODO: Need to change the string after the F2F
+val testGenCompletedField = ProgressField(
+ status = "success",
+ text = message("general.success"),
+ value = 100,
+ actions = emptyList()
+)
+
+val cancelTestGenButton = Button(
+ id = CodeTestButtonId.StopTestGeneration.id,
+ text = message("general.cancel"),
+ icon = "cancel"
+)
+
+fun testGenProgressField(value: Int) = ProgressField(
+ status = "default",
+ text = message("testgen.progressbar.generate_unit_tests"),
+ value = value,
+ valueText = "$value%",
+ actions = listOf(cancelTestGenButton)
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestConstants.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestConstants.kt
new file mode 100644
index 0000000000..e5e25ed7cb
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestConstants.kt
@@ -0,0 +1,19 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest
+
+const val FEATURE_NAME = "Amazon Q Unit Test Generation"
+
+enum class ConversationState {
+ IDLE,
+ WAITING_FOR_BUILD_COMMAND_INPUT,
+ WAITING_FOR_REGENERATE_INPUT,
+ IN_PROGRESS,
+}
+
+fun generateSummaryMessage(fileName: String): String = """
+ Sure. This may take a few minutes. I'll share updates here as I work on this.
+ **Generating unit tests for the following methods in $fileName**:
+
+""".trimIndent()
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererCodeTestSession.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererCodeTestSession.kt
new file mode 100644
index 0000000000..058c1bd472
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererCodeTestSession.kt
@@ -0,0 +1,149 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest
+import com.intellij.openapi.project.Project
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.time.withTimeout
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.controller.CodeTestChatHelper
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestChatMessageContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.PreviousUTGIterationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.codetest.sessionconfig.CodeTestSessionConfig
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.CreateUploadUrlServiceInvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_BYTES_IN_KB
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_MILLIS_IN_SECOND
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererZipUploadManager
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread
+import java.nio.file.Path
+import java.time.Duration
+import java.time.Instant
+import java.util.*
+import kotlin.coroutines.coroutineContext
+
+// TODO: Refactor with CodeWhispererCodeScanSession code since both are about zip CreateUploadUrl logic
+class CodeWhispererCodeTestSession(val sessionContext: CodeTestSessionContext) {
+ private fun now() = Instant.now().toEpochMilli()
+
+ /**
+ * Run UTG sessions are follow steps:
+ * 1. Zipping project
+ * 2. Creating Upload url & Upload to S3 bucket
+ * 3. StartTestGeneration API -> Get JobId
+ * 4. GetTestGeneration API
+ * 5. ExportResultsArchieve API
+ */
+ suspend fun run(codeTestChatHelper: CodeTestChatHelper, previousIterationContext: PreviousUTGIterationContext?): CodeTestResponseContext {
+ try {
+ assertIsNonDispatchThread()
+ coroutineContext.ensureActive()
+
+ val path = sessionContext.sessionConfig.getRelativePath()
+ ?: throw RuntimeException("Can not determine current file path for adding unit tests")
+
+ // Add card answer to show UTG in progress
+ val testSummaryMessageId =
+ if (previousIterationContext == null) {
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = generateSummaryMessage(path.fileName.toString()),
+ type = ChatMessageType.AnswerStream
+ )
+ ).also {
+ // For streaming effect
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(type = ChatMessageType.AnswerPart)
+ )
+ codeTestChatHelper.updateUI(
+ loadingChat = true,
+ promptInputDisabledState = true
+ )
+ if (it == null) {
+ throw RuntimeException("Can not add test summary card")
+ }
+ }
+ } else {
+ // non-first iteration doesn't have a test summary card
+ null
+ }
+
+ val (payloadContext, sourceZip) = withTimeout(Duration.ofSeconds(sessionContext.sessionConfig.createPayloadTimeoutInSeconds())) {
+ sessionContext.sessionConfig.createPayload()
+ }
+
+ LOG.debug {
+ "Total size of source payload in KB: ${payloadContext.srcPayloadSize * 1.0 / TOTAL_BYTES_IN_KB} \n" +
+ "Total size of build payload in KB: ${(payloadContext.buildPayloadSize ?: 0) * 1.0 / TOTAL_BYTES_IN_KB} \n" +
+ "Total size of source zip file in KB: ${payloadContext.srcZipFileSize * 1.0 / TOTAL_BYTES_IN_KB} \n" +
+ "Total number of lines included: ${payloadContext.totalLines} \n" +
+ "Total number of files included in payload: ${payloadContext.totalFiles} \n" +
+ "Total time taken for creating payload: ${payloadContext.totalTimeInMilliseconds * 1.0 / TOTAL_MILLIS_IN_SECOND} seconds\n" +
+ "Payload context language: ${payloadContext.language}"
+ }
+
+ // 2 & 3. CreateUploadURL and upload the context.
+ val artifactsUploadStartTime = now()
+ val taskName = UUID.randomUUID().toString()
+ val sourceZipUploadResponse =
+ CodeWhispererZipUploadManager.getInstance(sessionContext.project).createUploadUrlAndUpload(
+ sourceZip,
+ "SourceCode",
+ CodeWhispererConstants.UploadTaskType.UTG,
+ taskName
+ )
+
+ sourceZipUploadResponse.uploadId()
+
+ LOG.debug {
+ "Successfully uploaded source zip to s3: " +
+ "Upload id: ${sourceZipUploadResponse.uploadId()} " +
+ "Request id: ${sourceZipUploadResponse.responseMetadata().requestId()}"
+ }
+ val artifactsUploadDuration = now() - artifactsUploadStartTime
+
+ val codeTestResponseContext = CodeTestResponseContext(
+ payloadContext,
+ CreateUploadUrlServiceInvocationContext(artifactsUploadDuration = artifactsUploadDuration),
+ path,
+ sourceZipUploadResponse,
+ testSummaryMessageId
+ )
+ // TODO send telemetry for upload duration
+
+ return codeTestResponseContext
+ } catch (e: Exception) {
+ LOG.debug(e) { "Error when creating tests for the current file" }
+ throw e
+ }
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ }
+}
+
+sealed class CodeTestResponse {
+ abstract val responseContext: CodeTestResponseContext
+ data class Success(val message: String, override val responseContext: CodeTestResponseContext) : CodeTestResponse()
+
+ data class Error(val errorMessage: String, override val responseContext: CodeTestResponseContext) : CodeTestResponse()
+}
+
+data class CodeTestSessionContext(
+ val project: Project,
+ val sessionConfig: CodeTestSessionConfig,
+)
+
+data class CodeTestResponseContext(
+ val payloadContext: PayloadContext,
+ val serviceInvocationContext: CreateUploadUrlServiceInvocationContext,
+ val currentFileRelativePath: Path,
+ val createUploadUrlResponse: CreateUploadUrlResponse,
+ val testSummaryMessageId: String?,
+ val reason: String? = null,
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt
new file mode 100644
index 0000000000..4df0bc0b96
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt
@@ -0,0 +1,560 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest
+
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl
+import com.intellij.openapi.project.Project
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
+import software.amazon.awssdk.services.codewhispererruntime.model.GetTestGenerationResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.Range
+import software.amazon.awssdk.services.codewhispererruntime.model.StartTestGenerationResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.TargetCode
+import software.amazon.awssdk.services.codewhispererruntime.model.TestGenerationJobStatus
+import software.amazon.awssdk.services.codewhispererstreaming.model.ExportContext
+import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.jetbrains.services.amazonq.clients.AmazonQStreamingClient
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.controller.CodeTestChatHelper
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.Button
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestChatMessageContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.PreviousUTGIterationContext
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.ShortAnswer
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.BuildAndExecuteProgressStatus
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.Session
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.utils.combineBuildAndExecuteLogFiles
+import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency
+import software.aws.toolkits.jetbrains.services.codewhisperer.codetest.sessionconfig.CodeTestSessionConfig
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference
+import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+import software.aws.toolkits.jetbrains.utils.isQConnected
+import software.aws.toolkits.resources.message
+import software.aws.toolkits.telemetry.AmazonqTelemetry
+import software.aws.toolkits.telemetry.MetricResult
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.IOException
+import java.nio.file.Paths
+import java.time.Instant
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.zip.ZipInputStream
+
+@Service
+class CodeWhispererUTGChatManager(val project: Project, private val cs: CoroutineScope) {
+ // TODO: consider combining this with session.isGeneratingTests
+ private val isUTGInProgress = AtomicBoolean(false)
+ private val mapper = jacksonObjectMapper()
+ private val generatedTestDiffs = mutableMapOf()
+
+ private fun throwIfCancelled(session: Session) {
+ if (!session.isGeneratingTests) {
+ error(message("testgen.message.cancelled"))
+ }
+ }
+
+ private suspend fun launchTestGenFlow(
+ prompt: String,
+ codeTestChatHelper: CodeTestChatHelper,
+ previousIterationContext: PreviousUTGIterationContext?,
+ selectionRange: Range?,
+ ) {
+ // 1st API call: Zip project and call CreateUploadUrl
+ val session = codeTestChatHelper.getActiveSession()
+ session.isGeneratingTests = true
+ session.iteration++
+
+ // Set the Progress bar to "Generating unit tests..."
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = true,
+ promptInputProgress = testGenProgressField(0),
+ )
+
+ val codeTestResponseContext = createUploadUrl(codeTestChatHelper, previousIterationContext)
+ session.srcPayloadSize = codeTestResponseContext.payloadContext.srcPayloadSize
+ session.srcZipFileSize = codeTestResponseContext.payloadContext.srcZipFileSize
+ session.artifactUploadDuration = codeTestResponseContext.serviceInvocationContext.artifactsUploadDuration
+ val path = codeTestResponseContext.currentFileRelativePath
+
+ val createUploadUrlResponse = codeTestResponseContext.createUploadUrlResponse ?: return
+ throwIfCancelled(session)
+
+ LOG.debug {
+ "Q TestGen StartTestGenerationRequest: TabId= ${codeTestChatHelper.getActiveCodeTestTabId()}: " +
+ "uploadId: ${createUploadUrlResponse.uploadId()}, relativeTargetPath: ${codeTestResponseContext.currentFileRelativePath}, " +
+ "selectionRange: $selectionRange, "
+ }
+
+ // 2nd API call: StartTestGeneration
+ val startTestGenerationResponse = startTestGeneration(
+ uploadId = createUploadUrlResponse.uploadId(),
+ targetCode = listOf(
+ TargetCode.builder()
+ .relativeTargetPath(codeTestResponseContext.currentFileRelativePath.toString())
+ .targetLineRangeList(
+ if (selectionRange != null) {
+ listOf(
+ selectionRange
+ )
+ } else {
+ emptyList()
+ }
+ )
+ .build()
+ ),
+ userInput = prompt
+ )
+
+ val job = startTestGenerationResponse.testGenerationJob()
+ session.testGenerationJobGroupName = job.testGenerationJobGroupName()
+ session.testGenerationJob = job.testGenerationJobId()
+ throwIfCancelled(session)
+
+ // 3rd API call: Step 3: Polling mechanism on test job status with getTestGenStatus getTestGeneration
+ var finished = false
+ var testGenerationResponse: GetTestGenerationResponse? = null
+
+ var shortAnswer = ShortAnswer()
+ LOG.debug {
+ "Q TestGen session: ${codeTestChatHelper.getActiveCodeTestTabId()}: " +
+ "polling result for id: ${job.testGenerationJobId()}, group name: ${job.testGenerationJobGroupName()}, " +
+ "request id: ${startTestGenerationResponse.responseMetadata().requestId()}"
+ }
+
+ while (!finished) {
+ throwIfCancelled(session)
+ testGenerationResponse = getTestGenerationStatus(job.testGenerationJobId(), job.testGenerationJobGroupName())
+
+ val status = testGenerationResponse.testGenerationJob().status()
+ if (status == TestGenerationJobStatus.COMPLETED) {
+ LOG.debug {
+ "Q TestGen session: ${codeTestChatHelper.getActiveCodeTestTabId()}: " +
+ "Test generation completed, short answer string: ${testGenerationResponse.testGenerationJob().shortAnswer()}"
+ }
+ finished = true
+ if (testGenerationResponse.testGenerationJob().shortAnswer() != null) {
+ shortAnswer = parseShortAnswerString(testGenerationResponse.testGenerationJob().shortAnswer())
+
+ val testFileName = shortAnswer.testFilePath?.let { File(it).name }.orEmpty()
+ session.testFileName = testFileName
+ // Setting default value to 0 if the value is null or invalid
+ session.numberOfUnitTestCasesGenerated = shortAnswer.numberOfTestMethods
+ session.testFileRelativePathToProjectRoot = getTestFilePathRelativeToRoot(shortAnswer)
+
+ // update test summary card in success case
+ if (previousIterationContext == null) {
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(
+ message = generateSummaryMessage(path.fileName.toString()) + shortAnswer.planSummary,
+ type = ChatMessageType.Answer,
+ footer = listOf(testFileName)
+ ),
+ messageIdOverride = codeTestResponseContext.testSummaryMessageId
+ )
+ }
+ // update test summary card
+ } else {
+ throw Exception(message("testgen.message.failed"))
+ }
+ } else if (status == TestGenerationJobStatus.FAILED) {
+ LOG.debug {
+ "Q TestGen session: ${codeTestChatHelper.getActiveCodeTestTabId()}: " +
+ "Test generation failed, short answer string: ${testGenerationResponse.testGenerationJob().shortAnswer()}"
+ }
+ if (testGenerationResponse.testGenerationJob().shortAnswer() != null) {
+ shortAnswer = parseShortAnswerString(testGenerationResponse.testGenerationJob().shortAnswer())
+ if (shortAnswer.stopIteration == "true") {
+ throw Exception("${shortAnswer.planSummary}")
+ }
+ }
+
+ // TODO: Modify text according to FnF
+ throw Exception(message("testgen.message.failed"))
+ } else {
+ // In progress
+ LOG.debug {
+ "Q TestGen session: ${codeTestChatHelper.getActiveCodeTestTabId()}: " +
+ "Test generation in progress, progress rate ${testGenerationResponse.testGenerationJob().progressRate()}}"
+ }
+ val progressRate = testGenerationResponse.testGenerationJob().progressRate() ?: 0
+
+ if (previousIterationContext == null && testGenerationResponse.testGenerationJob().shortAnswer() != null) {
+ shortAnswer = parseShortAnswerString(testGenerationResponse.testGenerationJob().shortAnswer())
+ if (shortAnswer.stopIteration == "true") {
+ throw Exception("${shortAnswer.planSummary}")
+ }
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(
+ message = generateSummaryMessage(path.fileName.toString()) + shortAnswer.planSummary,
+ type = ChatMessageType.Answer
+ ),
+ messageIdOverride = codeTestResponseContext.testSummaryMessageId
+ )
+ }
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = true,
+ promptInputProgress = testGenProgressField(progressRate),
+ )
+ }
+
+ // polling every 2 seconds to reduce # of API calls
+ delay(2000)
+ }
+
+ throwIfCancelled(session)
+
+ // 4th API call: Step 4: ExportResultsArchive
+ val byteArray = AmazonQStreamingClient.getInstance(project).exportResultArchive(
+ createUploadUrlResponse.uploadId(),
+ ExportIntent.UNIT_TESTS,
+ ExportContext.fromUnitTestGenerationExportContext {
+ it.testGenerationJobId(job.testGenerationJobId())
+ it.testGenerationJobGroupName(job.testGenerationJobGroupName())
+ },
+ { e ->
+ LOG.error(e) { "ExportResultArchive failed: ${e.message}" }
+ },
+ { startTime ->
+ LOG.info { "ExportResultArchive latency: ${calculateTotalLatency(startTime, Instant.now())}" }
+ }
+ )
+ val result = byteArray.reduce { acc, next -> acc + next } // To map the result it is needed to combine the full byte array
+ storeGeneratedTestDiffs(result, session)
+ if (!session.isGeneratingTests) {
+ // TODO: Modify text according to FnF
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = message("testgen.message.failed"),
+ type = ChatMessageType.Answer,
+ canBeVoted = true
+ )
+ )
+ return
+ }
+
+ val codeReference = shortAnswer.codeReferences?.map { ref ->
+ CodeReference(
+ licenseName = ref.licenseName,
+ url = ref.url,
+ information = "${ref.licenseName} - ${ref.repository} "
+ )
+ }
+ shortAnswer.codeReferences?.let { session.codeReferences = it }
+ val isReferenceAllowed = CodeWhispererSettings.getInstance().isIncludeCodeWithReference()
+ if (!isReferenceAllowed && codeReference?.isNotEmpty() == true) {
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = """
+ Your settings do not allow code generation with references.
+ """.trimIndent(),
+ type = ChatMessageType.Answer,
+ )
+ )
+ } else {
+ if (previousIterationContext == null) {
+ // show another card as the answer
+ val viewDiffMessageId = codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = """
+ Please see the unit tests generated below. Click "View Diff" to review the changes in the code editor.
+ """.trimIndent(),
+ type = ChatMessageType.Answer,
+ buttons = listOf(Button("utg_view_diff", "View Diff", keepCardAfterClick = true, position = "outside", status = "info")),
+ fileList = listOf(getTestFilePathRelativeToRoot(shortAnswer)),
+ projectRootName = project.name,
+ canBeVoted = true,
+ codeReference = codeReference
+ )
+ )
+ session.viewDiffMessageId = viewDiffMessageId
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = false,
+ promptInputPlaceholder = "Specify a function(s) in the current file(optional)",
+ promptInputProgress = testGenCompletedField,
+ )
+ } else {
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(
+ type = ChatMessageType.Answer,
+ buttons = listOf(Button("utg_view_diff", "View Diff", keepCardAfterClick = true, position = "outside", status = "info")),
+ fileList = listOf(getTestFilePathRelativeToRoot(shortAnswer)),
+ projectRootName = project.name,
+ codeReference = codeReference
+ ),
+ messageIdOverride = previousIterationContext.buildAndExecuteMessageId
+ )
+ session.viewDiffMessageId = previousIterationContext.buildAndExecuteMessageId
+ codeTestChatHelper.updateUI(
+ loadingChat = false,
+ )
+ }
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = true,
+ promptInputPlaceholder = message("testgen.placeholder.view_diff"),
+ promptInputProgress = testGenCompletedField,
+ )
+ delay(1000)
+ }
+
+ codeTestChatHelper.sendUpdatePromptProgress(codeTestChatHelper.getActiveSession().tabId, null)
+ }
+
+ // Input: test file path relative to project root's parent .
+ // Output: test file path relative to project root.
+ // shortAnswer.testFilePath has a format of /.
+ // test file path in generatedTestDiffs map has a format of resultArtifacts/.
+ // both needs to be handled the same way which is remove the first sub-directory
+ private fun getTestFilePathRelativeToRoot(shortAnswer: ShortAnswer): String {
+ val pathString = shortAnswer.testFilePath ?: generatedTestDiffs.keys.firstOrNull() ?: throw RuntimeException("No test file path found")
+ val path = Paths.get(pathString)
+ val updatedPath = path.subpath(1, path.nameCount).toString()
+ return updatedPath
+ }
+
+ private fun parseShortAnswerString(shortAnswerString: String): ShortAnswer {
+ // Step 1: Replace single quotes with double quotes
+ var jsonString = shortAnswerString.replace("'", "\"").replace("```", "")
+
+ // Step 2: Replace Python's None with JSON's null
+ jsonString = jsonString.replace(": None", ": null")
+
+ // Step 3: remove extra quotes in the head and tail
+ if (jsonString.startsWith("\"") && jsonString.endsWith("\"")) {
+ jsonString = jsonString.substring(1, jsonString.length - 1) // Remove the first and last quote
+ }
+
+ // Step 4: unescape it
+ jsonString = jsonString.replace("\\\"", "\"")
+ .replace("\\\\", "\\")
+ // Deserialize JSON to Kotlin data class
+ try {
+ val shortAnswer: ShortAnswer = mapper.readValue(jsonString, ShortAnswer::class.java)
+ return shortAnswer
+ } catch (e: JsonParseException) {
+ LOG.debug(e) { "Test Generation JSON parsing error: ${e.message}" }
+ throw e
+ } catch (e: Exception) {
+ LOG.debug(e) { "Error parsing JSON" }
+ throw e
+ }
+ }
+
+ private fun storeGeneratedTestDiffs(byteArray: ByteArray, session: Session) {
+ try {
+ val byteArrayInputStream = ByteArrayInputStream(byteArray)
+ ZipInputStream(byteArrayInputStream).use { zipInputStream ->
+ var zipEntry = zipInputStream.nextEntry
+
+ while (zipEntry != null) {
+ if (zipEntry.isDirectory) {
+ zipInputStream.closeEntry()
+ zipEntry = zipInputStream.nextEntry
+ // We are only interested in test file diff in zip entries
+ continue
+ }
+
+ val baos = ByteArrayOutputStream()
+ val buffer = ByteArray(1024)
+ var len: Int
+
+ while (zipInputStream.read(buffer).also { len = it } > 0) {
+ baos.write(buffer, 0, len)
+ }
+
+ val fileContent = baos.toByteArray()
+ if (fileContent.toString(Charsets.UTF_8).isEmpty()) {
+ session.isGeneratingTests = false
+ return
+ }
+ val zipEntryPath = Paths.get(zipEntry.name)
+
+ // relative path to project root
+ val updatedZipEntryPath = zipEntryPath.subpath(1, zipEntryPath.nameCount).toString()
+ session.generatedTestDiffs[updatedZipEntryPath] = fileContent.toString(Charsets.UTF_8)
+
+ zipInputStream.closeEntry()
+ zipEntry = zipInputStream.nextEntry
+ }
+ }
+ } catch (e: IOException) {
+ LOG.debug(e) { "Error reading ZIP entries" }
+ throw e
+ }
+ }
+
+ private suspend fun createUploadUrl(
+ codeTestChatHelper: CodeTestChatHelper,
+ previousIterationContext: PreviousUTGIterationContext?,
+ ): CodeTestResponseContext {
+ throwIfCancelled(codeTestChatHelper.getActiveSession())
+ val file =
+ if (previousIterationContext == null) {
+ FileEditorManager.getInstance(project).selectedEditor?.file.also {
+ codeTestChatHelper.getActiveSession().selectedFile = it
+ }
+ } else {
+ previousIterationContext.selectedFile
+ }
+
+ val combinedBuildAndExecuteLogFile = combineBuildAndExecuteLogFiles(
+ previousIterationContext?.buildLogFile,
+ previousIterationContext?.testLogFile
+ )
+ val codeTestSessionConfig = CodeTestSessionConfig(file, project, combinedBuildAndExecuteLogFile)
+ codeTestChatHelper.getActiveSession().projectRoot = codeTestSessionConfig.projectRoot.path
+
+ val codeTestSessionContext = CodeTestSessionContext(project, codeTestSessionConfig)
+ val codeWhispererCodeTestSession = CodeWhispererCodeTestSession(codeTestSessionContext)
+ return codeWhispererCodeTestSession.run(codeTestChatHelper, previousIterationContext)
+ }
+
+ private fun startTestGeneration(uploadId: String, targetCode: List, userInput: String): StartTestGenerationResponse =
+ CodeWhispererClientAdaptor.getInstance(project).startTestGeneration(uploadId, targetCode, userInput)
+
+ private fun getTestGenerationStatus(jobId: String, jobGroupName: String): GetTestGenerationResponse =
+ CodeWhispererClientAdaptor.getInstance(project).getTestGeneration(jobId, jobGroupName)
+
+ /**
+ * Returns true if the UTG is in progress.
+ * This function will return true for a cancelled UTG job which is in cancellation state.
+ */
+ fun isUTGInProgress(): Boolean = isUTGInProgress.get()
+
+ private fun beforeTestGenFlow(session: Session) {
+ resetTestGenFlowSession(session)
+ session.isGeneratingTests = true
+ isUTGInProgress.set(true)
+ // Show in progress indicator
+
+ ApplicationManager.getApplication().invokeLater {
+ (FileDocumentManager.getInstance() as FileDocumentManagerImpl).saveAllDocuments(false)
+ }
+ }
+
+ private fun resetTestGenFlowSession(session: Session) {
+ // session.selectedFile doesn't need to be reset since it will remain unchanged
+ session.conversationState = ConversationState.IN_PROGRESS
+ session.shortAnswer = ShortAnswer()
+ session.openedDiffFile = null
+ session.testFileRelativePathToProjectRoot = ""
+ session.testFileName = ""
+ session.openedDiffFile = null
+ session.generatedTestDiffs.clear()
+ session.buildAndExecuteTaskContext.apply {
+ buildExitCode = -1
+ testExitCode = -1
+ progressStatus = BuildAndExecuteProgressStatus.START_STEP
+ }
+ }
+
+ private fun afterTestGenFlow() {
+ isUTGInProgress.set(false)
+ }
+
+ /**
+ * Triggers a unit test generation flow based on current open file.
+ */
+ fun generateTests(
+ prompt: String,
+ codeTestChatHelper: CodeTestChatHelper,
+ previousIterationContext: PreviousUTGIterationContext?,
+ selectionRange: Range?,
+ ): Job? {
+ val shouldStart = performTestGenPreChecks()
+ val session = codeTestChatHelper.getActiveSession()
+ if (!shouldStart) {
+ session.conversationState = ConversationState.IDLE
+ return null
+ }
+
+ beforeTestGenFlow(session)
+
+ return cs.launch {
+ try {
+ launchTestGenFlow(prompt, codeTestChatHelper, previousIterationContext, selectionRange)
+ } catch (e: Exception) {
+ // Add an answer for displaying error message
+ var errorMessage = e.message
+ if (e is JsonParseException) {
+ errorMessage = message("testgen.error.generic_error_message")
+ }
+
+ if (e is CodeWhispererRuntimeException) {
+ errorMessage = message("testgen.error.maximum_generations_reach")
+ }
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = errorMessage,
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ )
+ )
+
+ AmazonqTelemetry.utgGenerateTests(
+ cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
+ hasUserPromptSupplied = session.hasUserPromptSupplied,
+ isSupportedLanguage = true,
+ jobGroup = session.testGenerationJobGroupName,
+ jobId = session.testGenerationJob,
+ result = if (e.message == message("testgen.message.cancelled")) MetricResult.Cancelled else MetricResult.Failed,
+ reason = e.javaClass.name,
+ reasonDesc = e.message,
+ perfClientLatency = (Instant.now().toEpochMilli() - session.startTimeOfTestGeneration),
+ isCodeBlockSelected = session.isCodeBlockSelected,
+ artifactsUploadDuration = session.artifactUploadDuration,
+ buildPayloadBytes = session.srcPayloadSize,
+ buildZipFileBytes = session.srcZipFileSize
+ )
+ session.isGeneratingTests = false
+ } finally {
+ // Reset the flow if there is any error
+ if (!session.isGeneratingTests) {
+ codeTestChatHelper.updateUI(
+ promptInputProgress = cancellingProgressField
+ )
+ delay(1000)
+ codeTestChatHelper.sendUpdatePromptProgress(session.tabId, null)
+ codeTestChatHelper.deleteSession(session.tabId)
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = false,
+ promptInputPlaceholder = message("testgen.placeholder.newtab"),
+ )
+ }
+ session.isGeneratingTests = false
+ session.conversationState = ConversationState.IDLE
+ afterTestGenFlow()
+ // send message displaying card
+ }
+ }
+ }
+
+ private fun performTestGenPreChecks(): Boolean {
+ if (!isQConnected(project)) return false
+ if (isUTGInProgress()) return false
+ val connectionExpired = promptReAuth(project)
+ if (connectionExpired) return false
+ return true
+ }
+
+ companion object {
+ fun getInstance(project: Project) = project.service()
+ private val LOG = getLogger()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/InboundAppMessagesHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/InboundAppMessagesHandler.kt
new file mode 100644
index 0000000000..1c41befeb9
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/InboundAppMessagesHandler.kt
@@ -0,0 +1,24 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.IncomingCodeTestMessage
+
+interface InboundAppMessagesHandler {
+ suspend fun processPromptChatMessage(message: IncomingCodeTestMessage.ChatPrompt)
+
+ suspend fun processStartTestGen(message: IncomingCodeTestMessage.StartTestGen)
+
+ suspend fun processLinkClick(message: IncomingCodeTestMessage.ClickedLink)
+
+ suspend fun processNewTabCreatedMessage(message: IncomingCodeTestMessage.NewTabCreated)
+
+ suspend fun processClearQuickAction(message: IncomingCodeTestMessage.ClearChat)
+
+ suspend fun processHelpQuickAction(message: IncomingCodeTestMessage.Help)
+
+ suspend fun processTabRemovedMessage(message: IncomingCodeTestMessage.TabRemoved)
+
+ suspend fun processButtonClickedMessage(message: IncomingCodeTestMessage.ButtonClicked)
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/auth/CodeTestAuthUtils.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/auth/CodeTestAuthUtils.kt
new file mode 100644
index 0000000000..bd8161d53a
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/auth/CodeTestAuthUtils.kt
@@ -0,0 +1,11 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth
+
+import com.intellij.openapi.project.Project
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnection
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity
+
+fun isCodeTestAvailable(project: Project): Boolean = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q) is ActiveConnection.ValidBearer
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt
new file mode 100644
index 0000000000..69e41fa6c8
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt
@@ -0,0 +1,1252 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.controller
+
+import com.intellij.diff.DiffContentFactory
+import com.intellij.diff.DiffManager
+import com.intellij.diff.DiffManagerEx
+import com.intellij.diff.requests.SimpleDiffRequest
+import com.intellij.ide.BrowserUtil
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.future.await
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
+import software.amazon.awssdk.services.codewhispererruntime.model.Position
+import software.amazon.awssdk.services.codewhispererruntime.model.Range
+import software.amazon.awssdk.services.codewhispererruntime.model.Reference
+import software.amazon.awssdk.services.codewhispererruntime.model.Span
+import software.amazon.awssdk.services.codewhispererstreaming.CodeWhispererStreamingAsyncClient
+import software.amazon.awssdk.services.codewhispererstreaming.model.AssistantResponseEvent
+import software.amazon.awssdk.services.codewhispererstreaming.model.ChatMessage
+import software.amazon.awssdk.services.codewhispererstreaming.model.ChatResponseStream
+import software.amazon.awssdk.services.codewhispererstreaming.model.ChatTriggerType
+import software.amazon.awssdk.services.codewhispererstreaming.model.CursorState
+import software.amazon.awssdk.services.codewhispererstreaming.model.DocumentSymbol
+import software.amazon.awssdk.services.codewhispererstreaming.model.EditorState
+import software.amazon.awssdk.services.codewhispererstreaming.model.GenerateAssistantResponseRequest
+import software.amazon.awssdk.services.codewhispererstreaming.model.GenerateAssistantResponseResponseHandler
+import software.amazon.awssdk.services.codewhispererstreaming.model.ProgrammingLanguage
+import software.amazon.awssdk.services.codewhispererstreaming.model.RelevantTextDocument
+import software.amazon.awssdk.services.codewhispererstreaming.model.SymbolType
+import software.amazon.awssdk.services.codewhispererstreaming.model.TextDocument
+import software.amazon.awssdk.services.codewhispererstreaming.model.UserInputMessage
+import software.amazon.awssdk.services.codewhispererstreaming.model.UserInputMessageContext
+import software.amazon.awssdk.services.codewhispererstreaming.model.UserIntent
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.core.AwsClientManager
+import software.aws.toolkits.jetbrains.core.coroutines.EDT
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
+import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.CodeWhispererUTGChatManager
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.ConversationState
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.FEATURE_NAME
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.InboundAppMessagesHandler
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.Button
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestChatMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestChatMessageContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.IncomingCodeTestMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.PreviousUTGIterationContext
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.BuildAndExecuteProgressStatus
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.Session
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.storage.ChatSessionStorage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.utils.constructBuildAndExecutionSummaryText
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.utils.runBuildOrTestCommand
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAuthNeededException
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
+import software.aws.toolkits.jetbrains.services.cwc.ChatConstants
+import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData
+import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType
+import software.aws.toolkits.jetbrains.services.cwc.clients.chat.v1.ChatSessionV1.Companion.validLanguages
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticPrompt
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticTextResponse
+import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContext
+import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor
+import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType
+import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.FileContext
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import software.aws.toolkits.resources.message
+import software.aws.toolkits.telemetry.AmazonqTelemetry
+import software.aws.toolkits.telemetry.MetricResult
+import software.aws.toolkits.telemetry.UiTelemetry
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.time.Instant
+import java.util.UUID
+import software.amazon.awssdk.services.codewhispererstreaming.model.Position as StreamingPosition
+import software.amazon.awssdk.services.codewhispererstreaming.model.Range as StreamingRange
+
+class CodeTestChatController(
+ private val context: AmazonQAppInitContext,
+ private val chatSessionStorage: ChatSessionStorage,
+ private val authController: AuthController = AuthController(),
+ private val cs: CoroutineScope,
+) : InboundAppMessagesHandler {
+ val messenger = context.messagesFromAppToUi
+ private val codeTestChatHelper = CodeTestChatHelper(context.messagesFromAppToUi, chatSessionStorage)
+ private val supportedLanguage = setOf("python", "java")
+ val client = CodeWhispererClientAdaptor.getInstance(context.project)
+ override suspend fun processPromptChatMessage(message: IncomingCodeTestMessage.ChatPrompt) {
+ handleChat(tabId = message.tabId, message = message.chatMessage)
+ }
+
+ private fun isLanguageSupported(languageId: String): Boolean =
+ supportedLanguage.contains(languageId.lowercase())
+
+ private fun getEditorSelectionRange(project: Project): Range? {
+ var selectionRange: Range? = null
+
+ ApplicationManager.getApplication().invokeAndWait {
+ selectionRange = ApplicationManager.getApplication().runReadAction {
+ val editor = FileEditorManager.getInstance(project).selectedTextEditor
+ editor?.let {
+ val selectionModel = it.selectionModel
+ val startOffset = selectionModel.selectionStart
+ val endOffset = selectionModel.selectionEnd
+
+ val startLogicalPosition = editor.offsetToLogicalPosition(startOffset)
+ val endLogicalPosition = editor.offsetToLogicalPosition(endOffset)
+
+ if (startOffset != endOffset) {
+ val start = Position.builder()
+ .line(startLogicalPosition.line)
+ .character(startLogicalPosition.column)
+ .build()
+
+ val end = Position.builder()
+ .line(endLogicalPosition.line)
+ .character(endLogicalPosition.column)
+ .build()
+
+ Range.builder()
+ .start(start)
+ .end(end)
+ .build()
+ } else {
+ null
+ }
+ }
+ }
+ }
+
+ return selectionRange
+ }
+
+ override suspend fun processStartTestGen(message: IncomingCodeTestMessage.StartTestGen) {
+ codeTestChatHelper.setActiveCodeTestTabId(message.tabId)
+ val session = codeTestChatHelper.getActiveSession()
+ // check if IDE has active file open, yes return (fileName and filePath) else return null
+ val project = context.project
+ val fileInfo = checkActiveFileInIDE(project, message) ?: return
+ session.programmingLanguage = fileInfo.fileLanguage
+ if (session.isGeneratingTests === true) {
+ return
+ }
+ session.startTimeOfTestGeneration = Instant.now().toEpochMilli().toDouble()
+ session.isGeneratingTests = true
+
+ var requestId: String = ""
+ var statusCode: Int = 0
+ var conversationId: String? = null
+ var testResponseMessageId: String? = null
+ var testResponseText: String = ""
+
+ val userMessage = when {
+ message.prompt != "" -> {
+ "/test ${message.prompt}"
+ }
+ else -> "/test Generate unit tests for `${fileInfo.fileName}`"
+ }
+ session.hasUserPromptSupplied = message.prompt.isNotEmpty()
+
+ // Send user prompt to chat
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(message = userMessage, type = ChatMessageType.Prompt, canBeVoted = false),
+ message.tabId,
+ false
+ )
+ if (isLanguageSupported(fileInfo.fileLanguage.languageId)) {
+ // Send Capability card to chat
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(informationCard = true, message = null, type = ChatMessageType.Answer, canBeVoted = false),
+ message.tabId,
+ false
+ )
+
+ var selectionRange = getEditorSelectionRange(project)
+
+ session.isCodeBlockSelected = selectionRange !== null
+
+ // This check is added to remove /test if user accidentally added while doing Regenerate unit tests.
+ val userPrompt = if (message.prompt.startsWith("/test")) {
+ message.prompt.substringAfter("/test ").trim()
+ } else {
+ message.prompt
+ }
+ CodeWhispererUTGChatManager.getInstance(project).generateTests(userPrompt, codeTestChatHelper, null, selectionRange)
+ } else {
+ // Not adding a progress bar to unsupported language cases
+ val responseHandler = GenerateAssistantResponseResponseHandler.builder()
+ .onResponse {
+ requestId = it.responseMetadata().requestId()
+ statusCode = it.sdkHttpResponse().statusCode()
+ conversationId = it.conversationId()
+ }
+ .subscriber { stream: ChatResponseStream ->
+ stream.accept(object : GenerateAssistantResponseResponseHandler.Visitor {
+
+ override fun visitAssistantResponseEvent(event: AssistantResponseEvent) {
+ testResponseText += event.content()
+ cs.launch {
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(
+ message = testResponseText,
+ type = ChatMessageType.AnswerPart
+ ),
+ messageIdOverride = testResponseMessageId
+ )
+ }
+ }
+ })
+ }
+ .build()
+
+ val messageContent = "⚠ ${fileInfo.fileLanguage.languageId} is not a " +
+ "language I support specialized unit test generation for at the moment. The languages " +
+ "I support now are Python and Java. I can still provide examples, instructions and code suggestions."
+
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(
+ message = messageContent,
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ ),
+ message.tabId,
+ false
+ )
+ testResponseMessageId = codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = "",
+ type = ChatMessageType.AnswerStream
+ )
+ )
+ codeTestChatHelper.updateUI(
+ loadingChat = true,
+ promptInputDisabledState = true,
+ )
+ // Send Request to Sync UTG API
+ val connection = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
+ // this should never happen because it should have been handled upstream by [AuthController]
+ ?: error("connection was found to be null")
+ val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project)
+ val activeFileContext = ActiveFileContext(
+ fileContext = FileContext(
+ fileLanguage = fileInfo.fileLanguage.languageId,
+ filePath = fileInfo.filePath,
+ matchPolicy = null
+ ),
+ focusAreaContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage).focusAreaContext,
+ )
+
+ val requestData = ChatRequestData(
+ tabId = session.tabId,
+ message = "Generate unit tests for the following part of my code: ${message.prompt}",
+ activeFileContext = activeFileContext,
+ userIntent = UserIntent.GENERATE_UNIT_TESTS,
+ triggerType = TriggerType.ContextMenu,
+ customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(context.project),
+ relevantTextDocuments = emptyList(),
+ useRelevantDocuments = false,
+ )
+
+ val client = AwsClientManager.getInstance().getClient(connection.getConnectionSettings())
+ val request = requestData.toChatRequest()
+ client.generateAssistantResponse(request, responseHandler).await()
+ // TODO: Need to send isCodeBlockSelected field
+ AmazonqTelemetry.utgGenerateTests(
+ cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
+ hasUserPromptSupplied = session.hasUserPromptSupplied,
+ isSupportedLanguage = false,
+ result = MetricResult.Succeeded,
+ perfClientLatency = (Instant.now().toEpochMilli() - session.startTimeOfTestGeneration)
+ )
+ session.isGeneratingTests = false
+ codeTestChatHelper.updateUI(
+ loadingChat = false,
+ promptInputDisabledState = false
+ )
+ }
+ }
+ private fun ActiveFileContext.toEditorState(relevantDocuments: List, useRelevantDocuments: Boolean): EditorState {
+ val editorStateBuilder = EditorState.builder()
+ if (fileContext != null) {
+ val cursorStateBuilder = CursorState.builder()
+ // Cursor State
+ val start = focusAreaContext?.codeSelectionRange?.start
+ val end = focusAreaContext?.codeSelectionRange?.end
+
+ if (start != null && end != null) {
+ cursorStateBuilder.range(
+ StreamingRange.builder()
+ .start(
+ StreamingPosition.builder()
+ .line(start.row)
+ .character(start.column)
+ .build(),
+ )
+ .end(
+ StreamingPosition.builder()
+ .line(end.row)
+ .character(end.column)
+ .build(),
+ ).build(),
+ )
+ }
+ editorStateBuilder.cursorState(cursorStateBuilder.build())
+
+ // Code Names -> DocumentSymbols
+ val documentBuilder = TextDocument.builder()
+ val codeNames = focusAreaContext?.codeNames
+
+ val documentSymbolList = codeNames?.fullyQualifiedNames?.used?.map {
+ DocumentSymbol.builder()
+ .name(it.symbol?.joinToString(separator = "."))
+ .type(SymbolType.USAGE)
+ .source(it.source?.joinToString(separator = "."))
+ .build()
+ }?.filter { it.name().length in ChatConstants.FQN_SIZE_MIN until ChatConstants.FQN_SIZE_LIMIT }.orEmpty()
+ documentBuilder.documentSymbols(documentSymbolList)
+ // TODO: Do conditional check for focusAreaContext?.codeSelectionRange if undefined then get entire file
+ // File Text
+ val fileContent = Files.readString(Paths.get(fileContext.filePath))
+ documentBuilder.text(fileContent)
+
+ // Programming Language
+ val programmingLanguage = fileContext.fileLanguage
+ if (programmingLanguage != null && validLanguages.contains(programmingLanguage)) {
+ documentBuilder.programmingLanguage(
+ ProgrammingLanguage.builder()
+ .languageName(programmingLanguage).build(),
+ )
+ }
+
+ // Relative File Path
+ val filePath = fileContext.filePath
+ if (filePath != null) {
+ documentBuilder.relativeFilePath(filePath.take(ChatConstants.FILE_PATH_SIZE_LIMIT))
+ }
+ editorStateBuilder.document(documentBuilder.build())
+ }
+
+ // Relevant Documents
+ val documents: List = relevantDocuments.map { doc ->
+ RelevantTextDocument.builder().text(doc.text).relativeFilePath(doc.relativeFilePath.take(ChatConstants.FILE_PATH_SIZE_LIMIT)).build()
+ }
+
+ editorStateBuilder.relevantDocuments(documents)
+ editorStateBuilder.useRelevantDocuments(useRelevantDocuments)
+ return editorStateBuilder.build()
+ }
+
+ private fun ChatRequestData.toChatRequest(): GenerateAssistantResponseRequest {
+ val userInputMessageContextBuilder = UserInputMessageContext.builder()
+ userInputMessageContextBuilder.editorState(activeFileContext.toEditorState(relevantTextDocuments, useRelevantDocuments))
+ val userInputMessageContext = userInputMessageContextBuilder.build() //
+ val userInput = UserInputMessage.builder()
+ .content(message.take(ChatConstants.CUSTOMER_MESSAGE_SIZE_LIMIT))
+ .userInputMessageContext(userInputMessageContext)
+ .userIntent(userIntent)
+ .build()
+ println("UserInput Message: $userInput")
+ val conversationState = software.amazon.awssdk.services.codewhispererstreaming.model.ConversationState.builder()
+ .currentMessage(ChatMessage.fromUserInputMessage(userInput))
+ .chatTriggerType(if (triggerType == TriggerType.Inline) ChatTriggerType.INLINE_CHAT else ChatTriggerType.MANUAL)
+ .customizationArn(customization?.arn)
+ .build()
+ return GenerateAssistantResponseRequest.builder()
+ .conversationState(conversationState)
+ .build()
+ }
+
+ override suspend fun processNewTabCreatedMessage(message: IncomingCodeTestMessage.NewTabCreated) {
+ newTabOpened(message.tabId)
+ LOG.debug { "$FEATURE_NAME: New tab created: $message" }
+ codeTestChatHelper.setActiveCodeTestTabId(message.tabId)
+ }
+
+ override suspend fun processTabRemovedMessage(message: IncomingCodeTestMessage.TabRemoved) {
+ chatSessionStorage.deleteSession(message.tabId)
+ }
+
+ override suspend fun processClearQuickAction(message: IncomingCodeTestMessage.ClearChat) {
+ chatSessionStorage.deleteSession(message.tabId)
+ }
+
+ override suspend fun processHelpQuickAction(message: IncomingCodeTestMessage.Help) {
+ // TODO: Replace StaticPrompt and StaticTextResponse message according to Fnf
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(
+ message = StaticPrompt.Help.message,
+ type = ChatMessageType.Prompt,
+ canBeVoted = false
+ ),
+ message.tabId,
+ false
+ )
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(
+ message = StaticTextResponse.Help.message,
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ ),
+ message.tabId,
+ false
+ )
+ }
+
+ override suspend fun processLinkClick(message: IncomingCodeTestMessage.ClickedLink) {
+ BrowserUtil.browse(message.link)
+ }
+
+ override suspend fun processButtonClickedMessage(message: IncomingCodeTestMessage.ButtonClicked) {
+ val session = codeTestChatHelper.getActiveSession()
+ var numberOfLinesGenerated = 0
+ var numberOfLinesSelected = 0
+ var lineDifference = 0
+ var numberOfCharsGenerated = 0
+ var numberOfCharsSelected = 0
+ var charDifference = 0
+ var generatedFileContent = ""
+ var selectedFileContent = ""
+ var latencyOfTestGeneration = 0.0
+
+ when (message.actionID) {
+ "utg_view_diff" -> {
+ withContext(EDT) {
+ (DiffManager.getInstance() as DiffManagerEx).showDiffBuiltin(
+ context.project,
+ SimpleDiffRequest(
+ session.testFileName,
+ DiffContentFactory.getInstance().create(
+ getFileContentAtTestFilePath(
+ session.projectRoot,
+ session.testFileRelativePathToProjectRoot
+ )
+ ),
+ DiffContentFactory.getInstance().create(session.generatedTestDiffs.values.first()),
+ "Before",
+ "After"
+ )
+ )
+ session.openedDiffFile = FileEditorManager.getInstance(context.project).selectedEditor?.file
+ ApplicationManager.getApplication().runReadAction {
+ generatedFileContent = getFileContentAtTestFilePath(
+ session.projectRoot,
+ session.testFileRelativePathToProjectRoot
+ )
+ val selectedFile = FileEditorManager.getInstance(context.project).selectedEditor?.file
+ selectedFileContent = selectedFile?.let {
+ FileDocumentManager.getInstance().getDocument(it)?.text
+ }.orEmpty()
+ }
+
+ // Line difference calculation: linesOfCodeGenerated = number of lines in generated test file - number of lines in original test file
+ numberOfLinesGenerated = generatedFileContent.lines().size
+ numberOfLinesSelected = selectedFileContent.lines().size
+ lineDifference = numberOfLinesGenerated - numberOfLinesSelected
+
+ // Character difference calculation: charsOfCodeGenerated = number of characters in generated test file - number of characters in original test file
+ numberOfCharsGenerated = generatedFileContent.length
+ numberOfCharsSelected = selectedFileContent.length
+ charDifference = numberOfCharsGenerated - numberOfCharsSelected
+
+ session.linesOfCodeGenerated = lineDifference.coerceAtLeast(0)
+ session.charsOfCodeGenerated = charDifference.coerceAtLeast(0)
+ latencyOfTestGeneration = (Instant.now().toEpochMilli() - session.startTimeOfTestGeneration)
+ UiTelemetry.click(null as Project?, "unitTestGeneration_viewDiff")
+
+ val buttonList = mutableListOf()
+ buttonList.add(
+ Button(
+ "utg_reject",
+ "Reject",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "error",
+ ),
+ )
+ /*
+ // TODO: for unit test regeneration loop
+ if (session.iteration < 2) {
+ buttonList.add(
+ Button(
+ "utg_regenerate",
+ "Regenerate",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ )
+ }
+ */
+
+ buttonList.add(
+ Button(
+ "utg_accept",
+ "Accept",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "success",
+ ),
+ )
+
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = true,
+ promptInputPlaceholder = message("testgen.placeholder.select_an_option"),
+ )
+
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(
+ type = ChatMessageType.AnswerPart,
+ buttons = buttonList,
+ ),
+ messageIdOverride = session.viewDiffMessageId
+ )
+ }
+ }
+ "utg_accept" -> {
+ // open the file at test path relative to the project root
+ val testFileAbsolutePath = Paths.get(session.projectRoot, session.testFileRelativePathToProjectRoot)
+ openOrCreateTestFileAndApplyDiff(context.project, testFileAbsolutePath, session.generatedTestDiffs.values.first(), session.openedDiffFile)
+ session.codeReferences?.let { references ->
+ LOG.debug { "Accepted unit tests with references: $references" }
+ val manager = CodeWhispererCodeReferenceManager.getInstance(context.project)
+ references.forEach { ref ->
+ var referenceContentSpan: Span? = null
+ ref.recommendationContentSpan?.let {
+ referenceContentSpan = Span.builder().start(ref.recommendationContentSpan.start).end(ref.recommendationContentSpan.end).build()
+ }
+ val reference = Reference.builder().url(
+ ref.url
+ ).licenseName(ref.licenseName).repository(ref.repository).recommendationContentSpan(referenceContentSpan).build()
+ var originalContent: String? = null
+ ref.recommendationContentSpan?.let {
+ originalContent = session.generatedTestDiffs.values.first().substring(
+ ref.recommendationContentSpan.start,
+ ref.recommendationContentSpan.end
+ )
+ }
+ LOG.debug { "Original code content from reference span: $originalContent" }
+ withContext(EDT) {
+ manager.addReferenceLogPanelEntry(reference = reference, null, null, originalContent?.split("\n"))
+ manager.toolWindow?.show()
+ }
+ }
+ }
+ val testGenerationEventResponse = client.sendTestGenerationEvent(
+ session.testGenerationJob,
+ session.testGenerationJobGroupName,
+ session.programmingLanguage,
+ session.numberOfUnitTestCasesGenerated,
+ session.numberOfUnitTestCasesGenerated,
+ session.linesOfCodeGenerated,
+ session.linesOfCodeGenerated,
+ session.charsOfCodeGenerated,
+ session.charsOfCodeGenerated
+ )
+ LOG.debug {
+ "Successfully sent test generation telemetry. RequestId: ${
+ testGenerationEventResponse.responseMetadata().requestId()}"
+ }
+
+ UiTelemetry.click(null as Project?, "unitTestGeneration_acceptDiff")
+
+ AmazonqTelemetry.utgGenerateTests(
+ cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
+ hasUserPromptSupplied = session.hasUserPromptSupplied,
+ isSupportedLanguage = true,
+ jobGroup = session.testGenerationJobGroupName,
+ jobId = session.testGenerationJob,
+ acceptedCount = session.numberOfUnitTestCasesGenerated?.toLong(),
+ generatedCount = session.numberOfUnitTestCasesGenerated?.toLong(),
+ acceptedLinesCount = session.linesOfCodeGenerated?.toLong(),
+ generatedLinesCount = session.linesOfCodeGenerated?.toLong(),
+ acceptedCharactersCount = session.charsOfCodeGenerated?.toLong(),
+ generatedCharactersCount = session.charsOfCodeGenerated?.toLong(),
+ result = MetricResult.Succeeded,
+ perfClientLatency = latencyOfTestGeneration,
+ isCodeBlockSelected = session.isCodeBlockSelected,
+ artifactsUploadDuration = session.artifactUploadDuration,
+ buildPayloadBytes = session.srcPayloadSize,
+ buildZipFileBytes = session.srcZipFileSize
+ )
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = message("testgen.message.success"),
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ )
+ )
+ sessionCleanUp(session.tabId)
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = false,
+ promptInputPlaceholder = message("testgen.placeholder.waiting_on_your_inputs"),
+ )
+ /*
+ val taskContext = session.buildAndExecuteTaskContext
+ if (session.iteration < 2) {
+ taskContext.buildCommand = getBuildCommand(message.tabId)
+ taskContext.executionCommand = getExecutionCommand(message.tabId)
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = """
+ Would you like me to help build and execute the test? I'll run following commands
+
+ ```sh
+ ${taskContext.buildCommand}
+ ${taskContext.executionCommand}
+ ```
+ """.trimIndent(),
+ type = ChatMessageType.Answer,
+ canBeVoted = true,
+ buttons = listOf(
+ Button(
+ "utg_skip_and_finish",
+ "Skip and finish",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ Button(
+ "utg_modify_command",
+ "Modify commands",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ Button(
+ "utg_build_and_execute",
+ "Build and execute",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ )
+ )
+ )
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = true,
+ )
+ } else if (session.iteration < 4) {
+ // Already built and executed once, display # of iterations left message
+ val remainingIterationsCount = UTG_CHAT_MAX_ITERATION - session.iteration
+ val iterationCountString = "$remainingIterationsCount ${if (remainingIterationsCount > 1) "iterations" else "iteration"}"
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = """
+ Would you like Amazon Q to build and execute again, and fix errors?
+
+ You have $iterationCountString left.
+
+ """.trimIndent(),
+ type = ChatMessageType.AIPrompt,
+ buttons = listOf(
+ Button(
+ "utg_skip_and_finish",
+ "Skip and finish",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ Button(
+ "utg_proceed",
+ "Proceed",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ ),
+ )
+ )
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = true,
+ )
+ } else {
+ // TODO: change this hardcoded string
+ val monthlyLimitString = "25 out of 30"
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = """
+ You have gone through all three iterations and this unit test generation workflow is complete. You have $monthlyLimitString Amazon Q Developer Agent invocations left this month.
+ """.trimIndent(),
+ type = ChatMessageType.Answer,
+ )
+ )
+ codeTestChatHelper.updateUI(
+ promptInputPlaceholder = message("testgen.placeholder.newtab")
+ )
+ }
+ */
+ }
+ /*
+ //TODO: this is for unit test regeneration build iteration loop
+ "utg_regenerate" -> {
+ // close the existing open diff in the editor.
+ ApplicationManager.getApplication().invokeLater {
+ session.openedDiffFile?.let { FileEditorManager.getInstance(context.project).closeFile(it) }
+ }
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = message("testgen.message.regenerate_input"),
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ )
+ )
+ val testGenerationEventResponse = client.sendTestGenerationEvent(
+ session.testGenerationJob,
+ session.testGenerationJobGroupName,
+ session.programmingLanguage,
+ session.numberOfUnitTestCasesGenerated,
+ 0,
+ session.linesOfCodeGenerated,
+ 0,
+ session.charsOfCodeGenerated,
+ 0
+ )
+ LOG.debug {
+ "Successfully sent test generation telemetry. RequestId: ${
+ testGenerationEventResponse.responseMetadata().requestId()}"
+ }
+ sessionCleanUp(session.tabId)
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = false,
+ promptInputPlaceholder = message("testgen.placeholder.waiting_on_your_inputs"),
+ )
+ }
+ */
+
+ "utg_reject" -> {
+ ApplicationManager.getApplication().invokeLater {
+ session.openedDiffFile?.let { FileEditorManager.getInstance(context.project).closeFile(it) }
+ }
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = message("testgen.message.success"),
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ )
+ )
+ val testGenerationEventResponse = client.sendTestGenerationEvent(
+ session.testGenerationJob,
+ session.testGenerationJobGroupName,
+ session.programmingLanguage,
+ session.numberOfUnitTestCasesGenerated,
+ 0,
+ session.linesOfCodeGenerated,
+ 0,
+ session.charsOfCodeGenerated,
+ 0
+ )
+ LOG.debug {
+ "Successfully sent test generation telemetry. RequestId: ${
+ testGenerationEventResponse.responseMetadata().requestId()}"
+ }
+
+ UiTelemetry.click(null as Project?, "unitTestGeneration_rejectDiff")
+ AmazonqTelemetry.utgGenerateTests(
+ cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
+ hasUserPromptSupplied = session.hasUserPromptSupplied,
+ isSupportedLanguage = true,
+ jobGroup = session.testGenerationJobGroupName,
+ jobId = session.testGenerationJob,
+ acceptedCount = 0,
+ generatedCount = session.numberOfUnitTestCasesGenerated?.toLong(),
+ acceptedLinesCount = 0,
+ generatedLinesCount = session.linesOfCodeGenerated?.toLong(),
+ acceptedCharactersCount = 0,
+ generatedCharactersCount = session.charsOfCodeGenerated?.toLong(),
+ result = MetricResult.Succeeded,
+ perfClientLatency = latencyOfTestGeneration,
+ isCodeBlockSelected = session.isCodeBlockSelected,
+ artifactsUploadDuration = session.artifactUploadDuration,
+ buildPayloadBytes = session.srcPayloadSize,
+ buildZipFileBytes = session.srcZipFileSize
+ )
+ sessionCleanUp(message.tabId)
+ }
+ "utg_skip_and_finish" -> {
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = message("testgen.message.success"),
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ )
+ )
+ sessionCleanUp(message.tabId)
+ }
+ "utg_proceed", "utg_build_and_execute" -> {
+ // handle both "Proceed" and "Build and execute" button clicks since their actions are similar
+ // TODO: show install dependencies card if needed
+ session.conversationState = ConversationState.IN_PROGRESS
+
+ // display build in progress card
+ val taskContext = session.buildAndExecuteTaskContext
+
+ taskContext.progressStatus = BuildAndExecuteProgressStatus.RUN_BUILD
+ val messageId = updateBuildAndExecuteProgressCard(taskContext.progressStatus, null, session.iteration)
+ // TODO: build and execute case
+ val buildLogsFile = VirtualFileManager.getInstance().findFileByNioPath(
+ withContext(currentCoroutineContext()) {
+ Files.createTempFile(null, null)
+ }
+ )
+ if (buildLogsFile == null) {
+ // TODO: handle no log file case
+ return
+ }
+ LOG.debug {
+ "Q TestGen session: ${codeTestChatHelper.getActiveCodeTestTabId()}: " +
+ "tmpFile for build logs:\n ${buildLogsFile.path}"
+ }
+
+ runBuildOrTestCommand(taskContext.buildCommand, buildLogsFile, context.project, isBuildCommand = true, taskContext)
+ while (taskContext.buildExitCode < 0) {
+ // wait until build command finished
+ delay(1000)
+ }
+
+ // TODO: only go to future iterations when buildExitCode or testExitCode > 0, right now iterate regardless
+ if (taskContext.buildExitCode > 0) {
+ // TODO: handle build failure case
+ // ...
+// return
+ }
+ taskContext.progressStatus = BuildAndExecuteProgressStatus.RUN_EXECUTION_TESTS
+ updateBuildAndExecuteProgressCard(taskContext.progressStatus, messageId, session.iteration)
+
+ val testLogsFile = VirtualFileManager.getInstance().findFileByNioPath(
+ withContext(currentCoroutineContext()) {
+ Files.createTempFile(null, null)
+ }
+ )
+ if (testLogsFile == null) {
+ // TODO: handle no log file case
+ return
+ }
+ LOG.debug {
+ "Q TestGen session: ${codeTestChatHelper.getActiveCodeTestTabId()}: " +
+ "tmpFile for test logs:\n ${buildLogsFile.path}"
+ }
+ delay(1000)
+ runBuildOrTestCommand(taskContext.executionCommand, testLogsFile, context.project, isBuildCommand = false, taskContext)
+ while (taskContext.testExitCode < 0) {
+ // wait until test command finished
+ delay(1000)
+ }
+
+ if (taskContext.testExitCode == 0) {
+ taskContext.progressStatus = BuildAndExecuteProgressStatus.TESTS_EXECUTED
+ updateBuildAndExecuteProgressCard(taskContext.progressStatus, messageId, session.iteration)
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = message("testgen.message.success"),
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ )
+ )
+ sessionCleanUp(message.tabId)
+ return
+ }
+
+ // has test failure, we will zip the latest project and invoke backend again
+ taskContext.progressStatus = BuildAndExecuteProgressStatus.FIXING_TEST_CASES
+ val buildAndExecuteMessageId = updateBuildAndExecuteProgressCard(taskContext.progressStatus, messageId, session.iteration)
+
+ val previousUTGIterationContext = PreviousUTGIterationContext(
+ buildLogFile = buildLogsFile,
+ testLogFile = testLogsFile,
+ selectedFile = session.selectedFile,
+ buildAndExecuteMessageId = buildAndExecuteMessageId
+ )
+
+ val job = CodeWhispererUTGChatManager.getInstance(context.project).generateTests("", codeTestChatHelper, previousUTGIterationContext, null)
+ job?.join()
+
+ taskContext.progressStatus = BuildAndExecuteProgressStatus.PROCESS_TEST_RESULTS
+ // session.iteration already updated in generateTests
+ updateBuildAndExecuteProgressCard(taskContext.progressStatus, messageId, session.iteration - 1)
+ }
+ "utg_modify_command" -> {
+ // TODO allow user input to modify the command
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = """
+ Sure. Let me know which command you'd like to modify or you could also provide all command lines you'd like me to run.
+
+ """.trimIndent(),
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ )
+ )
+ session.conversationState = ConversationState.WAITING_FOR_BUILD_COMMAND_INPUT
+ }
+ "utg_install_and_continue" -> {
+ // TODO: install dependencies and build
+ }
+ "stop_test_generation" -> {
+ UiTelemetry.click(null as Project?, "unitTestGeneration_cancelTestGenerationProgress")
+ session.isGeneratingTests = false
+ sessionCleanUp(message.tabId)
+ return
+ }
+ else -> {
+ // Handle other cases or do nothing
+ }
+ }
+ }
+
+ private suspend fun updateBuildAndExecuteProgressCard(
+ currentStatus: BuildAndExecuteProgressStatus,
+ messageId: String?,
+ iterationNum: Int,
+ ): String? {
+ val updatedText = constructBuildAndExecutionSummaryText(currentStatus, iterationNum)
+
+ if (currentStatus == BuildAndExecuteProgressStatus.RUN_BUILD) {
+ val buildAndExecuteMessageId = codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = updatedText,
+ type = ChatMessageType.AnswerStream,
+ canBeVoted = true,
+ )
+ )
+ // For streaming effect
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(type = ChatMessageType.AnswerPart),
+ messageIdOverride = buildAndExecuteMessageId
+ )
+ codeTestChatHelper.updateUI(
+ loadingChat = true,
+ promptInputDisabledState = true,
+ )
+ return buildAndExecuteMessageId
+ } else {
+ val isLastStage = currentStatus == BuildAndExecuteProgressStatus.PROCESS_TEST_RESULTS
+ codeTestChatHelper.updateAnswer(
+ CodeTestChatMessageContent(
+ message = updatedText,
+ type = if (isLastStage) ChatMessageType.Answer else ChatMessageType.AnswerPart,
+ canBeVoted = true
+ ),
+ messageId
+ )
+ codeTestChatHelper.updateUI(
+ loadingChat = !isLastStage,
+ promptInputDisabledState = true,
+ )
+ return messageId
+ }
+ }
+
+ /**
+ * Perform Session CleanUp in below cases
+ * 1. UTG success workflow or UTG build success.
+ * 2. If user click Reject or SkipAndFinish button
+ * 3. Error while generating unit tests
+ * 4. After finishing 3 build loop iterations
+ * 5. Closing a Q-Test tab
+ * 6. Progress bar cancel
+ */
+ private suspend fun sessionCleanUp(tabId: String) {
+ // TODO: May be need to clear all the session data like jobId, jobGroupName and etc along with temp build log files
+ chatSessionStorage.deleteSession(tabId)
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = false
+ )
+ codeTestChatHelper.sendUpdatePlaceholder(tabId, message("testgen.placeholder.newtab"))
+ }
+
+ private fun openOrCreateTestFileAndApplyDiff(
+ project: Project,
+ testFileAbsolutePath: Path,
+ afterContent: String,
+ openedDiffFile: VirtualFile?,
+ ) {
+ val virtualFile: VirtualFile?
+
+ // Check if the file exists
+ if (Files.exists(testFileAbsolutePath)) {
+ // File exists, get the VirtualFile
+ virtualFile = LocalFileSystem.getInstance().findFileByPath(testFileAbsolutePath.toString())
+ if (virtualFile == null) return
+ val beforeContent = String(virtualFile.contentsToByteArray()) // Read the existing content
+
+ ApplicationManager.getApplication().invokeLater {
+ ApplicationManager.getApplication().runWriteAction {
+ applyDiffAndWriteContent(virtualFile, beforeContent, afterContent)
+ }
+ }
+ } else {
+ // File does not exist, create it
+ virtualFile = createFile(testFileAbsolutePath, afterContent)
+ }
+ if (virtualFile == null) return
+ ApplicationManager.getApplication().invokeLater {
+ openedDiffFile?.let { FileEditorManager.getInstance(project).closeFile(it) }
+ FileEditorManager.getInstance(project).openFile(virtualFile, true) // Open the file in editor
+ }
+ }
+
+ // Function to create the file and write content
+ private fun createFile(path: Path, content: String): VirtualFile? {
+ val parentPath = path.parent
+ if (!Files.exists(parentPath)) {
+ Files.createDirectories(parentPath) // Ensure parent directories exist
+ }
+
+ val file = Files.createFile(path) // Create the file
+ Files.writeString(file, content) // Write the afterContent to the file
+ return LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString())
+ }
+
+ // Function to apply the diff and write the new content
+ private fun applyDiffAndWriteContent(
+ virtualFile: VirtualFile,
+ beforeContent: String,
+ afterContent: String,
+ ) {
+ if (beforeContent == afterContent) return
+ virtualFile.setBinaryContent(afterContent.toByteArray()) // Update the file content
+ }
+
+ // Return test file content if it exists, return an empty string otherwise.
+ private fun getFileContentAtTestFilePath(projectRoot: String, testFileRelativePathToProjectRoot: String): String {
+ val testFileAbsolutePath = Paths.get(projectRoot, testFileRelativePathToProjectRoot)
+ return if (Files.exists(testFileAbsolutePath)) {
+ Files.readString(testFileAbsolutePath) // Read and return the file content
+ } else {
+ "" // Return an empty string if the file does not exist
+ }
+ }
+
+ /*
+ If shortAnswer has buildCommand, use it, if it doesn't hardcode it according to the user type(internal or not)
+ private fun getBuildCommand(tabId: String): String {
+ val buildCommand = codeTestChatHelper.getSession(tabId).shortAnswer.buildCommand
+ if (buildCommand != null) return buildCommand
+
+ // TODO: remove hardcode
+ return "pip install -e ."
+ }
+
+ private fun getExecutionCommand(tabId: String): String {
+ val executionCommand = codeTestChatHelper.getSession(tabId).shortAnswer.executionCommand
+ if (executionCommand != null) return executionCommand
+
+ // TODO: remove hardcode
+ return "pytest"
+ }
+ */
+
+ private suspend fun newTabOpened(tabId: String) {
+ // TODO: the logic of checking auth is needed (for calling APIs) but need refactor with FeatureDev
+ val session: Session?
+ try {
+ session = codeTestChatHelper.getSession(tabId)
+ LOG.debug {
+ "$FEATURE_NAME:" +
+ " Session created with id: ${session.tabId}"
+ }
+ val credentialState = authController.getAuthNeededStates(context.project).amazonQ
+ if (credentialState != null) {
+ messenger.sendAuthNeededException(
+ tabId = tabId,
+ triggerId = UUID.randomUUID().toString(),
+ credentialState = credentialState,
+ )
+ session.isAuthenticating = true
+ return
+ }
+ } catch (err: Exception) {
+ messenger.publish(
+ CodeTestChatMessage(
+ tabId = tabId,
+ messageType = ChatMessageType.Answer,
+ message = message("codescan.chat.message.error_request")
+ )
+ )
+ return
+ }
+ }
+
+ data class ActiveFileInfo(
+ val filePath: String,
+ val fileName: String,
+ val fileLanguage: CodeWhispererProgrammingLanguage,
+ )
+
+ private suspend fun updateUIState() {
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = false,
+ promptInputPlaceholder = message("testgen.placeholder.newtab")
+ )
+ }
+
+ private suspend fun handleInvalidFileState(tabId: String) {
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(
+ message = message("testgen.no_file_found"),
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ ),
+ tabId,
+ false
+ )
+ sessionCleanUp(codeTestChatHelper.getActiveSession().tabId)
+ updateUIState()
+ }
+
+ private suspend fun checkActiveFileInIDE(
+ project: Project,
+ message: IncomingCodeTestMessage.StartTestGen,
+ ): ActiveFileInfo? {
+ try {
+ val fileEditorManager = FileEditorManager.getInstance(project)
+ val activeEditor = fileEditorManager.selectedEditor
+ val activeFile = fileEditorManager.selectedFiles.firstOrNull()
+
+ if (activeEditor == null || activeFile == null) {
+ handleInvalidFileState(message.tabId)
+ return null
+ }
+ val programmingLanguage: CodeWhispererProgrammingLanguage = activeFile.programmingLanguage()
+ if (programmingLanguage.languageId.equals("unknown", ignoreCase = true)) {
+ handleInvalidFileState(message.tabId)
+ return null
+ }
+ return ActiveFileInfo(
+ filePath = activeFile.path,
+ fileName = activeFile.name,
+ fileLanguage = programmingLanguage,
+ )
+ } catch (e: Exception) {
+ LOG.debug { "Error checking active file: $e" }
+ updateUIState()
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(message = e.message, type = ChatMessageType.Answer, canBeVoted = false),
+ message.tabId,
+ false
+ )
+ return null
+ }
+ }
+
+ /* UTG Tab Chat input use cases:
+ * 1. If User exits the flow and want to start a new generate unit test cycle.
+ * 2. If User clicks on Modify build command option and can enter the build command from chat input
+ * 3. If User trys to regenerate the unit tests case using Regenerate button
+ * */
+ private suspend fun handleChat(tabId: String, message: String) {
+ val session = codeTestChatHelper.getActiveSession()
+ LOG.debug {
+ "$FEATURE_NAME: " +
+ "Processing message: $message " +
+ "tabId: $tabId"
+ }
+ when (session.conversationState) {
+ ConversationState.WAITING_FOR_BUILD_COMMAND_INPUT -> handleBuildCommandInput(session, message)
+ ConversationState.WAITING_FOR_REGENERATE_INPUT -> handleRegenerateInput(session, message)
+ else -> this.processStartTestGen(
+ message = IncomingCodeTestMessage.StartTestGen(
+ tabId = session.tabId,
+ prompt = message,
+ )
+ )
+ }
+ }
+
+ private suspend fun handleRegenerateInput(session: Session, message: String) {
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message,
+ type = ChatMessageType.Prompt,
+ canBeVoted = false
+ )
+ )
+ session.conversationState = ConversationState.IDLE
+ // Start the UTG workflow with new user prompt
+ CodeWhispererUTGChatManager.getInstance(
+ context.project
+ ).generateTests(message, codeTestChatHelper, null, getEditorSelectionRange(context.project))
+ }
+
+ private suspend fun handleBuildCommandInput(session: Session, message: String) {
+ // TODO: Logic to store modified build command
+ session.conversationState = ConversationState.IDLE
+ // for now treat user's input as a single build command.
+ session.buildAndExecuteTaskContext.buildCommand = message
+ session.buildAndExecuteTaskContext.executionCommand = ""
+ codeTestChatHelper.addAnswer(
+ CodeTestChatMessageContent(
+ message = """
+ Would you like me to help build and execute the test? I'll run following commands
+
+ ```sh
+ $message
+ ```
+ """.trimIndent(),
+ type = ChatMessageType.Answer,
+ canBeVoted = true,
+ buttons = listOf(
+ Button(
+ "utg_skip_and_finish",
+ "Skip and finish task",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ Button(
+ "utg_modify_command",
+ "Modify commands",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ Button(
+ "utg_build_and_execute",
+ "Build and execute",
+ keepCardAfterClick = true,
+ position = "outside",
+ status = "info",
+ ),
+ )
+ )
+ )
+ codeTestChatHelper.updateUI(
+ promptInputDisabledState = true,
+ )
+ println(message)
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatHelper.kt
new file mode 100644
index 0000000000..d4cbe27e7d
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatHelper.kt
@@ -0,0 +1,156 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.controller
+
+import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestAddAnswerMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestChatMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestChatMessageContent
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestUpdateAnswerMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.CodeTestUpdateUIMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.ProgressField
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.PromptProgressMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.UpdatePlaceholderMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.Session
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.storage.ChatSessionStorage
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import java.util.UUID
+
+class CodeTestChatHelper(
+ private val messagePublisher: MessagePublisher,
+ private val chatSessionStorage: ChatSessionStorage,
+) {
+ private var activeCodeTestTabId: String = ""
+
+ fun setActiveCodeTestTabId(tabId: String) {
+ activeCodeTestTabId = tabId
+ }
+
+ fun getActiveCodeTestTabId(): String = activeCodeTestTabId
+
+ fun getSession(tabId: String): Session = chatSessionStorage.getSession(tabId)
+
+ fun deleteSession(tabId: String) = chatSessionStorage.deleteSession(tabId)
+
+ fun getActiveSession(): Session = chatSessionStorage.getSession(activeCodeTestTabId)
+
+ private fun isInvalidSession() = chatSessionStorage.getSession(activeCodeTestTabId).isAuthenticating
+
+ // helper for adding a brand new answer(card) to chat UI.
+ suspend fun addAnswer(
+ content: CodeTestChatMessageContent,
+ messageIdOverride: String? = null,
+ ): String? {
+ if (isInvalidSession()) return null
+
+ val messageId = messageIdOverride ?: UUID.randomUUID().toString()
+ messagePublisher.publish(
+ CodeTestAddAnswerMessage(
+ tabId = activeCodeTestTabId,
+ messageId = messageId,
+ messageType = content.type,
+ message = content.message,
+ buttons = content.buttons,
+ formItems = content.formItems,
+ followUps = content.followUps,
+ canBeVoted = content.canBeVoted,
+ isAddingNewItem = true,
+ isLoading = content.type == ChatMessageType.AnswerStream,
+ clearPreviousItemButtons = false,
+ fileList = content.fileList,
+ footer = content.footer,
+ projectRootName = content.projectRootName,
+ codeReference = content.codeReference
+ )
+ )
+ return messageId
+ }
+
+ // helper for updating a specific chat card to chat UI. If messageId is not specified, update the last card.
+ suspend fun updateAnswer(
+ content: CodeTestChatMessageContent,
+ messageIdOverride: String? = null,
+ ) {
+ if (isInvalidSession()) return
+
+ messagePublisher.publish(
+ CodeTestUpdateAnswerMessage(
+ tabId = activeCodeTestTabId,
+ messageId = messageIdOverride,
+ messageType = content.type,
+ message = content.message,
+ buttons = content.buttons,
+ formItems = content.formItems,
+ followUps = content.followUps,
+ isAddingNewItem = true,
+ isLoading = content.type == ChatMessageType.AnswerPart,
+ clearPreviousItemButtons = false,
+ fileList = content.fileList,
+ footer = content.footer,
+ projectRootName = content.projectRootName,
+ codeReference = content.codeReference
+ )
+ )
+ }
+
+ suspend fun sendUpdatePlaceholder(tabId: String, newPlaceholder: String) {
+ messagePublisher.publish(
+ UpdatePlaceholderMessage(
+ tabId = tabId,
+ newPlaceholder = newPlaceholder,
+ )
+ )
+ }
+
+ // Everything to be nullable so that only those that are assigned are changed
+ suspend fun updateUI(
+ loadingChat: Boolean? = null,
+ cancelButtonWhenLoading: Boolean? = null,
+ promptInputPlaceholder: String? = null,
+ promptInputDisabledState: Boolean? = null,
+ promptInputProgress: ProgressField? = null,
+ ) {
+ messagePublisher.publish(
+ CodeTestUpdateUIMessage(
+ activeCodeTestTabId,
+ loadingChat,
+ cancelButtonWhenLoading,
+ promptInputPlaceholder,
+ promptInputDisabledState,
+ promptInputProgress
+ )
+ )
+ }
+
+ // currently only used for removing progress bar
+ suspend fun sendUpdatePromptProgress(tabId: String, progressField: ProgressField?) {
+ if (isInvalidSession()) return
+ messagePublisher.publish(PromptProgressMessage(tabId, progressField))
+ }
+
+ suspend fun addNewMessage(
+ content: CodeTestChatMessageContent,
+ messageIdOverride: String? = null,
+ clearPreviousItemButtons: Boolean? = false,
+ ) {
+ if (isInvalidSession()) return
+
+ messagePublisher.publish(
+ CodeTestChatMessage(
+ tabId = activeCodeTestTabId,
+ messageId = messageIdOverride ?: UUID.randomUUID().toString(),
+ messageType = content.type,
+ message = content.message,
+ buttons = content.buttons,
+ formItems = content.formItems,
+ followUps = content.followUps,
+ canBeVoted = content.canBeVoted,
+ informationCard = content.informationCard,
+ isAddingNewItem = true,
+ isLoading = content.type == ChatMessageType.AnswerPart,
+ clearPreviousItemButtons = clearPreviousItemButtons as Boolean
+ )
+ )
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/messages/CodeTestMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/messages/CodeTestMessage.kt
new file mode 100644
index 0000000000..9ece7fc48a
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/messages/CodeTestMessage.kt
@@ -0,0 +1,241 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthFollowUpType
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType
+import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference
+import software.aws.toolkits.jetbrains.services.cwc.messages.FollowUp
+import java.time.Instant
+import java.util.UUID
+
+const val CODE_TEST_TAB_NAME = "codetest"
+
+enum class CodeTestButtonId(val id: String) {
+ StopTestGeneration("stop_test_generation"),
+}
+
+data class Button(
+ val id: String,
+ val text: String,
+ val description: String? = null,
+ val icon: String? = null,
+ val keepCardAfterClick: Boolean? = false,
+ val disabled: Boolean? = false,
+ val waitMandatoryFormItems: Boolean? = false,
+ val position: String = "inside",
+ val status: String = "primary",
+)
+
+data class ProgressField(
+ val title: String? = null,
+ val value: Int? = null,
+ val valueText: String? = null,
+ val status: String? = null,
+ val actions: List? = null,
+ val text: String? = null,
+)
+
+data class FormItemOption(
+ val label: String,
+ val value: String,
+)
+
+data class FormItem(
+ val id: String,
+ val type: String = "select",
+ val title: String,
+ val mandatory: Boolean = true,
+ val options: List = emptyList(),
+)
+
+sealed interface CodeTestBaseMessage : AmazonQMessage
+
+// === UI -> App Messages ===
+sealed interface IncomingCodeTestMessage : CodeTestBaseMessage {
+ data class ChatPrompt(
+ val chatMessage: String,
+ val command: String,
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeTestMessage
+
+ data class StartTestGen(
+ @JsonProperty("tabID") val tabId: String,
+ val prompt: String,
+ ) : IncomingCodeTestMessage
+
+ data class ClickedLink(
+ @JsonProperty("tabID") val tabId: String,
+ val command: String,
+ val messageId: String?,
+ val link: String,
+ ) : IncomingCodeTestMessage
+
+ data class ClearChat(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeTestMessage
+
+ data class Help(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeTestMessage
+
+ data class NewTabCreated(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeTestMessage
+
+ data class TabRemoved(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingCodeTestMessage
+
+ data class ButtonClicked(
+ @JsonProperty("tabID") val tabId: String,
+ @JsonProperty("actionID") val actionID: String,
+ ) : IncomingCodeTestMessage
+}
+
+data class UpdatePlaceholderMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val newPlaceholder: String,
+) : UiMessage(
+ tabId = tabId,
+ type = "updatePlaceholderMessage"
+)
+
+data class ChatInputEnabledMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val enabled: Boolean,
+) : UiMessage(
+ tabId = tabId,
+ type = "chatInputEnabledMessage"
+)
+
+data class CodeTestUpdateUIMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val loadingChat: Boolean?,
+ val cancelButtonWhenLoading: Boolean?,
+ val promptInputPlaceholder: String?,
+ val promptInputDisabledState: Boolean?,
+ val promptInputProgress: ProgressField?,
+) : UiMessage(
+ tabId = tabId,
+ type = "updateUI"
+)
+
+// === App -> UI messages ===
+sealed class UiMessage(
+ open val tabId: String?,
+ open val type: String,
+ open val messageId: String? = UUID.randomUUID().toString(),
+) : CodeTestBaseMessage {
+ val time = Instant.now().epochSecond
+ val sender = CODE_TEST_TAB_NAME
+}
+
+data class AuthenticationUpdateMessage(
+ val authenticatingTabIDs: List,
+ val featureDevEnabled: Boolean,
+ val codeTransformEnabled: Boolean,
+ val codeScanEnabled: Boolean,
+ val codeTestEnabled: Boolean,
+ val docEnabled: Boolean,
+ val message: String? = null,
+) : UiMessage(
+ null,
+ type = "authenticationUpdateMessage"
+)
+
+data class AuthenticationNeededExceptionMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val authType: AuthFollowUpType,
+ val message: String? = null,
+) : UiMessage(
+ tabId = tabId,
+ type = "authNeededException"
+)
+
+data class PromptProgressMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val progressField: ProgressField? = null,
+) : UiMessage(
+ tabId = tabId,
+ type = "updatePromptProgress",
+)
+
+data class CodeTestChatMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ override val messageId: String? = UUID.randomUUID().toString(),
+ val messageType: ChatMessageType,
+ val message: String? = null,
+ val buttons: List? = null,
+ val canBeVoted: Boolean? = false,
+ val formItems: List? = null,
+ val followUps: List? = null,
+ val informationCard: Boolean? = false,
+ val isAddingNewItem: Boolean = true,
+ val isLoading: Boolean = false,
+ val clearPreviousItemButtons: Boolean = true,
+) : UiMessage(
+ messageId = messageId,
+ tabId = tabId,
+ type = "chatMessage",
+)
+
+data class CodeTestUpdateAnswerMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ override val messageId: String? = UUID.randomUUID().toString(),
+ val messageType: ChatMessageType,
+ val message: String? = null,
+ val buttons: List? = null,
+ val formItems: List? = null,
+ val followUps: List? = null,
+ val isAddingNewItem: Boolean = true,
+ val isLoading: Boolean = false,
+ val clearPreviousItemButtons: Boolean = true,
+ val fileList: List? = null,
+ val footer: List? = null,
+ val projectRootName: String? = null,
+ val codeReference: List? = null,
+) : UiMessage(
+ messageId = messageId,
+ tabId = tabId,
+ type = "updateAnswer",
+)
+
+data class CodeTestAddAnswerMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ override val messageId: String? = UUID.randomUUID().toString(),
+ val messageType: ChatMessageType,
+ val message: String? = null,
+ val buttons: List? = null,
+ val formItems: List? = null,
+ val followUps: List? = null,
+ val isAddingNewItem: Boolean = true,
+ val isLoading: Boolean = false,
+ val clearPreviousItemButtons: Boolean = true,
+ val fileList: List? = null,
+ val footer: List? = null,
+ val projectRootName: String? = null,
+ val canBeVoted: Boolean = false,
+ val codeReference: List? = null,
+) : UiMessage(
+ messageId = messageId,
+ tabId = tabId,
+ type = "addAnswer",
+)
+
+data class CodeTestChatMessageContent(
+ val message: String? = null,
+ val buttons: List? = null,
+ val formItems: List? = null,
+ val followUps: List? = null,
+ val type: ChatMessageType,
+ val canBeVoted: Boolean = false,
+ val informationCard: Boolean? = false,
+ val fileList: List? = null,
+ val footer: List? = null,
+ val projectRootName: String? = null,
+ val codeReference: List? = null,
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/BuildAndExecuteStatusIcon.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/BuildAndExecuteStatusIcon.kt
new file mode 100644
index 0000000000..0847cec649
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/BuildAndExecuteStatusIcon.kt
@@ -0,0 +1,39 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.model
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.BuildAndExecuteProgressStatus
+
+enum class BuildAndExecuteStatusIcon(val icon: String) {
+ WAIT("☐ "),
+ CURRENT("☐ "),
+ DONE("✔ "),
+}
+
+fun getBuildIcon(progressStatus: BuildAndExecuteProgressStatus) =
+ if (progressStatus < BuildAndExecuteProgressStatus.RUN_BUILD) {
+ BuildAndExecuteStatusIcon.WAIT.icon
+ } else if (progressStatus == BuildAndExecuteProgressStatus.RUN_BUILD) {
+ BuildAndExecuteStatusIcon.CURRENT.icon
+ } else {
+ BuildAndExecuteStatusIcon.DONE.icon
+ }
+
+fun getExecutionIcon(progressStatus: BuildAndExecuteProgressStatus) =
+ if (progressStatus < BuildAndExecuteProgressStatus.RUN_EXECUTION_TESTS) {
+ BuildAndExecuteStatusIcon.WAIT.icon
+ } else if (progressStatus == BuildAndExecuteProgressStatus.RUN_EXECUTION_TESTS) {
+ BuildAndExecuteStatusIcon.CURRENT.icon
+ } else {
+ BuildAndExecuteStatusIcon.DONE.icon
+ }
+
+fun getFixingTestCasesIcon(progressStatus: BuildAndExecuteProgressStatus) =
+ if (progressStatus < BuildAndExecuteProgressStatus.FIXING_TEST_CASES) {
+ BuildAndExecuteStatusIcon.WAIT.icon
+ } else if (progressStatus == BuildAndExecuteProgressStatus.FIXING_TEST_CASES) {
+ BuildAndExecuteStatusIcon.CURRENT.icon
+ } else {
+ BuildAndExecuteStatusIcon.DONE.icon
+ }
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/PreviousUTGIterationContext.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/PreviousUTGIterationContext.kt
new file mode 100644
index 0000000000..4576a0ea0c
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/PreviousUTGIterationContext.kt
@@ -0,0 +1,13 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.model
+
+import com.intellij.openapi.vfs.VirtualFile
+
+data class PreviousUTGIterationContext(
+ val buildLogFile: VirtualFile,
+ val testLogFile: VirtualFile,
+ val selectedFile: VirtualFile?,
+ val buildAndExecuteMessageId: String?,
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/ShortAnswer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/ShortAnswer.kt
new file mode 100644
index 0000000000..fb8caef7aa
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/model/ShortAnswer.kt
@@ -0,0 +1,41 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.model
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+
+data class ShortAnswerReference(
+ val licenseName: String? = null,
+ val repository: String? = null,
+ val url: String? = null,
+ val recommendationContentSpan: RecommendationContentSpan? = null,
+) {
+ data class RecommendationContentSpan(
+ val start: Int,
+ val end: Int,
+ )
+}
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class ShortAnswer(
+ val sourceFilePath: String? = null,
+
+ val testFramework: String? = null,
+
+ val testFilePath: String? = null,
+
+ val buildCommand: String? = null,
+
+ val executionCommand: String? = null,
+
+ val testCoverage: String? = null,
+
+ val stopIteration: String? = null,
+
+ val planSummary: String? = null,
+
+ val codeReferences: List? = null,
+
+ val numberOfTestMethods: Int? = 0,
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/session/Session.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/session/Session.kt
new file mode 100644
index 0000000000..1597c18d64
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/session/Session.kt
@@ -0,0 +1,69 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.session
+
+import com.intellij.openapi.vfs.VirtualFile
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.ConversationState
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.ShortAnswer
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.ShortAnswerReference
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage
+
+data class Session(val tabId: String) {
+ var isAuthenticating: Boolean = false
+ var authNeededNotified: Boolean = false
+ var conversationState: ConversationState = ConversationState.IDLE
+
+ // Generating unit tests
+ var isGeneratingTests: Boolean = false
+ var programmingLanguage: CodeWhispererProgrammingLanguage = CodeWhispererUnknownLanguage.INSTANCE
+ var testGenerationJob: String = ""
+ var testGenerationJobGroupName: String = ""
+
+ // Telemetry
+ var hasUserPromptSupplied: Boolean = false
+ var numberOfUnitTestCasesGenerated: Int? = null
+ var linesOfCodeGenerated: Int? = null
+ var charsOfCodeGenerated: Int? = null
+ var startTimeOfTestGeneration: Double = 0.0
+ var isCodeBlockSelected: Boolean = false
+ var srcPayloadSize: Long = 0
+ var srcZipFileSize: Long = 0
+ var artifactUploadDuration: Long = 0
+
+ // First iteration will have a value of 1
+ var iteration: Int = 0
+ var projectRoot: String = "/"
+ var shortAnswer: ShortAnswer = ShortAnswer()
+ var selectedFile: VirtualFile? = null
+ var testFileRelativePathToProjectRoot: String = ""
+ var testFileName: String = ""
+ var viewDiffMessageId: String? = null
+ var openedDiffFile: VirtualFile? = null
+ val generatedTestDiffs = mutableMapOf()
+ var codeReferences: List? = null
+
+ // Build loop execution
+ val buildAndExecuteTaskContext = BuildAndExecuteTaskContext()
+}
+
+data class BuildAndExecuteTaskContext(
+ var buildCommand: String = "",
+ var executionCommand: String = "",
+ var buildExitCode: Int = -1,
+ var testExitCode: Int = -1,
+ var progressStatus: BuildAndExecuteProgressStatus = BuildAndExecuteProgressStatus.START_STEP,
+)
+
+enum class BuildAndExecuteProgressStatus {
+ START_STEP,
+ INSTALL_DEPENDENCIES,
+ RUN_BUILD,
+ RUN_EXECUTION_TESTS,
+ TESTS_EXECUTED,
+ FIXING_TEST_CASES,
+ PROCESS_TEST_RESULTS,
+}
+
+const val UTG_CHAT_MAX_ITERATION = 4
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt
new file mode 100644
index 0000000000..0a9a782ecb
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt
@@ -0,0 +1,25 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.storage
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.Session
+
+class ChatSessionStorage {
+ private val sessions = mutableMapOf()
+
+ private fun createSession(tabId: String): Session {
+ val session = Session(tabId)
+ sessions[tabId] = session
+ return session
+ }
+
+ fun getSession(tabId: String): Session = sessions[tabId] ?: createSession(tabId)
+
+ fun deleteSession(tabId: String) {
+ sessions.remove(tabId)
+ }
+
+ // Find all sessions that are currently waiting to be authenticated
+ fun getAuthenticatingSessions(): List = this.sessions.values.filter { it.isAuthenticating }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/utils/UTGChatUtil.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/utils/UTGChatUtil.kt
new file mode 100644
index 0000000000..9b1615c461
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/utils/UTGChatUtil.kt
@@ -0,0 +1,200 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqCodeTest.utils
+
+import com.intellij.build.BuildContentManager
+import com.intellij.execution.impl.ConsoleViewImpl
+import com.intellij.execution.process.OSProcessHandler
+import com.intellij.execution.process.ProcessAdapter
+import com.intellij.execution.process.ProcessEvent
+import com.intellij.execution.process.ProcessHandler
+import com.intellij.execution.ui.ConsoleView
+import com.intellij.execution.ui.ConsoleViewContentType
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Key
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.ui.content.impl.ContentImpl
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.withContext
+import software.aws.toolkits.jetbrains.core.coroutines.EDT
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.getBuildIcon
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.getExecutionIcon
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.model.getFixingTestCasesIcon
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.BuildAndExecuteProgressStatus
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.session.BuildAndExecuteTaskContext
+import java.io.File
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+
+fun constructBuildAndExecutionSummaryText(currentStatus: BuildAndExecuteProgressStatus, iterationNum: Int): String {
+ val progressMessages = mutableListOf()
+
+ if (currentStatus >= BuildAndExecuteProgressStatus.RUN_BUILD) {
+ val verb = if (currentStatus == BuildAndExecuteProgressStatus.RUN_BUILD) "in progress" else "complete"
+ progressMessages.add("${getBuildIcon(currentStatus)}: Build $verb")
+ }
+
+ if (currentStatus >= BuildAndExecuteProgressStatus.RUN_EXECUTION_TESTS) {
+ val verb = if (currentStatus == BuildAndExecuteProgressStatus.RUN_EXECUTION_TESTS) "Executing" else "Executed"
+ progressMessages.add("${getExecutionIcon(currentStatus)}: $verb passed tests")
+ }
+
+ if (currentStatus >= BuildAndExecuteProgressStatus.FIXING_TEST_CASES) {
+ val verb = if (currentStatus == BuildAndExecuteProgressStatus.FIXING_TEST_CASES) "Fixing" else "Fixed"
+ progressMessages.add("${getFixingTestCasesIcon(currentStatus)}: $verb errors in tests")
+ }
+
+ if (currentStatus >= BuildAndExecuteProgressStatus.PROCESS_TEST_RESULTS) {
+ progressMessages.add("\n")
+ progressMessages.add("**Test case summary**")
+ progressMessages.add("\n")
+ progressMessages.add("Unit test coverage X%")
+ progressMessages.add("Build fails Y")
+ progressMessages.add("Assertion fails Z")
+ }
+
+ val prefix =
+ if (iterationNum < 2) {
+ "Sure"
+ } else {
+ val timeString = when (iterationNum) {
+ 2 -> "second"
+ 3 -> "third"
+ 4 -> "fourth"
+ // shouldn't reach
+ else -> "fifth"
+ }
+ "Working on the $timeString iteration now"
+ }
+
+ // Join all progress messages into a single string
+ return """
+ $prefix. This may take a few minutes and I'll update the progress here.
+
+ **Progress summary**
+
+ """.trimIndent() + progressMessages.joinToString("\n")
+}
+
+fun runBuildOrTestCommand(
+ localCommand: String,
+ tmpFile: VirtualFile,
+ project: Project,
+ isBuildCommand: Boolean,
+ buildAndExecuteTaskContext: BuildAndExecuteTaskContext,
+) {
+ if (localCommand.isEmpty()) {
+ buildAndExecuteTaskContext.testExitCode = 0
+ return
+ }
+ val repositoryPath = project.basePath ?: return
+ val commandParts = localCommand.split(" ")
+ val command = commandParts.first()
+ val args = commandParts.drop(1)
+ val file = File(tmpFile.path)
+
+ // Create Console View for Build Output
+ val console: ConsoleView = ConsoleViewImpl(project, true)
+
+ // Attach Console View to Build Tool Window
+ ApplicationManager.getApplication().invokeLater {
+ val tabName = if (isBuildCommand) "Q TestGen Build Output" else "Q Test Gen Test Execution Output"
+ val content = ContentImpl(console.component, tabName, true)
+ BuildContentManager.getInstance(project).addContent(content)
+ // TODO: remove these tabs when they are not needed
+ BuildContentManager.getInstance(project).setSelectedContent(content, false, false, true, null)
+ }
+
+ val processBuilder = ProcessBuilder()
+ .command(listOf(command) + args)
+ .directory(File(repositoryPath))
+ .redirectErrorStream(true)
+
+ try {
+ val process = processBuilder.start()
+ val processHandler: ProcessHandler = OSProcessHandler(process, localCommand, null)
+
+ // Attach Process Listener for Output Handling
+ processHandler.addProcessListener(object : ProcessAdapter() {
+ override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
+ val cleanedText = cleanText(event.text)
+ ApplicationManager.getApplication().invokeLater {
+ ApplicationManager.getApplication().runWriteAction {
+ file.appendText(cleanedText)
+ }
+ }
+ }
+
+ override fun processTerminated(event: ProcessEvent) {
+ val exitCode = event.exitCode
+ if (exitCode == 0) {
+ // green color
+ console.print("\nBUILD SUCCESSFUL\n", ConsoleViewContentType.USER_INPUT)
+ } else {
+ // red color
+ console.print("\nBUILD FAILED with exit code $exitCode\n", ConsoleViewContentType.ERROR_OUTPUT)
+ }
+ if (isBuildCommand) {
+ buildAndExecuteTaskContext.buildExitCode = exitCode
+ } else {
+ buildAndExecuteTaskContext.testExitCode = exitCode
+ }
+ }
+ })
+
+ // Start Process and Notify
+ console.attachToProcess(processHandler)
+ processHandler.startNotify()
+ console.print("\n", ConsoleViewContentType.NORMAL_OUTPUT)
+ } catch (e: Exception) {
+ console.print("Error executing command: $localCommand\n", ConsoleViewContentType.ERROR_OUTPUT)
+ console.print("$e", ConsoleViewContentType.ERROR_OUTPUT)
+ if (isBuildCommand) {
+ buildAndExecuteTaskContext.buildExitCode = 1
+ } else {
+ buildAndExecuteTaskContext.testExitCode = 1
+ }
+ return
+ }
+}
+
+private fun cleanText(input: String): String {
+ val cleaned = StringBuilder()
+ for (char in input) {
+ if (char == '\b' && cleaned.isNotEmpty()) {
+ // Remove the last character when encountering a backspace
+ cleaned.deleteCharAt(cleaned.length - 1)
+ } else if (char != '\b') {
+ cleaned.append(char)
+ }
+ }
+ return cleaned.toString()
+}
+
+suspend fun combineBuildAndExecuteLogFiles(
+ buildLogFile: VirtualFile?,
+ testLogFile: VirtualFile?,
+): VirtualFile? {
+ if (buildLogFile == null || testLogFile == null) return null
+ val buildLogFileContent = String(buildLogFile.contentsToByteArray(), StandardCharsets.UTF_8)
+ val testLogFileContent = String(testLogFile.contentsToByteArray(), StandardCharsets.UTF_8)
+
+ val combinedContent = "Build Output:\n$buildLogFileContent\nTest Execution Output:\n$testLogFileContent"
+
+ // Create a new virtual file and write combined content
+ val newFile = VirtualFileManager.getInstance().findFileByNioPath(
+ withContext(currentCoroutineContext()) {
+ Files.createTempFile(null, null)
+ }
+ )
+ withContext(EDT) {
+ ApplicationManager.getApplication().runWriteAction {
+ newFile?.setBinaryContent(combinedContent.toByteArray(StandardCharsets.UTF_8))
+ }
+ }
+
+ return newFile
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt
new file mode 100644
index 0000000000..0662b23d49
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt
@@ -0,0 +1,99 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc
+
+import com.intellij.openapi.application.ApplicationManager
+import kotlinx.coroutines.launch
+import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
+import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
+import software.aws.toolkits.jetbrains.services.amazonqDoc.controller.DocController
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.AuthenticationUpdateMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.IncomingDocMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.storage.ChatSessionStorage
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable
+import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable
+
+class DocApp : AmazonQApp {
+
+ private val scope = disposableCoroutineScope(this)
+
+ override val tabTypes = listOf("doc")
+
+ override fun init(context: AmazonQAppInitContext) {
+ val chatSessionStorage = ChatSessionStorage()
+ // Create Doc controller
+ val inboundAppMessagesHandler =
+ DocController(context, chatSessionStorage)
+
+ context.messageTypeRegistry.register(
+ "chat-prompt" to IncomingDocMessage.ChatPrompt::class,
+ "new-tab-was-created" to IncomingDocMessage.NewTabCreated::class,
+ "tab-was-removed" to IncomingDocMessage.TabRemoved::class,
+ "auth-follow-up-was-clicked" to IncomingDocMessage.AuthFollowUpWasClicked::class,
+ "follow-up-was-clicked" to IncomingDocMessage.FollowupClicked::class,
+ "chat-item-voted" to IncomingDocMessage.ChatItemVotedMessage::class,
+ "chat-item-feedback" to IncomingDocMessage.ChatItemFeedbackMessage::class,
+ "response-body-link-click" to IncomingDocMessage.ClickedLink::class,
+ "insert_code_at_cursor_position" to IncomingDocMessage.InsertCodeAtCursorPosition::class,
+ "open-diff" to IncomingDocMessage.OpenDiff::class,
+ "file-click" to IncomingDocMessage.FileClicked::class,
+ "doc_stop_generate" to IncomingDocMessage.StopDocGeneration::class
+ )
+
+ scope.launch {
+ context.messagesFromUiToApp.flow.collect { message ->
+ // Launch a new coroutine to handle each message
+ scope.launch { handleMessage(message, inboundAppMessagesHandler) }
+ }
+ }
+
+ ApplicationManager.getApplication().messageBus.connect(this).subscribe(
+ ToolkitConnectionManagerListener.TOPIC,
+ object : ToolkitConnectionManagerListener {
+ override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
+ scope.launch {
+ context.messagesFromAppToUi.publish(
+ AuthenticationUpdateMessage(
+ featureDevEnabled = isFeatureDevAvailable(context.project),
+ codeTransformEnabled = isCodeTransformAvailable(context.project),
+ codeScanEnabled = isCodeScanAvailable(context.project),
+ codeTestEnabled = isCodeTestAvailable(context.project),
+ docEnabled = isDocAvailable(context.project),
+ authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabID }
+ )
+ )
+ }
+ }
+ }
+ )
+ }
+
+ private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
+ when (message) {
+ is IncomingDocMessage.ChatPrompt -> inboundAppMessagesHandler.processPromptChatMessage(message)
+ is IncomingDocMessage.NewTabCreated -> inboundAppMessagesHandler.processNewTabCreatedMessage(message)
+ is IncomingDocMessage.TabRemoved -> inboundAppMessagesHandler.processTabRemovedMessage(message)
+ is IncomingDocMessage.AuthFollowUpWasClicked -> inboundAppMessagesHandler.processAuthFollowUpClick(message)
+ is IncomingDocMessage.FollowupClicked -> inboundAppMessagesHandler.processFollowupClickedMessage(message)
+ is IncomingDocMessage.ChatItemVotedMessage -> inboundAppMessagesHandler.processChatItemVotedMessage(message)
+ is IncomingDocMessage.ChatItemFeedbackMessage -> inboundAppMessagesHandler.processChatItemFeedbackMessage(message)
+ is IncomingDocMessage.ClickedLink -> inboundAppMessagesHandler.processLinkClick(message)
+ is IncomingDocMessage.InsertCodeAtCursorPosition -> inboundAppMessagesHandler.processInsertCodeAtCursorPosition(message)
+ is IncomingDocMessage.OpenDiff -> inboundAppMessagesHandler.processOpenDiff(message)
+ is IncomingDocMessage.FileClicked -> inboundAppMessagesHandler.processFileClicked(message)
+ is IncomingDocMessage.StopDocGeneration -> inboundAppMessagesHandler.processStopDocGeneration(message)
+ }
+ }
+
+ override fun dispose() {
+ // nothing to do
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocAppFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocAppFactory.kt
new file mode 100644
index 0000000000..de2526a665
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocAppFactory.kt
@@ -0,0 +1,11 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc
+
+import com.intellij.openapi.project.Project
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppFactory
+
+class DocAppFactory : AmazonQAppFactory {
+ override fun createApp(project: Project) = DocApp()
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocChatItems.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocChatItems.kt
new file mode 100644
index 0000000000..b4abb0540c
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocChatItems.kt
@@ -0,0 +1,45 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc
+
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.Button
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.ProgressField
+import software.aws.toolkits.resources.message
+
+val cancellingProgressField = ProgressField(
+ status = "warning",
+ text = message("general.canceling"),
+ value = -1,
+ actions = emptyList()
+)
+
+// TODO: Need to change the string after the F2F
+val docGenCompletedField = ProgressField(
+ status = "success",
+ text = message("general.success"),
+ value = 100,
+ actions = emptyList()
+)
+
+val cancelTestGenButton = Button(
+ id = "doc_stop_generate",
+ text = message("general.cancel"),
+ icon = "cancel"
+)
+
+fun inProgress(progress: Int, message: String? = null): ProgressField? {
+ // Constants to improve readability and maintainability
+ val completionProgress = 100
+ val completionValue = -1
+
+ // Pre-calculate the conditions to avoid repeated evaluations
+ val isComplete = progress >= completionProgress
+
+ return ProgressField(
+ status = "default",
+ text = message ?: message("amazonqDoc.inprogress_message.generating"),
+ value = if (isComplete) completionValue else progress,
+ actions = listOf(cancelTestGenButton)
+ )
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocConstants.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocConstants.kt
new file mode 100644
index 0000000000..0d3c7c4f2c
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocConstants.kt
@@ -0,0 +1,27 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc
+
+const val FEATURE_EVALUATION_PRODUCT_NAME = "DocGeneration"
+
+const val FEATURE_NAME = "Amazon Q Documentation Generation"
+
+// Max number of times a user can attempt to retry a code generation request if it fails
+const val CODE_GENERATION_RETRY_LIMIT = 3
+
+// The default retry limit used when the session could not be found
+const val DEFAULT_RETRY_LIMIT = 0
+
+// Max allowed size for a repository in bytes
+const val MAX_PROJECT_SIZE_BYTES: Long = 200 * 1024 * 1024
+
+enum class ModifySourceFolderErrorReason(
+ private val reasonText: String,
+) {
+ ClosedBeforeSelection("ClosedBeforeSelection"),
+ NotInWorkspaceFolder("NotInWorkspaceFolder"),
+ ;
+
+ override fun toString(): String = reasonText
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocExceptions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocExceptions.kt
new file mode 100644
index 0000000000..b27592b465
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocExceptions.kt
@@ -0,0 +1,25 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc
+
+import software.aws.toolkits.resources.message
+
+open class DocException(override val message: String?, override val cause: Throwable? = null) : RuntimeException()
+
+class ZipFileError(override val message: String, override val cause: Throwable?) : RuntimeException()
+
+class CodeIterationLimitError(override val message: String, override val cause: Throwable?) : RuntimeException()
+
+internal fun docServiceError(message: String?): Nothing =
+ throw DocException(message)
+
+internal fun codeGenerationFailedError(): Nothing =
+ throw DocException(message("amazonqFeatureDev.code_generation.failed_generation"))
+
+internal fun conversationIdNotFound(): Nothing =
+ throw DocException(message("amazonqFeatureDev.exception.conversation_not_found"))
+
+val denyListedErrors = arrayOf("Deserialization error", "Inaccessible host", "UnknownHost")
+fun createUserFacingErrorMessage(message: String?): String? =
+ if (message != null && denyListedErrors.any { message.contains(it) }) "$FEATURE_NAME API request failed" else message
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/InboundAppMessagesHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/InboundAppMessagesHandler.kt
new file mode 100644
index 0000000000..34038a749a
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/InboundAppMessagesHandler.kt
@@ -0,0 +1,21 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc
+
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.IncomingDocMessage
+
+interface InboundAppMessagesHandler {
+ suspend fun processPromptChatMessage(message: IncomingDocMessage.ChatPrompt)
+ suspend fun processNewTabCreatedMessage(message: IncomingDocMessage.NewTabCreated)
+ suspend fun processTabRemovedMessage(message: IncomingDocMessage.TabRemoved)
+ suspend fun processAuthFollowUpClick(message: IncomingDocMessage.AuthFollowUpWasClicked)
+ suspend fun processFollowupClickedMessage(message: IncomingDocMessage.FollowupClicked)
+ suspend fun processChatItemVotedMessage(message: IncomingDocMessage.ChatItemVotedMessage)
+ suspend fun processChatItemFeedbackMessage(message: IncomingDocMessage.ChatItemFeedbackMessage)
+ suspend fun processLinkClick(message: IncomingDocMessage.ClickedLink)
+ suspend fun processInsertCodeAtCursorPosition(message: IncomingDocMessage.InsertCodeAtCursorPosition)
+ suspend fun processOpenDiff(message: IncomingDocMessage.OpenDiff)
+ suspend fun processFileClicked(message: IncomingDocMessage.FileClicked)
+ suspend fun processStopDocGeneration(message: IncomingDocMessage.StopDocGeneration)
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/auth/DocAuthUtils.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/auth/DocAuthUtils.kt
new file mode 100644
index 0000000000..3390a8c5bb
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/auth/DocAuthUtils.kt
@@ -0,0 +1,16 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.auth
+
+import com.intellij.openapi.project.Project
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnection
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnectionType
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity
+
+fun isDocAvailable(project: Project): Boolean {
+ val connection = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q)
+ return (connection.connectionType == ActiveConnectionType.IAM_IDC || connection.connectionType == ActiveConnectionType.BUILDER_ID) &&
+ connection is ActiveConnection.ValidBearer
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt
new file mode 100644
index 0000000000..28c02cfe02
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocController.kt
@@ -0,0 +1,1114 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.controller
+
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.intellij.diff.DiffContentFactory
+import com.intellij.diff.DiffManager
+import com.intellij.diff.contents.EmptyContent
+import com.intellij.diff.requests.SimpleDiffRequest
+import com.intellij.diff.util.DiffUserDataKeys
+import com.intellij.ide.BrowserUtil
+import com.intellij.openapi.application.runInEdt
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.editor.Caret
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.vfs.VfsUtil
+import com.intellij.openapi.wm.ToolWindowManager
+import kotlinx.coroutines.withContext
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationFolderLevel
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationInteractionType
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationUserDecision
+import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.common.util.selectFolder
+import software.aws.toolkits.jetbrains.core.coroutines.EDT
+import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
+import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
+import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
+import software.aws.toolkits.jetbrains.services.amazonqDoc.DEFAULT_RETRY_LIMIT
+import software.aws.toolkits.jetbrains.services.amazonqDoc.DocException
+import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_NAME
+import software.aws.toolkits.jetbrains.services.amazonqDoc.InboundAppMessagesHandler
+import software.aws.toolkits.jetbrains.services.amazonqDoc.ModifySourceFolderErrorReason
+import software.aws.toolkits.jetbrains.services.amazonqDoc.ZipFileError
+import software.aws.toolkits.jetbrains.services.amazonqDoc.cancellingProgressField
+import software.aws.toolkits.jetbrains.services.amazonqDoc.createUserFacingErrorMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.denyListedErrors
+import software.aws.toolkits.jetbrains.services.amazonqDoc.inProgress
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.DocMessageType
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUp
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpIcons
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpStatusType
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpTypes
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.IncomingDocMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.initialExamples
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAnswer
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAsyncEventProgress
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAuthNeededException
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAuthenticationInProgressMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendChatInputEnabledMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendCodeResult
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendError
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendFolderConfirmationMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendMonthlyLimitError
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendSystemPrompt
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendUpdatePlaceholder
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendUpdatePromptProgress
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.updateFileComponent
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.DocSession
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.PrepareDocGenerationState
+import software.aws.toolkits.jetbrains.services.amazonqDoc.storage.ChatSessionStorage
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
+import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
+import software.aws.toolkits.jetbrains.utils.notifyError
+import software.aws.toolkits.resources.message
+import software.aws.toolkits.telemetry.AmazonqTelemetry
+import software.aws.toolkits.telemetry.Result
+import java.util.UUID
+
+enum class DocGenerationStep {
+ UPLOAD_TO_S3,
+ CREATE_KNOWLEDGE_GRAPH,
+ SUMMARIZING_FILES,
+ GENERATING_ARTIFACTS,
+ COMPLETE,
+}
+
+enum class Mode(val value: String) {
+ NONE("None"),
+ CREATE("Create"),
+ SYNC("Sync"),
+ EDIT("Edit"),
+}
+
+val checkIcons = mapOf(
+ "wait" to "☐",
+ "current" to "☐",
+ "done" to "☑"
+)
+
+fun getIconForStep(targetStep: DocGenerationStep, currentStep: DocGenerationStep): String? = when {
+ currentStep == targetStep -> checkIcons["current"]
+ currentStep > targetStep -> checkIcons["done"]
+ else -> checkIcons["wait"]
+}
+
+fun docGenerationProgressMessage(currentStep: DocGenerationStep, mode: Mode): String {
+ val isCreationMode = mode == Mode.CREATE
+ val baseLine = if (isCreationMode) message("amazonqDoc.progress_message.creating") else message("amazonqDoc.progress_message.updating")
+
+ return """
+ $baseLine ${message("amazonqDoc.progress_message.baseline")}
+
+ ${getIconForStep(DocGenerationStep.UPLOAD_TO_S3, currentStep)} ${message("amazonqDoc.progress_message.scanning")}
+
+ ${getIconForStep(DocGenerationStep.SUMMARIZING_FILES, currentStep)} ${message("amazonqDoc.progress_message.summarizing")}
+
+ ${getIconForStep(DocGenerationStep.GENERATING_ARTIFACTS, currentStep)} ${message("amazonqDoc.progress_message.generating")}
+ """.trimIndent()
+}
+
+class DocController(
+ private val context: AmazonQAppInitContext,
+ private val chatSessionStorage: ChatSessionStorage,
+ private val authController: AuthController = AuthController(),
+) : InboundAppMessagesHandler {
+ val messenger = context.messagesFromAppToUi
+ var mode: Mode = Mode.CREATE
+ val toolWindow = ToolWindowManager.getInstance(context.project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID)
+ var docGenerationTask = DocGenerationTask()
+
+ override suspend fun processPromptChatMessage(message: IncomingDocMessage.ChatPrompt) {
+ handleChat(
+ tabId = message.tabId,
+ message = message.chatMessage
+ )
+ }
+
+ override suspend fun processNewTabCreatedMessage(message: IncomingDocMessage.NewTabCreated) {
+ newTabOpened(message.tabId)
+ }
+
+ override suspend fun processTabRemovedMessage(message: IncomingDocMessage.TabRemoved) {
+ docGenerationTask.reset()
+ chatSessionStorage.deleteSession(message.tabId)
+ }
+
+ override suspend fun processAuthFollowUpClick(message: IncomingDocMessage.AuthFollowUpWasClicked) {
+ authController.handleAuth(context.project, message.authType)
+ messenger.sendAuthenticationInProgressMessage(message.tabId) // show user that authentication is in progress
+ messenger.sendChatInputEnabledMessage(message.tabId, enabled = false) // disable the input field while authentication is in progress
+ }
+
+ override suspend fun processFollowupClickedMessage(message: IncomingDocMessage.FollowupClicked) {
+ val session = getSessionInfo(message.tabId)
+
+ session.preloader(message.followUp.pillText, messenger) // also stores message in session history
+
+ when (message.followUp.type) {
+ FollowUpTypes.RETRY -> retryRequests(message.tabId)
+ FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER -> modifyDefaultSourceFolder(message.tabId)
+ FollowUpTypes.DEV_EXAMPLES -> messenger.initialExamples(message.tabId)
+ FollowUpTypes.INSERT_CODE -> insertCode(message.tabId)
+ FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE -> provideFeedbackAndRegenerateCode(message.tabId)
+ FollowUpTypes.NEW_TASK -> newTask(message.tabId)
+ FollowUpTypes.CLOSE_SESSION -> closeSession(message.tabId)
+ FollowUpTypes.CREATE_DOCUMENTATION -> {
+ docGenerationTask.interactionType = DocGenerationInteractionType.GENERATE_README
+ mode = Mode.CREATE
+ promptForDocTarget(message.tabId)
+ }
+
+ FollowUpTypes.UPDATE_DOCUMENTATION -> {
+ docGenerationTask.interactionType = DocGenerationInteractionType.UPDATE_README
+ updateDocumentation(message.tabId)
+ }
+
+ FollowUpTypes.CANCEL_FOLDER_SELECTION -> {
+ docGenerationTask.reset()
+ newTask(message.tabId)
+ }
+
+ FollowUpTypes.PROCEED_FOLDER_SELECTION -> if (mode == Mode.EDIT) makeChanges(message.tabId) else onDocsGeneration(message)
+ FollowUpTypes.ACCEPT_CHANGES -> {
+ docGenerationTask.userDecision = DocGenerationUserDecision.ACCEPT
+ sendDocGenerationTelemetry(message.tabId)
+ acceptChanges(message)
+ }
+
+ FollowUpTypes.MAKE_CHANGES -> {
+ mode = Mode.EDIT
+ makeChanges(message.tabId)
+ }
+
+ FollowUpTypes.REJECT_CHANGES -> {
+ docGenerationTask.userDecision = DocGenerationUserDecision.REJECT
+ sendDocGenerationTelemetry(message.tabId)
+ rejectChanges(message)
+ }
+
+ FollowUpTypes.SYNCHRONIZE_DOCUMENTATION -> {
+ mode = Mode.SYNC
+ promptForDocTarget(message.tabId)
+ }
+
+ FollowUpTypes.EDIT_DOCUMENTATION -> {
+ mode = Mode.EDIT
+ docGenerationTask.interactionType = DocGenerationInteractionType.EDIT_README
+ promptForDocTarget(message.tabId)
+ }
+ }
+ }
+
+ override suspend fun processStopDocGeneration(message: IncomingDocMessage.StopDocGeneration) {
+ messenger.sendUpdatePromptProgress(
+ tabId = message.tabId,
+ progressField = cancellingProgressField
+ )
+
+ messenger.sendAnswer(
+ tabId = message.tabId,
+ message("amazonqFeatureDev.code_generation.stopping_code_generation"),
+ messageType = DocMessageType.Answer,
+ canBeVoted = false
+ )
+ messenger.sendUpdatePlaceholder(
+ tabId = message.tabId,
+ newPlaceholder = message("amazonqFeatureDev.code_generation.stopping_code_generation")
+ )
+ messenger.sendChatInputEnabledMessage(tabId = message.tabId, enabled = false)
+ val session = getSessionInfo(message.tabId)
+
+ if (session.sessionState.token?.token !== null) {
+ session.sessionState.token?.cancel()
+ }
+ }
+
+ private suspend fun updateDocumentation(tabId: String) {
+ messenger.sendAnswer(
+ tabId,
+ messageType = DocMessageType.Answer,
+ followUp = listOf(
+ FollowUp(
+ type = FollowUpTypes.SYNCHRONIZE_DOCUMENTATION,
+ pillText = message("amazonqDoc.prompt.update.follow_up.sync"),
+ prompt = message("amazonqDoc.prompt.update.follow_up.sync"),
+ ),
+ FollowUp(
+ type = FollowUpTypes.EDIT_DOCUMENTATION,
+ pillText = message("amazonqDoc.prompt.update.follow_up.edit"),
+ prompt = message("amazonqDoc.prompt.update.follow_up.edit"),
+ )
+ )
+ )
+
+ messenger.sendChatInputEnabledMessage(tabId, enabled = false)
+ }
+
+ private suspend fun makeChanges(tabId: String) {
+ messenger.sendAnswer(
+ tabId = tabId,
+ message = message("amazonqDoc.edit.message"),
+ messageType = DocMessageType.Answer
+ )
+
+ messenger.sendUpdatePlaceholder(tabId, message("amazonqDoc.edit.placeholder"))
+ messenger.sendChatInputEnabledMessage(tabId, true)
+ }
+
+ private suspend fun rejectChanges(message: IncomingDocMessage.FollowupClicked) {
+ messenger.sendAnswer(
+ tabId = message.tabId,
+ message = message("amazonqDoc.prompt.reject.message"),
+ followUp = listOf(
+ FollowUp(
+ pillText = message("amazonqFeatureDev.follow_up.new_task"),
+ prompt = message("amazonqFeatureDev.follow_up.new_task"),
+ status = FollowUpStatusType.Info,
+ type = FollowUpTypes.NEW_TASK
+ ),
+ FollowUp(
+ pillText = message("amazonqFeatureDev.follow_up.close_session"),
+ prompt = message("amazonqFeatureDev.follow_up.close_session"),
+ status = FollowUpStatusType.Info,
+ type = FollowUpTypes.CLOSE_SESSION
+ )
+ ),
+ messageType = DocMessageType.Answer
+ )
+
+ messenger.sendChatInputEnabledMessage(message.tabId, false)
+ }
+
+ private suspend fun acceptChanges(message: IncomingDocMessage.FollowupClicked) {
+ insertCode(message.tabId)
+ }
+
+ private suspend fun promptForDocTarget(tabId: String) {
+ val session = getSessionInfo(tabId)
+
+ val currentSourceFolder = session.context.selectedSourceFolder
+
+ try {
+ messenger.sendFolderConfirmationMessage(
+ tabId = tabId,
+ message = if (mode == Mode.CREATE) message("amazonqDoc.prompt.create") else message("amazonqDoc.prompt.update"),
+ folderPath = currentSourceFolder.name,
+ followUps = listOf(
+ FollowUp(
+ icon = FollowUpIcons.Ok,
+ pillText = message("amazonqDoc.prompt.folder.proceed"),
+ prompt = message("amazonqDoc.prompt.folder.proceed"),
+ status = FollowUpStatusType.Success,
+ type = FollowUpTypes.PROCEED_FOLDER_SELECTION
+ ),
+ FollowUp(
+ icon = FollowUpIcons.Refresh,
+ pillText = message("amazonqDoc.prompt.folder.change"),
+ prompt = message("amazonqDoc.prompt.folder.change"),
+ status = FollowUpStatusType.Info,
+ type = FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER
+ ),
+ FollowUp(
+ icon = FollowUpIcons.Cancel,
+ pillText = message("general.cancel"),
+ prompt = message("general.cancel"),
+ status = FollowUpStatusType.Error,
+ type = FollowUpTypes.CANCEL_FOLDER_SELECTION
+ ),
+ )
+ )
+
+ messenger.sendChatInputEnabledMessage(tabId, false)
+ } catch (e: Exception) {
+ logger.error { "Error sending answer: ${e.message}" }
+ // Consider logging the error or handling it appropriately
+ }
+ }
+
+ override suspend fun processChatItemVotedMessage(message: IncomingDocMessage.ChatItemVotedMessage) {
+ logger.debug { "$FEATURE_NAME: Processing ChatItemVotedMessage: $message" }
+
+ val session = chatSessionStorage.getSession(message.tabId, context.project)
+ when (message.vote) {
+ "upvote" -> {
+ AmazonqTelemetry.codeGenerationThumbsUp(
+ amazonqConversationId = session.conversationId,
+ credentialStartUrl = getStartUrl(project = context.project)
+ )
+ }
+
+ "downvote" -> {
+ AmazonqTelemetry.codeGenerationThumbsDown(
+ amazonqConversationId = session.conversationId,
+ credentialStartUrl = getStartUrl(project = context.project)
+ )
+ }
+ }
+ }
+
+ override suspend fun processChatItemFeedbackMessage(message: IncomingDocMessage.ChatItemFeedbackMessage) {
+ logger.debug { "$FEATURE_NAME: Processing ChatItemFeedbackMessage: ${message.comment}" }
+
+ val session = getSessionInfo(message.tabId)
+
+ val comment = FeedbackComment(
+ conversationId = session.conversationId,
+ userComment = message.comment.orEmpty(),
+ reason = message.selectedOption,
+ messageId = message.messageId,
+ type = "doc-chat-answer-feedback"
+ )
+
+ try {
+ TelemetryService.getInstance().sendFeedback(
+ sentiment = Sentiment.NEGATIVE,
+ comment = objectMapper.writeValueAsString(comment),
+ )
+ logger.info { "$FEATURE_NAME answer feedback sent: \"Negative\"" }
+ } catch (e: Throwable) {
+ e.notifyError(message("feedback.submit_failed", e))
+ logger.warn(e) { "Failed to submit feedback" }
+ return
+ }
+ }
+
+ override suspend fun processLinkClick(message: IncomingDocMessage.ClickedLink) {
+ BrowserUtil.browse(message.link)
+ }
+
+ override suspend fun processInsertCodeAtCursorPosition(message: IncomingDocMessage.InsertCodeAtCursorPosition) {
+ logger.debug { "$FEATURE_NAME: Processing InsertCodeAtCursorPosition: $message" }
+
+ withContext(EDT) {
+ val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditor ?: return@withContext
+
+ val caret: Caret = editor.caretModel.primaryCaret
+ val offset: Int = caret.offset
+
+ WriteCommandAction.runWriteCommandAction(context.project) {
+ if (caret.hasSelection()) {
+ editor.document.deleteString(caret.selectionStart, caret.selectionEnd)
+ }
+ editor.document.insertString(offset, message.code)
+ }
+ }
+ }
+
+ override suspend fun processOpenDiff(message: IncomingDocMessage.OpenDiff) {
+ val session = getSessionInfo(message.tabId)
+
+ AmazonqTelemetry.isReviewedChanges(
+ amazonqConversationId = session.conversationId,
+ enabled = true,
+ credentialStartUrl = getStartUrl(project = context.project)
+ )
+
+ val project = context.project
+ val sessionState = session.sessionState
+
+ when (sessionState) {
+ is PrepareDocGenerationState -> {
+ runInEdt {
+ val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
+
+ val leftDiffContent = if (existingFile == null) {
+ EmptyContent()
+ } else {
+ DiffContentFactory.getInstance().create(project, existingFile)
+ }
+
+ val newFileContent = sessionState.filePaths.find { it.zipFilePath == message.filePath }?.fileContent
+
+ val rightDiffContent = if (message.deleted || newFileContent == null) {
+ EmptyContent()
+ } else {
+ DiffContentFactory.getInstance().create(newFileContent)
+ }
+
+ val request = SimpleDiffRequest(message.filePath, leftDiffContent, rightDiffContent, null, null)
+ request.putUserData(DiffUserDataKeys.FORCE_READ_ONLY, true)
+
+ DiffManager.getInstance().showDiff(project, request)
+ }
+ }
+
+ else -> {
+ logger.error { "$FEATURE_NAME: OpenDiff event is received for a conversation that has ${session.sessionState.phase} phase" }
+ messenger.sendError(
+ tabId = message.tabId,
+ errMessage = message("amazonqFeatureDev.exception.open_diff_failed"),
+ retries = 0,
+ conversationId = session.conversationIdUnsafe
+ )
+ }
+ }
+ }
+
+ override suspend fun processFileClicked(message: IncomingDocMessage.FileClicked) {
+ val fileToUpdate = message.filePath
+ val session = getSessionInfo(message.tabId)
+ val messageId = message.messageId
+
+ var filePaths: List = emptyList()
+ var deletedFiles: List = emptyList()
+ when (val state = session.sessionState) {
+ is PrepareDocGenerationState -> {
+ filePaths = state.filePaths
+ deletedFiles = state.deletedFiles
+ }
+ }
+
+ // Mark the file as rejected or not depending on the previous state
+ filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
+ deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected }
+
+ messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId)
+ }
+
+ private suspend fun newTabOpened(tabId: String) {
+ var session: DocSession? = null
+ try {
+ session = getSessionInfo(tabId)
+ logger.debug { "$FEATURE_NAME: Session created with id: ${session.tabID}" }
+
+ val credentialState = authController.getAuthNeededStates(context.project).amazonQ
+ if (credentialState != null) {
+ messenger.sendAuthNeededException(
+ tabId = tabId,
+ triggerId = UUID.randomUUID().toString(),
+ credentialState = credentialState,
+ )
+ session.isAuthenticating = true
+ return
+ }
+ docGenerationTask.userIdentity = session.getUserIdentity()
+ docGenerationTask.numberOfNavigation += 1
+ messenger.sendUpdatePlaceholder(tabId, message("amazonqDoc.prompt.placeholder"))
+ } catch (err: Exception) {
+ val message = createUserFacingErrorMessage(err.message)
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = message ?: message("amazonqFeatureDev.exception.request_failed"),
+ retries = retriesRemaining(session),
+ conversationId = session?.conversationIdUnsafe
+ )
+ }
+ }
+
+ private suspend fun insertCode(tabId: String) {
+ var session: DocSession? = null
+ try {
+ session = getSessionInfo(tabId)
+
+ var filePaths: List = emptyList()
+ var deletedFiles: List = emptyList()
+
+ when (val state = session.sessionState) {
+ is PrepareDocGenerationState -> {
+ filePaths = state.filePaths
+ deletedFiles = state.deletedFiles
+ }
+ }
+
+ AmazonqTelemetry.isAcceptedCodeChanges(
+ amazonqNumberOfFilesAccepted = (filePaths.filterNot { it.rejected }.size + deletedFiles.filterNot { it.rejected }.size) * 1.0,
+ amazonqConversationId = session.conversationId,
+ enabled = true,
+ credentialStartUrl = getStartUrl(project = context.project)
+ )
+
+ session.insertChanges(
+ filePaths = filePaths.filterNot { it.rejected },
+ deletedFiles = deletedFiles.filterNot { it.rejected }
+ )
+
+ messenger.sendSystemPrompt(
+ tabId = tabId,
+ followUp = listOf(
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.reject.new_task"),
+ type = FollowUpTypes.NEW_TASK,
+ status = FollowUpStatusType.Info
+ ),
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.reject.close_session"),
+ type = FollowUpTypes.CLOSE_SESSION,
+ status = FollowUpStatusType.Info
+ )
+ )
+ )
+
+ messenger.sendUpdatePlaceholder(
+ tabId = tabId,
+ newPlaceholder = message("amazonqFeatureDev.placeholder.additional_improvements")
+ )
+ } catch (err: Exception) {
+ val message = createUserFacingErrorMessage("Failed to insert code changes: ${err.message}")
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = message ?: message("amazonqFeatureDev.exception.insert_code_failed"),
+ retries = retriesRemaining(session),
+ conversationId = session?.conversationIdUnsafe
+ )
+ }
+ }
+
+ private suspend fun newTask(tabId: String) {
+ val session = getSessionInfo(tabId)
+ val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
+ docGenerationTask = DocGenerationTask()
+ AmazonqTelemetry.endChat(
+ amazonqConversationId = session.conversationId,
+ amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
+ credentialStartUrl = getStartUrl(project = context.project)
+ )
+ chatSessionStorage.deleteSession(tabId)
+
+ messenger.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
+ )
+
+ messenger.sendUpdatePlaceholder(
+ tabId = tabId,
+ newPlaceholder = message("amazonqFeatureDev.placeholder.after_code_generation")
+ )
+
+ newTabOpened(tabId)
+
+ messenger.sendSystemPrompt(
+ tabId = tabId,
+ followUp = listOf(
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.create"),
+ prompt = message("amazonqDoc.prompt.create"),
+ type = FollowUpTypes.CREATE_DOCUMENTATION,
+ ),
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.update"),
+ prompt = message("amazonqDoc.prompt.update"),
+ type = FollowUpTypes.UPDATE_DOCUMENTATION,
+ )
+ )
+ )
+ }
+
+ private suspend fun closeSession(tabId: String) {
+ messenger.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = message("amazonqFeatureDev.chat_message.closed_session"),
+ canBeVoted = true
+ )
+
+ messenger.sendUpdatePlaceholder(
+ tabId = tabId,
+ newPlaceholder = message("amazonqFeatureDev.placeholder.closed_session")
+ )
+
+ messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = false)
+ docGenerationTask.reset()
+
+ val session = getSessionInfo(tabId)
+ val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
+ AmazonqTelemetry.endChat(
+ amazonqConversationId = session.conversationId,
+ amazonqEndOfTheConversationLatency = sessionLatency.toDouble(),
+ credentialStartUrl = getStartUrl(project = context.project)
+ )
+ }
+
+ private suspend fun provideFeedbackAndRegenerateCode(tabId: String) {
+ val session = getSessionInfo(tabId)
+
+ AmazonqTelemetry.isProvideFeedbackForCodeGen(
+ amazonqConversationId = session.conversationId,
+ enabled = true,
+ credentialStartUrl = getStartUrl(project = context.project)
+ )
+
+ // Unblock the message button
+ messenger.sendAsyncEventProgress(tabId = tabId, inProgress = false)
+
+ messenger.sendAnswer(
+ tabId = tabId,
+ message = message("amazonqFeatureDev.code_generation.provide_code_feedback"),
+ messageType = DocMessageType.Answer,
+ canBeVoted = true
+ )
+ messenger.sendUpdatePlaceholder(tabId, message("amazonqFeatureDev.placeholder.provide_code_feedback"))
+ }
+
+ private suspend fun processErrorChatMessage(err: Exception, session: DocSession?, tabId: String) {
+ logger.warn(err) { "Encountered ${err.message} for tabId: $tabId" }
+ messenger.sendUpdatePromptProgress(tabId, null)
+
+ when (err) {
+ is RepoSizeError -> {
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = err.message,
+ retries = retriesRemaining(session),
+ conversationId = session?.conversationIdUnsafe
+ )
+ messenger.sendSystemPrompt(
+ tabId = tabId,
+ followUp = listOf(
+ FollowUp(
+ pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
+ type = FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER,
+ status = FollowUpStatusType.Info,
+ )
+ ),
+ )
+ }
+
+ is ZipFileError -> {
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = err.message,
+ retries = 0,
+ conversationId = session?.conversationIdUnsafe
+ )
+ }
+
+ is MonthlyConversationLimitError -> {
+ messenger.sendUpdatePlaceholder(tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_monthly_limit"))
+ messenger.sendChatInputEnabledMessage(tabId, enabled = true)
+ messenger.sendMonthlyLimitError(tabId = tabId)
+ }
+
+ is DocException -> {
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = err.message,
+ retries = retriesRemaining(session),
+ conversationId = session?.conversationIdUnsafe
+ )
+ }
+
+ is CodeIterationLimitException -> {
+ messenger.sendUpdatePlaceholder(tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_monthly_limit"))
+ messenger.sendChatInputEnabledMessage(tabId, enabled = true)
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = err.message,
+ retries = retriesRemaining(session),
+ conversationId = session?.conversationIdUnsafe,
+ showDefaultMessage = true,
+ )
+
+ val filePaths: List = when (val state = session?.sessionState) {
+ is PrepareDocGenerationState -> state.filePaths ?: emptyList()
+ else -> emptyList()
+ }
+
+ val deletedFiles: List = when (val state = session?.sessionState) {
+ is PrepareDocGenerationState -> state.deletedFiles ?: emptyList()
+ else -> emptyList()
+ }
+
+ val followUp = if (filePaths.size == 0 && deletedFiles.size == 0) {
+ listOf(
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.reject.new_task"),
+ type = FollowUpTypes.NEW_TASK,
+ status = FollowUpStatusType.Info
+ ),
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.reject.close_session"),
+ type = FollowUpTypes.CLOSE_SESSION,
+ status = FollowUpStatusType.Info
+ )
+ )
+ } else {
+ listOf(
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.review.accept"),
+ prompt = message("amazonqDoc.prompt.review.accept"),
+ status = FollowUpStatusType.Success,
+ type = FollowUpTypes.ACCEPT_CHANGES,
+ icon = FollowUpIcons.Ok,
+ ),
+ FollowUp(
+ pillText = message("general.reject"),
+ prompt = message("general.reject"),
+ status = FollowUpStatusType.Error,
+ type = FollowUpTypes.REJECT_CHANGES,
+ icon = FollowUpIcons.Cancel,
+ ),
+ )
+ }
+
+ messenger.sendSystemPrompt(
+ tabId = tabId,
+ followUp = followUp,
+ )
+ }
+
+ else -> {
+ var msg = createUserFacingErrorMessage("$FEATURE_NAME request failed: ${err.message ?: err.cause?.message}")
+ val isDenyListedError = denyListedErrors.any { msg?.contains(it) ?: false }
+ val defaultMessage: String = when (session?.sessionState?.phase) {
+ SessionStatePhase.CODEGEN -> {
+ if (isDenyListedError || retriesRemaining(session) > 0) {
+ message("amazonqFeatureDev.code_generation.error_message")
+ } else {
+ message("amazonqFeatureDev.code_generation.no_retries.error_message")
+ }
+ }
+
+ else -> message("amazonqFeatureDev.error_text")
+ }
+
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = defaultMessage,
+ retries = retriesRemaining(session),
+ conversationId = session?.conversationIdUnsafe
+ )
+ }
+ }
+ }
+
+ private suspend fun handleChat(
+ tabId: String,
+ message: String,
+ ) {
+ var session: DocSession? = null
+ try {
+ logger.debug { "$FEATURE_NAME: Processing message: $message" }
+ session = getSessionInfo(tabId)
+
+ val credentialState = authController.getAuthNeededStates(context.project).amazonQ
+ if (credentialState != null) {
+ messenger.sendAuthNeededException(
+ tabId = tabId,
+ triggerId = UUID.randomUUID().toString(),
+ credentialState = credentialState,
+ )
+ session.isAuthenticating = true
+ return
+ }
+ docGenerationTask.userIdentity = session.getUserIdentity()
+ session.preloader(message, messenger)
+
+ when (session.sessionState.phase) {
+ SessionStatePhase.CODEGEN -> {
+ messenger.sendUpdatePromptProgress(tabId, inProgress(progress = 10))
+ onCodeGeneration(session, message, tabId, mode)
+ }
+ else -> null
+ }
+
+ val filePaths: List = when (val state = session.sessionState) {
+ is PrepareDocGenerationState -> state.filePaths
+ else -> emptyList()
+ }
+
+ processOpenDiff(
+ message = IncomingDocMessage.OpenDiff(tabId = tabId, filePath = filePaths[0].zipFilePath, deleted = false)
+ )
+ } catch (err: Exception) {
+ processErrorChatMessage(err, session, tabId)
+
+ // Lock the chat input until they explicitly click one of the follow-ups
+ messenger.sendChatInputEnabledMessage(tabId, enabled = false)
+ }
+ }
+
+ private suspend fun onDocsGeneration(followUpMessage: IncomingDocMessage.FollowupClicked) {
+ messenger.sendUpdatePromptProgress(tabId = followUpMessage.tabId, inProgress(progress = 10, message("amazonqDoc.progress_message.scanning")))
+
+ val session = getSessionInfo(followUpMessage.tabId)
+
+ messenger.sendAnswer(
+ message = docGenerationProgressMessage(DocGenerationStep.UPLOAD_TO_S3, this.mode),
+ messageType = DocMessageType.AnswerPart,
+ tabId = followUpMessage.tabId,
+ )
+
+ try {
+ val sessionMessage: String = when (mode) {
+ Mode.CREATE -> message("amazonqDoc.session.create")
+ else -> message("amazonqDoc.session.sync")
+ }
+
+ session.send(sessionMessage)
+
+ val filePaths: List = when (val state = session.sessionState) {
+ is PrepareDocGenerationState -> state.filePaths ?: emptyList()
+ else -> emptyList()
+ }
+
+ val deletedFiles: List = when (val state = session.sessionState) {
+ is PrepareDocGenerationState -> state.deletedFiles ?: emptyList()
+ else -> emptyList()
+ }
+
+ val references: List = when (val state = session.sessionState) {
+ is PrepareDocGenerationState -> state.references ?: emptyList()
+ else -> emptyList()
+ }
+
+ if (session.sessionState.token
+ ?.token
+ ?.isCancellationRequested() == true
+ ) {
+ return
+ }
+
+ if (filePaths.isEmpty() && deletedFiles.isEmpty()) {
+ handleEmptyFiles(followUpMessage, session)
+ return
+ }
+
+ messenger.sendAnswer(
+ message = docGenerationProgressMessage(DocGenerationStep.COMPLETE, mode),
+ messageType = DocMessageType.AnswerPart,
+ tabId = followUpMessage.tabId,
+ )
+
+ messenger.sendCodeResult(
+ tabId = followUpMessage.tabId,
+ filePaths = filePaths,
+ deletedFiles = deletedFiles,
+ uploadId = session.conversationId,
+ references = references
+ )
+
+ messenger.sendAnswer(
+ messageType = DocMessageType.Answer,
+ tabId = followUpMessage.tabId,
+ message = message("amazonqDoc.prompt.review.message")
+ )
+
+ messenger.sendAnswer(
+ messageType = DocMessageType.SystemPrompt,
+ tabId = followUpMessage.tabId,
+ followUp = listOf(
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.review.accept"),
+ prompt = message("amazonqDoc.prompt.review.accept"),
+ status = FollowUpStatusType.Success,
+ type = FollowUpTypes.ACCEPT_CHANGES,
+ icon = FollowUpIcons.Ok,
+ ),
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.review.changes"),
+ prompt = message("amazonqDoc.prompt.review.changes"),
+ status = FollowUpStatusType.Info,
+ type = FollowUpTypes.MAKE_CHANGES,
+ icon = FollowUpIcons.Info,
+ ),
+ FollowUp(
+ pillText = message("general.reject"),
+ prompt = message("general.reject"),
+ status = FollowUpStatusType.Error,
+ type = FollowUpTypes.REJECT_CHANGES,
+ icon = FollowUpIcons.Cancel,
+ )
+ )
+ )
+
+ processOpenDiff(
+ message = IncomingDocMessage.OpenDiff(tabId = followUpMessage.tabId, filePath = filePaths[0].zipFilePath, deleted = false)
+ )
+ } catch (err: Exception) {
+ processErrorChatMessage(err, session, tabId = followUpMessage.tabId)
+
+ // Lock the chat input until they explicitly click one of the follow-ups
+ messenger.sendChatInputEnabledMessage(tabId = followUpMessage.tabId, enabled = false)
+ } finally {
+ messenger.sendUpdatePlaceholder(
+ tabId = followUpMessage.tabId,
+ newPlaceholder = message("amazonqDoc.prompt.placeholder")
+ )
+
+ messenger.sendChatInputEnabledMessage(followUpMessage.tabId, false)
+
+ if (session.sessionState.token
+ ?.token
+ ?.isCancellationRequested() == true
+ ) {
+ session.sessionState.token = CancellationTokenSource()
+ } else {
+ messenger.sendAsyncEventProgress(tabId = followUpMessage.tabId, inProgress = false) // Finish processing the event
+ messenger.sendChatInputEnabledMessage(tabId = followUpMessage.tabId, enabled = false) // Lock chat input until a follow-up is clicked.
+ }
+ }
+ }
+
+ private suspend fun handleEmptyFiles(
+ message: IncomingDocMessage.FollowupClicked,
+ session: DocSession,
+ ) {
+ messenger.sendAnswer(
+ message = message("amazonqDoc.error.generating"),
+ messageType = DocMessageType.Answer,
+ tabId = message.tabId,
+ canBeVoted = true
+ )
+
+ messenger.sendAnswer(
+ messageType = DocMessageType.SystemPrompt,
+ tabId = message.tabId,
+ followUp = if (retriesRemaining(session) > 0) {
+ listOf(
+ FollowUp(
+ pillText = message("amazonqFeatureDev.follow_up.retry"),
+ type = FollowUpTypes.RETRY,
+ status = FollowUpStatusType.Warning
+ )
+ )
+ } else {
+ emptyList()
+ }
+ )
+
+ // Lock the chat input until they explicitly click retry
+ messenger.sendChatInputEnabledMessage(tabId = message.tabId, enabled = false)
+ }
+
+ private suspend fun retryRequests(tabId: String) {
+ var session: DocSession? = null
+ docGenerationTask = DocGenerationTask()
+ try {
+ messenger.sendAsyncEventProgress(
+ tabId = tabId,
+ inProgress = true,
+ )
+ session = getSessionInfo(tabId)
+
+ // Decrease retries before making this request, just in case this one fails as well
+ session.decreaseRetries()
+
+ // Sending an empty message will re-run the last state with the previous values
+ handleChat(
+ tabId = tabId,
+ message = session.latestMessage
+ )
+ } catch (err: Exception) {
+ logger.error(err) { "Failed to retry request: ${err.message}" }
+ val message = createUserFacingErrorMessage("Failed to retry request: ${err.message}")
+ messenger.sendError(
+ tabId = tabId,
+ errMessage = message ?: message("amazonqFeatureDev.exception.retry_request_failed"),
+ retries = retriesRemaining(session),
+ conversationId = session?.conversationIdUnsafe,
+ )
+ } finally {
+ // Finish processing the event
+ messenger.sendAsyncEventProgress(
+ tabId = tabId,
+ inProgress = false,
+ )
+ }
+ }
+
+ private suspend fun modifyDefaultSourceFolder(tabId: String) {
+ val session = getSessionInfo(tabId)
+ val currentSourceFolder = session.context.selectedSourceFolder
+ val projectRoot = session.context.projectRoot
+
+ var result: Result = Result.Failed
+ var reason: ModifySourceFolderErrorReason? = null
+
+ withContext(EDT) {
+ val selectedFolder = selectFolder(context.project, currentSourceFolder)
+ // No folder was selected
+ if (selectedFolder == null) {
+ logger.info { "Cancelled dialog and not selected any folder" }
+
+ reason = ModifySourceFolderErrorReason.ClosedBeforeSelection
+ return@withContext
+ }
+
+ // The folder is not in the workspace
+ if (!selectedFolder.path.startsWith(projectRoot.path)) {
+ logger.info { "Selected folder not in workspace: ${selectedFolder.path}" }
+
+ messenger.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = message("amazonqFeatureDev.follow_up.incorrect_source_folder"),
+ )
+
+ reason = ModifySourceFolderErrorReason.NotInWorkspaceFolder
+ return@withContext
+ }
+ if (selectedFolder.path == projectRoot.path) {
+ docGenerationTask.folderLevel = DocGenerationFolderLevel.ENTIRE_WORKSPACE
+ } else {
+ docGenerationTask.folderLevel = DocGenerationFolderLevel.SUB_FOLDER
+ }
+
+ logger.info { "Selected correct folder inside workspace: ${selectedFolder.path}" }
+
+ session.context.selectedSourceFolder = selectedFolder
+ result = Result.Succeeded
+
+ promptForDocTarget(tabId)
+
+ messenger.sendChatInputEnabledMessage(tabId, enabled = false)
+
+ messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqDoc.prompt.placeholder"))
+ }
+
+ AmazonqTelemetry.modifySourceFolder(
+ amazonqConversationId = session.conversationId,
+ credentialStartUrl = getStartUrl(project = context.project),
+ result = result,
+ reason = reason?.toString()
+ )
+ }
+
+ private fun sendDocGenerationTelemetry(tabId: String) {
+ val session = getSessionInfo(tabId)
+ var filePaths: List = emptyList()
+
+ when (val state = session.sessionState) {
+ is PrepareDocGenerationState -> {
+ filePaths = state.filePaths
+ }
+ }
+ docGenerationTask.conversationId = session.conversationId
+ val (totalAddedChars, totalAddedLines, totalAddedFiles) = session.countAddedContent(filePaths, docGenerationTask.interactionType)
+ docGenerationTask.numberOfAddChars = totalAddedChars
+ docGenerationTask.numberOfAddLines = totalAddedLines
+ docGenerationTask.numberOfAddFiles = totalAddedFiles
+
+ val docGenerationEvent = docGenerationTask.docGenerationEventBase()
+ session.sendDocGenerationEvent(docGenerationEvent)
+ }
+
+ fun getProject() = context.project
+
+ private fun getSessionInfo(tabId: String) = chatSessionStorage.getSession(tabId, context.project)
+
+ fun retriesRemaining(session: DocSession?): Int = session?.retries ?: DEFAULT_RETRY_LIMIT
+
+ companion object {
+ private val logger = getLogger()
+
+ private val objectMapper = jacksonObjectMapper()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocControllerExtensions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocControllerExtensions.kt
new file mode 100644
index 0000000000..ff4374eae1
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocControllerExtensions.kt
@@ -0,0 +1,123 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.controller
+
+import com.intellij.notification.NotificationAction
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.DocMessageType
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUp
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpStatusType
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpTypes
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAnswer
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAsyncEventProgress
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendChatInputEnabledMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendCodeResult
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendSystemPrompt
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendUpdatePlaceholder
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.DocSession
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.PrepareDocGenerationState
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendSystemPrompt
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
+import software.aws.toolkits.jetbrains.utils.notifyInfo
+import software.aws.toolkits.resources.message
+
+suspend fun DocController.onCodeGeneration(session: DocSession, message: String, tabId: String, mode: Mode) {
+ try {
+ val sessionMessage = if (mode == Mode.CREATE) {
+ message(
+ "amazonqDoc.session.create"
+ )
+ } else if (mode == Mode.EDIT) message else message("amazonqDoc.session.sync")
+
+ session.send(sessionMessage) // Trigger code generation
+
+ val state = session.sessionState
+
+ var filePaths: List = emptyList()
+ var deletedFiles: List = emptyList()
+ var references: List = emptyList()
+ var uploadId = ""
+ var remainingIterations: Int? = null
+ var totalIterations: Int? = null
+
+ when (state) {
+ is PrepareDocGenerationState -> {
+ filePaths = state.filePaths
+ deletedFiles = state.deletedFiles
+ references = state.references
+ uploadId = state.uploadId
+ remainingIterations = state.codeGenerationRemainingIterationCount
+ totalIterations = state.codeGenerationTotalIterationCount
+ }
+ }
+
+ // Atm this is the only possible path as codegen is mocked to return empty.
+ if (filePaths.size or deletedFiles.size == 0) {
+ messenger.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = message("amazonqFeatureDev.code_generation.no_file_changes")
+ )
+ messenger.sendSystemPrompt(
+ tabId = tabId,
+ followUp = if (retriesRemaining(session) > 0) {
+ listOf(
+ FollowUp(
+ pillText = message("amazonqFeatureDev.follow_up.retry"),
+ type = FollowUpTypes.RETRY,
+ status = FollowUpStatusType.Warning
+ )
+ )
+ } else {
+ emptyList()
+ }
+ )
+ messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = false) // Lock chat input until retry is clicked.
+ return
+ }
+
+ messenger.sendCodeResult(tabId = tabId, uploadId = uploadId, filePaths = filePaths, deletedFiles = deletedFiles, references = references)
+
+ if (remainingIterations != null && totalIterations != null) {
+ messenger.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = if (remainingIterations == 0) {
+ message("amazonqFeatureDev.code_generation.iteration_zero")
+ } else {
+ message(
+ "amazonqFeatureDev.code_generation.iteration_counts",
+ remainingIterations,
+ totalIterations
+ )
+ }
+ )
+ }
+
+ messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase, InsertAction.ALL))
+
+ messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_code_generation"))
+ } finally {
+ messenger.sendAsyncEventProgress(tabId = tabId, inProgress = false) // Finish processing the event
+ messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = false) // Lock chat input until a follow-up is clicked.
+
+ if (toolWindow != null && !toolWindow.isVisible) {
+ notifyInfo(
+ title = message("amazonqFeatureDev.code_generation.notification_title"),
+ content = message("amazonqFeatureDev.code_generation.notification_message"),
+ project = getProject(),
+ notificationActions = listOf(openChatNotificationAction())
+ )
+ }
+ }
+}
+
+private fun DocController.openChatNotificationAction() = NotificationAction.createSimple(
+ message("amazonqFeatureDev.code_generation.notification_open_link")
+) {
+ toolWindow?.show()
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocGenerationTask.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocGenerationTask.kt
new file mode 100644
index 0000000000..5080ab5b4f
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/controller/DocGenerationTask.kt
@@ -0,0 +1,62 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.controller
+
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationEvent
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationFolderLevel
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationInteractionType
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationUserDecision
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+
+class DocGenerationTask {
+ // Telemetry fields
+ var conversationId: String? = null
+ var numberOfAddChars: Int? = null
+ var numberOfAddLines: Int? = null
+ var numberOfAddFiles: Int? = null
+ var userDecision: DocGenerationUserDecision? = null
+ var interactionType: DocGenerationInteractionType? = null
+ var userIdentity: String? = null
+ var numberOfNavigation = 0
+ var folderLevel: DocGenerationFolderLevel? = DocGenerationFolderLevel.ENTIRE_WORKSPACE
+ fun docGenerationEventBase(): DocGenerationEvent {
+ val undefinedProps = this::class.java.declaredFields
+ .filter { it.get(this) == null }
+ .map { it.name }
+
+ if (undefinedProps.isNotEmpty()) {
+ val undefinedValue = undefinedProps.joinToString(", ")
+ logger.debug { "DocGenerationEvent has undefined properties: $undefinedValue" }
+ }
+
+ return DocGenerationEvent.builder()
+ .conversationId(conversationId)
+ .numberOfAddChars(numberOfAddChars)
+ .numberOfAddLines(numberOfAddLines)
+ .numberOfAddFiles(numberOfAddFiles)
+ .userDecision(userDecision)
+ .interactionType(interactionType)
+ .userIdentity(userIdentity)
+ .numberOfNavigation(numberOfNavigation)
+ .folderLevel(folderLevel)
+ .build()
+ }
+
+ fun reset() {
+ conversationId = null
+ numberOfAddChars = null
+ numberOfAddLines = null
+ numberOfAddFiles = null
+ userDecision = null
+ interactionType = null
+ userIdentity = null
+ numberOfNavigation = 0
+ folderLevel = null
+ }
+
+ companion object {
+ private val logger = getLogger()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessage.kt
new file mode 100644
index 0000000000..61c8bc0109
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessage.kt
@@ -0,0 +1,287 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.messages
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonValue
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthFollowUpType
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
+import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference
+import java.time.Instant
+import java.util.UUID
+
+sealed interface DocBaseMessage : AmazonQMessage
+
+// === UI -> App Messages ===
+sealed interface IncomingDocMessage : DocBaseMessage {
+
+ data class ChatPrompt(
+ val chatMessage: String,
+ val command: String,
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingDocMessage
+
+ data class NewTabCreated(
+ val command: String,
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingDocMessage
+
+ data class AuthFollowUpWasClicked(
+ @JsonProperty("tabID") val tabId: String,
+ val authType: AuthFollowUpType,
+ ) : IncomingDocMessage
+
+ data class TabRemoved(
+ val command: String,
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingDocMessage
+
+ data class FollowupClicked(
+ val followUp: FollowUp,
+ @JsonProperty("tabID") val tabId: String,
+ val messageId: String?,
+ val command: String,
+ val tabType: String,
+ ) : IncomingDocMessage
+
+ data class ChatItemVotedMessage(
+ @JsonProperty("tabID") val tabId: String,
+ val messageId: String,
+ val vote: String,
+ ) : IncomingDocMessage
+
+ data class ChatItemFeedbackMessage(
+ @JsonProperty("tabID") val tabId: String,
+ val selectedOption: String,
+ val comment: String?,
+ val messageId: String,
+ ) : IncomingDocMessage
+
+ data class ClickedLink(
+ @JsonProperty("tabID") val tabId: String,
+ val command: String,
+ val messageId: String?,
+ val link: String,
+ ) : IncomingDocMessage
+
+ data class InsertCodeAtCursorPosition(
+ @JsonProperty("tabID") val tabId: String,
+ val code: String,
+ val insertionTargetType: String?,
+ val codeReference: List?,
+ ) : IncomingDocMessage
+
+ data class OpenDiff(
+ @JsonProperty("tabID") val tabId: String,
+ val filePath: String,
+ val deleted: Boolean,
+ ) : IncomingDocMessage
+
+ data class FileClicked(
+ @JsonProperty("tabID") val tabId: String,
+ val filePath: String,
+ val messageId: String,
+ val actionName: String,
+ ) : IncomingDocMessage
+
+ data class StopDocGeneration(
+ @JsonProperty("tabID") val tabId: String,
+ ) : IncomingDocMessage
+}
+
+// === UI -> App Messages ===
+
+sealed class UiMessage(
+ open val tabId: String?,
+ open val type: String,
+) : DocBaseMessage {
+ val time = Instant.now().epochSecond
+ val sender = "docChat"
+}
+
+enum class DocMessageType(
+ @field:JsonValue val json: String,
+) {
+ Answer("answer"),
+ AnswerPart("answer-part"),
+ AnswerStream("answer-stream"),
+ SystemPrompt("system-prompt"),
+}
+
+data class DocMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ @JsonProperty("triggerID") val triggerId: String,
+ val messageType: DocMessageType,
+ val messageId: String,
+ val message: String? = null,
+ val followUps: List? = null,
+ val canBeVoted: Boolean,
+ val snapToTop: Boolean,
+
+) : UiMessage(
+ tabId = tabId,
+ type = "chatMessage",
+)
+
+data class AsyncEventProgressMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val message: String? = null,
+ val inProgress: Boolean,
+) : UiMessage(
+ tabId = tabId,
+ type = "asyncEventProgressMessage"
+)
+
+data class UpdatePlaceholderMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val newPlaceholder: String,
+) : UiMessage(
+ tabId = tabId,
+ type = "updatePlaceholderMessage"
+)
+
+data class FileComponent(
+ @JsonProperty("tabID") override val tabId: String,
+ val filePaths: List,
+ val deletedFiles: List,
+ val messageId: String,
+) : UiMessage(
+ tabId = tabId,
+ type = "updateFileComponent"
+)
+
+data class ChatInputEnabledMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val enabled: Boolean,
+) : UiMessage(
+ tabId = tabId,
+ type = "chatInputEnabledMessage"
+)
+data class ErrorMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val title: String,
+ val message: String,
+) : UiMessage(
+ tabId = tabId,
+ type = "errorMessage",
+)
+
+data class FolderConfirmationMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val folderPath: String,
+ val message: String,
+ val followUps: List?,
+) : UiMessage(
+ tabId = tabId,
+ type = "folderConfirmationMessage"
+)
+
+// this should come from mynah?
+data class ChatItemButton(
+ val id: String,
+ val text: String,
+ val icon: String,
+ val keepCardAfterClick: Boolean,
+ val disabled: Boolean,
+ val waitMandatoryFormItems: Boolean,
+)
+
+data class ProgressField(
+ val status: String,
+ val text: String,
+ val value: Int,
+ var actions: List,
+)
+
+data class AuthenticationUpdateMessage(
+ val authenticatingTabIDs: List,
+ val featureDevEnabled: Boolean,
+ val codeTransformEnabled: Boolean,
+ val codeScanEnabled: Boolean,
+ val codeTestEnabled: Boolean,
+ val docEnabled: Boolean,
+ val message: String? = null,
+ val messageId: String = UUID.randomUUID().toString(),
+) : UiMessage(
+ null,
+ type = "authenticationUpdateMessage",
+)
+
+data class AuthNeededException(
+ @JsonProperty("tabID") override val tabId: String,
+ @JsonProperty("triggerID") val triggerId: String,
+ val authType: AuthFollowUpType,
+ val message: String,
+) : UiMessage(
+ tabId = tabId,
+ type = "authNeededException",
+)
+
+data class CodeResultMessage(
+ @JsonProperty("tabID") override val tabId: String,
+ val conversationId: String,
+ val filePaths: List,
+ val deletedFiles: List,
+ val references: List,
+) : UiMessage(
+ tabId = tabId,
+ type = "codeResultMessage"
+)
+
+data class FollowUp(
+ val type: FollowUpTypes,
+ val pillText: String,
+ val prompt: String? = null,
+ val disabled: Boolean? = false,
+ val description: String? = null,
+ val status: FollowUpStatusType? = null,
+ val icon: FollowUpIcons? = null,
+)
+
+enum class FollowUpIcons(
+ @field:JsonValue val json: String,
+) {
+ Ok("ok"),
+ Refresh("refresh"),
+ Cancel("cancel"),
+ Info("info"),
+ Error("error"),
+}
+
+enum class FollowUpStatusType(
+ @field:JsonValue val json: String,
+) {
+ Info("info"),
+ Success("success"),
+ Warning("warning"),
+ Error("error"),
+}
+
+enum class FollowUpTypes(
+ @field:JsonValue val json: String,
+) {
+ RETRY("Retry"),
+ MODIFY_DEFAULT_SOURCE_FOLDER("ModifyDefaultSourceFolder"),
+ DEV_EXAMPLES("DevExamples"),
+ INSERT_CODE("InsertCode"),
+ PROVIDE_FEEDBACK_AND_REGENERATE_CODE("ProvideFeedbackAndRegenerateCode"),
+ NEW_TASK("NewTask"),
+ CLOSE_SESSION("CloseSession"),
+ CREATE_DOCUMENTATION("CreateDocumentation"),
+ UPDATE_DOCUMENTATION("UpdateDocumentation"),
+ CANCEL_FOLDER_SELECTION("CancelFolderSelection"),
+ PROCEED_FOLDER_SELECTION("ProceedFolderSelection"),
+ ACCEPT_CHANGES("AcceptChanges"),
+ MAKE_CHANGES("MakeChanges"),
+ REJECT_CHANGES("RejectChanges"),
+ SYNCHRONIZE_DOCUMENTATION("SynchronizeDocumentation"),
+ EDIT_DOCUMENTATION("EditDocumentation"),
+}
+
+// Util classes
+data class ReducedCodeReference(
+ val information: String,
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessagePublisherExtensions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessagePublisherExtensions.kt
new file mode 100644
index 0000000000..963b6d95be
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/messages/DocMessagePublisherExtensions.kt
@@ -0,0 +1,215 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.messages
+
+import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthNeededState
+import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.ProgressField
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.PromptProgressMessage
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
+import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference
+import software.aws.toolkits.jetbrains.services.cwc.messages.RecommendationContentSpan
+import software.aws.toolkits.resources.message
+import java.util.UUID
+
+suspend fun MessagePublisher.sendAnswer(
+ tabId: String,
+ message: String? = null,
+ messageType: DocMessageType,
+ followUp: List? = null,
+ canBeVoted: Boolean? = false,
+ snapToTop: Boolean? = false,
+) {
+ val chatMessage =
+ DocMessage(
+ tabId = tabId,
+ triggerId = UUID.randomUUID().toString(),
+ messageId = UUID.randomUUID().toString(),
+ messageType = messageType,
+ message = message,
+ followUps = followUp,
+ canBeVoted = canBeVoted ?: false,
+ snapToTop = snapToTop ?: false
+ )
+ this.publish(chatMessage)
+}
+
+suspend fun MessagePublisher.sendAnswerPart(
+ tabId: String,
+ message: String? = null,
+ canBeVoted: Boolean? = null,
+) {
+ this.sendAnswer(
+ tabId = tabId,
+ message = message,
+ messageType = DocMessageType.AnswerPart,
+ canBeVoted = canBeVoted
+ )
+}
+
+suspend fun MessagePublisher.sendSystemPrompt(
+ tabId: String,
+ followUp: List,
+) {
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.SystemPrompt,
+ followUp = followUp
+ )
+}
+
+suspend fun MessagePublisher.updateFileComponent(tabId: String, filePaths: List, deletedFiles: List, messageId: String) {
+ val fileComponentMessage = FileComponent(
+ tabId = tabId,
+ filePaths = filePaths,
+ deletedFiles = deletedFiles,
+ messageId = messageId,
+ )
+ this.publish(fileComponentMessage)
+}
+
+suspend fun MessagePublisher.sendAsyncEventProgress(tabId: String, inProgress: Boolean, message: String? = null) {
+ val asyncEventProgressMessage = AsyncEventProgressMessage(
+ tabId = tabId,
+ message = message,
+ inProgress = inProgress,
+ )
+ this.publish(asyncEventProgressMessage)
+}
+
+suspend fun MessagePublisher.sendUpdatePlaceholder(tabId: String, newPlaceholder: String) {
+ val updatePlaceholderMessage = UpdatePlaceholderMessage(
+ tabId = tabId,
+ newPlaceholder = newPlaceholder
+ )
+ this.publish(updatePlaceholderMessage)
+}
+
+suspend fun MessagePublisher.sendAuthNeededException(tabId: String, triggerId: String, credentialState: AuthNeededState) {
+ val message = AuthNeededException(
+ tabId = tabId,
+ triggerId = triggerId,
+ authType = credentialState.authType,
+ message = credentialState.message,
+ )
+ this.publish(message)
+}
+
+suspend fun MessagePublisher.sendAuthenticationInProgressMessage(tabId: String) {
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = message("amazonqFeatureDev.follow_instructions_for_authentication")
+ )
+}
+suspend fun MessagePublisher.sendChatInputEnabledMessage(tabId: String, enabled: Boolean) {
+ val chatInputEnabledMessage = ChatInputEnabledMessage(
+ tabId,
+ enabled,
+ )
+ this.publish(chatInputEnabledMessage)
+}
+
+suspend fun MessagePublisher.sendError(tabId: String, errMessage: String?, retries: Int, conversationId: String? = null, showDefaultMessage: Boolean? = false) {
+ val conversationIdText = if (conversationId == null) "" else "\n\nConversation ID: **$conversationId**"
+
+ if (retries == 0) {
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = if (showDefaultMessage == true) errMessage else message("amazonqFeatureDev.no_retries.error_text") + conversationIdText,
+ )
+
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.SystemPrompt,
+ )
+ return
+ }
+
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = errMessage + conversationIdText,
+ )
+
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.SystemPrompt,
+ followUp = listOf(
+ FollowUp(
+ pillText = message("amazonqFeatureDev.follow_up.retry"),
+ type = FollowUpTypes.RETRY,
+ status = FollowUpStatusType.Warning
+ )
+ ),
+ )
+}
+
+suspend fun MessagePublisher.sendMonthlyLimitError(tabId: String) {
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = message("amazonqFeatureDev.exception.monthly_limit_error")
+ )
+ this.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_monthly_limit"))
+}
+
+suspend fun MessagePublisher.initialExamples(tabId: String) {
+ this.sendAnswer(
+ tabId = tabId,
+ messageType = DocMessageType.Answer,
+ message = message("amazonqFeatureDev.example_text"),
+ )
+}
+
+suspend fun MessagePublisher.sendCodeResult(
+ tabId: String,
+ uploadId: String,
+ filePaths: List,
+ deletedFiles: List,
+ references: List,
+) {
+ val refs = references.map { ref ->
+ CodeReference(
+ licenseName = ref.licenseName,
+ repository = ref.repository,
+ url = ref.url,
+ recommendationContentSpan = RecommendationContentSpan(
+ ref.recommendationContentSpan?.start ?: 0,
+ ref.recommendationContentSpan?.end ?: 0,
+ ),
+ information = "Reference code under **${ref.licenseName}** license from repository [${ref.repository}](${ref.url})"
+ )
+ }
+
+ this.publish(
+ CodeResultMessage(
+ tabId = tabId,
+ conversationId = uploadId,
+ filePaths = filePaths,
+ deletedFiles = deletedFiles,
+ references = refs
+ )
+ )
+}
+
+suspend fun MessagePublisher.sendFolderConfirmationMessage(
+ tabId: String,
+ message: String,
+ folderPath: String,
+ followUps: List,
+) {
+ this.publish(
+ FolderConfirmationMessage(tabId = tabId, folderPath = folderPath, message = message, followUps = followUps)
+ )
+}
+
+suspend fun MessagePublisher.sendUpdatePromptProgress(tabId: String, progressField: ProgressField?) {
+ this.publish(
+ PromptProgressMessage(tabId, progressField)
+ )
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocGenerationState.kt
new file mode 100644
index 0000000000..81fa4b8819
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocGenerationState.kt
@@ -0,0 +1,288 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.session
+
+import kotlinx.coroutines.delay
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeGenerationWorkflowStatus
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.common.session.Intent
+import software.aws.toolkits.jetbrains.common.session.SessionState
+import software.aws.toolkits.jetbrains.common.session.SessionStateConfig
+import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
+import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_NAME
+import software.aws.toolkits.jetbrains.services.amazonqDoc.controller.DocGenerationStep
+import software.aws.toolkits.jetbrains.services.amazonqDoc.controller.Mode
+import software.aws.toolkits.jetbrains.services.amazonqDoc.controller.docGenerationProgressMessage
+import software.aws.toolkits.jetbrains.services.amazonqDoc.docServiceError
+import software.aws.toolkits.jetbrains.services.amazonqDoc.inProgress
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.DocMessageType
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUp
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpTypes
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAnswer
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAnswerPart
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendUpdatePromptProgress
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeGenerationResult
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Interaction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStateAction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.registerDeletedFiles
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.registerNewFiles
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
+import software.aws.toolkits.resources.message
+import software.aws.toolkits.telemetry.AmazonqTelemetry
+import software.aws.toolkits.telemetry.MetricResult
+
+private val logger = getLogger()
+
+class DocGenerationState(
+ override val tabID: String,
+ override var approach: String,
+ val config: SessionStateConfig,
+ val uploadId: String,
+ val currentIteration: Int,
+ val repositorySize: Double,
+ val messenger: MessagePublisher,
+ var codeGenerationRemainingIterationCount: Int? = null,
+ var codeGenerationTotalIterationCount: Int? = null,
+ override val phase: SessionStatePhase?,
+ override var token: CancellationTokenSource?,
+) : SessionState {
+ override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
+ val startTime = System.currentTimeMillis()
+ var result: MetricResult = MetricResult.Succeeded
+ var failureReason: String? = null
+ var codeGenerationWorkflowStatus: CodeGenerationWorkflowStatus = CodeGenerationWorkflowStatus.COMPLETE
+ var numberOfReferencesGenerated: Int? = null
+ var numberOfFilesGenerated: Int? = null
+ try {
+ val response = config.amazonQCodeGenService.startTaskAssistCodeGeneration(
+ conversationId = config.conversationId,
+ uploadId = uploadId,
+ message = action.msg,
+ intent = Intent.DOC
+ )
+
+ val codeGenerationResult = generateCode(codeGenerationId = response.codeGenerationId(), token)
+ numberOfReferencesGenerated = codeGenerationResult.references.size
+ numberOfFilesGenerated = codeGenerationResult.newFiles.size
+ codeGenerationRemainingIterationCount = codeGenerationResult.codeGenerationRemainingIterationCount
+ codeGenerationTotalIterationCount = codeGenerationResult.codeGenerationTotalIterationCount
+
+ val nextState = PrepareDocGenerationState(
+ tabID = tabID,
+ approach = approach,
+ config = config,
+ filePaths = codeGenerationResult.newFiles,
+ deletedFiles = codeGenerationResult.deletedFiles,
+ references = codeGenerationResult.references,
+ currentIteration = currentIteration + 1,
+ uploadId = uploadId,
+ messenger = messenger,
+ codeGenerationRemainingIterationCount = codeGenerationRemainingIterationCount,
+ codeGenerationTotalIterationCount = codeGenerationTotalIterationCount,
+ token = token
+ )
+
+ // It is not needed to interact right away with the PrepareCodeGeneration.
+ // returns therefore a SessionStateInteraction object to be handled by the controller.
+ return SessionStateInteraction(
+ nextState = nextState,
+ interaction = Interaction(content = "", interactionSucceeded = true)
+ )
+ } catch (e: Exception) {
+ logger.warn(e) { "$FEATURE_NAME: Code generation failed: ${e.message}" }
+ result = MetricResult.Failed
+ failureReason = e.javaClass.simpleName
+ codeGenerationWorkflowStatus = CodeGenerationWorkflowStatus.FAILED
+
+ throw e
+ } finally {
+ AmazonqTelemetry.codeGenerationInvoke(
+ amazonqConversationId = config.conversationId,
+ amazonqCodeGenerationResult = codeGenerationWorkflowStatus.toString(),
+ amazonqGenerateCodeIteration = currentIteration.toDouble(),
+ amazonqNumberOfReferences = numberOfReferencesGenerated?.toDouble(),
+ amazonqGenerateCodeResponseLatency = (System.currentTimeMillis() - startTime).toDouble(),
+ amazonqNumberOfFilesGenerated = numberOfFilesGenerated?.toDouble(),
+ amazonqRepositorySize = repositorySize,
+ result = result,
+ reason = failureReason,
+ duration = (System.currentTimeMillis() - startTime).toDouble(),
+ credentialStartUrl = getStartUrl(config.amazonQCodeGenService.project)
+ )
+ }
+ }
+}
+
+fun getFileSummaryPercentage(input: String): Double {
+ // Split the input string by newline characters
+ val lines = input.split("\n")
+
+ // Find the line containing "summarized:"
+ val summaryLine = lines.find { it.contains("summarized:") }
+
+ // If the line is not found, return -1.0
+ if (summaryLine == null) {
+ return -1.0
+ }
+
+ // Extract the numbers from the summary line
+ val (summarized, total) = summaryLine.split(":")[1].trim().split(" of ").map { it.toDouble() }
+
+ // Calculate the percentage
+ val percentage = (summarized / total) * 100
+
+ return percentage
+}
+
+private suspend fun DocGenerationState.generateCode(codeGenerationId: String, token: CancellationTokenSource?): CodeGenerationResult {
+ val pollCount = 180
+ val requestDelay = 10000L
+
+ repeat(pollCount) {
+ if (token?.token?.isCancellationRequested() == true) {
+ // This should be switched to newTask or something. Looks different than previously and may need to clean up previous run
+ messenger.sendUpdatePromptProgress(tabId = tabID, null)
+ messenger.sendAnswer(
+ messageType = DocMessageType.SystemPrompt,
+ tabId = tabID,
+ followUp = listOf(
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.create"),
+ prompt = message("amazonqDoc.prompt.create"),
+ type = FollowUpTypes.CREATE_DOCUMENTATION,
+ ),
+ FollowUp(
+ pillText = message("amazonqDoc.prompt.update"),
+ prompt = message("amazonqDoc.prompt.update"),
+ type = FollowUpTypes.UPDATE_DOCUMENTATION,
+ )
+ )
+ )
+ return CodeGenerationResult(emptyList(), emptyList(), emptyList())
+ }
+
+ val codeGenerationResultState = config.amazonQCodeGenService.getTaskAssistCodeGeneration(
+ conversationId = config.conversationId,
+ codeGenerationId = codeGenerationId,
+ )
+
+ when (codeGenerationResultState.codeGenerationStatus().status()) {
+ CodeGenerationWorkflowStatus.COMPLETE -> {
+ val codeGenerationStreamResult = config.amazonQCodeGenService.exportTaskAssistArchiveResult(
+ conversationId = config.conversationId
+ )
+
+ val newFileInfo = registerNewFiles(newFileContents = codeGenerationStreamResult.newFileContents)
+ val deletedFileInfo = registerDeletedFiles(deletedFiles = codeGenerationStreamResult.deletedFiles.orEmpty())
+
+ messenger.sendUpdatePromptProgress(tabId = tabID, progressField = null)
+
+ return CodeGenerationResult(
+ newFiles = newFileInfo,
+ deletedFiles = deletedFileInfo,
+ references = codeGenerationStreamResult.references,
+ codeGenerationRemainingIterationCount = codeGenerationResultState.codeGenerationRemainingIterationCount(),
+ codeGenerationTotalIterationCount = codeGenerationResultState.codeGenerationTotalIterationCount()
+ )
+ }
+
+ CodeGenerationWorkflowStatus.IN_PROGRESS -> {
+ if (codeGenerationResultState.codeGenerationStatusDetail() != null) {
+ val progress = getFileSummaryPercentage(codeGenerationResultState.codeGenerationStatusDetail())
+
+ messenger.sendUpdatePromptProgress(
+ tabID,
+ inProgress(
+ progress.toInt(),
+ message(if (progress >= 100) "amazonqDoc.inprogress_message.generating" else "amazonqDoc.progress_message.summarizing")
+ )
+ )
+
+ messenger.sendAnswerPart(
+ tabId = tabID,
+ message = docGenerationProgressMessage(
+ if (progress < 100) {
+ if (progress < 20) {
+ DocGenerationStep.CREATE_KNOWLEDGE_GRAPH
+ } else {
+ DocGenerationStep.SUMMARIZING_FILES
+ }
+ } else {
+ DocGenerationStep.GENERATING_ARTIFACTS
+ },
+ mode = Mode.CREATE
+ )
+ )
+ }
+
+ delay(requestDelay)
+ }
+
+ CodeGenerationWorkflowStatus.FAILED -> {
+ messenger.sendUpdatePromptProgress(tabId = tabID, progressField = null)
+
+ when (true) {
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "README_TOO_LARGE"
+ ),
+ -> docServiceError(message("amazonqDoc.exception.readme_too_large"))
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "WORKSPACE_TOO_LARGE"
+ ),
+ -> docServiceError(message("amazonqDoc.exception.content_length_error"))
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "WORKSPACE_EMPTY"
+ ),
+ -> docServiceError(message("amazonqDoc.exception.workspace_empty"))
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "PROMPT_UNRELATED"
+ ),
+ -> docServiceError(message("amazonqDoc.exception.prompt_unrelated"))
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "PROMPT_TOO_VAGUE"
+ ),
+ -> docServiceError(message("amazonqDoc.exception.prompt_too_vague"))
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "PromptRefusal"
+ ),
+ -> docServiceError(message("amazonqFeatureDev.exception.prompt_refusal"))
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "Guardrails"
+ ),
+ -> docServiceError(message("amazonqDoc.error_text"))
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "EmptyPatch"
+ ),
+ -> {
+ if (codeGenerationResultState.codeGenerationStatusDetail()?.contains("NO_CHANGE_REQUIRED") == true) {
+ docServiceError(message("amazonqDoc.exception.no_change_required"))
+ }
+ docServiceError(message("amazonqDoc.error_text"))
+ }
+
+ codeGenerationResultState.codeGenerationStatusDetail()?.contains(
+ "Throttling"
+ ),
+ -> docServiceError(message("amazonqFeatureDev.exception.throttling"))
+
+ else -> docServiceError(message("amazonqDoc.error_text"))
+ }
+ }
+
+ else -> error("Unknown status: ${codeGenerationResultState.codeGenerationStatus().status()}")
+ }
+ }
+
+ return CodeGenerationResult(emptyList(), emptyList(), emptyList())
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocSession.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocSession.kt
new file mode 100644
index 0000000000..c34ccb1589
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocSession.kt
@@ -0,0 +1,235 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.session
+
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VfsUtil
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationEvent
+import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationInteractionType
+import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryEventResponse
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.common.clients.AmazonQCodeGenerateClient
+import software.aws.toolkits.jetbrains.common.session.ConversationNotStartedState
+import software.aws.toolkits.jetbrains.common.session.SessionState
+import software.aws.toolkits.jetbrains.common.session.SessionStateConfigData
+import software.aws.toolkits.jetbrains.common.util.AmazonQCodeGenService
+import software.aws.toolkits.jetbrains.common.util.getDiffCharsAndLines
+import software.aws.toolkits.jetbrains.common.util.resolveAndCreateOrUpdateFile
+import software.aws.toolkits.jetbrains.common.util.resolveAndDeleteFile
+import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
+import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
+import software.aws.toolkits.jetbrains.services.amazonqDoc.CODE_GENERATION_RETRY_LIMIT
+import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_NAME
+import software.aws.toolkits.jetbrains.services.amazonqDoc.MAX_PROJECT_SIZE_BYTES
+import software.aws.toolkits.jetbrains.services.amazonqDoc.conversationIdNotFound
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.sendAsyncEventProgress
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Interaction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStateAction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
+
+private val logger = getLogger()
+
+class DocSession(val tabID: String, val project: Project) {
+ var context: FeatureDevSessionContext
+ val sessionStartTime = System.currentTimeMillis()
+
+ var state: SessionState?
+ var preloaderFinished: Boolean = false
+ var localConversationId: String? = null
+ var localLatestMessage: String = ""
+ var task: String = ""
+ val proxyClient: AmazonQCodeGenerateClient
+ val amazonQCodeGenService: AmazonQCodeGenService
+
+ // retry session state vars
+ private var codegenRetries: Int
+
+ // Used to keep track of whether the current session/tab is currently authenticating/needs authenticating
+ var isAuthenticating: Boolean
+
+ init {
+ context = FeatureDevSessionContext(project, MAX_PROJECT_SIZE_BYTES)
+ proxyClient = AmazonQCodeGenerateClient.getInstance(project)
+ amazonQCodeGenService = AmazonQCodeGenService(proxyClient, project)
+ state = ConversationNotStartedState("", tabID, token = null)
+ isAuthenticating = false
+ codegenRetries = CODE_GENERATION_RETRY_LIMIT
+ }
+
+ fun conversationIDLog(conversationId: String) = "$FEATURE_NAME Conversation ID: $conversationId"
+
+ /**
+ * Preload any events that have to run before a chat message can be sent
+ */
+ suspend fun preloader(msg: String, messenger: MessagePublisher) {
+ if (!preloaderFinished) {
+ setupConversation(msg, messenger)
+ preloaderFinished = true
+ messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
+ }
+ }
+
+ /**
+ * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
+ */
+ fun setupConversation(msg: String, messenger: MessagePublisher) {
+ // Store the initial message when setting up the conversation so that if it fails we can retry with this message
+ localLatestMessage = msg
+
+ localConversationId = amazonQCodeGenService.createConversation()
+ logger().info(conversationIDLog(this.conversationId))
+
+ val sessionStateConfig = getSessionStateConfig().copy(conversationId = this.conversationId)
+ state = PrepareDocGenerationState(
+ tabID = sessionState.tabID,
+ approach = sessionState.approach,
+ config = sessionStateConfig,
+ filePaths = emptyList(),
+ deletedFiles = emptyList(),
+ references = emptyList(),
+ currentIteration = 0, // first code gen iteration
+ uploadId = "", // There is no code gen uploadId so far
+ messenger = messenger,
+ token = CancellationTokenSource()
+ )
+ }
+
+ /**
+ * Triggered by the Insert code follow-up button to apply code changes.
+ */
+ fun insertChanges(filePaths: List, deletedFiles: List) {
+ val selectedSourceFolder = context.selectedSourceFolder.toNioPath()
+
+ filePaths.forEach { resolveAndCreateOrUpdateFile(selectedSourceFolder, it.zipFilePath, it.fileContent) }
+
+ deletedFiles.forEach { resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath) }
+
+ // Taken from https://intellij-support.jetbrains.com/hc/en-us/community/posts/206118439-Refresh-after-external-changes-to-project-structure-and-sources
+ VfsUtil.markDirtyAndRefresh(true, true, true, context.selectedSourceFolder)
+ }
+
+ data class AddedContent(
+ val totalAddedChars: Int,
+ val totalAddedLines: Int,
+ val totalAddedFiles: Int,
+ )
+
+ fun countAddedContent(filePaths: List, interactionType: DocGenerationInteractionType? = null): AddedContent {
+ var totalAddedChars = 0
+ var totalAddedLines = 0
+ var totalAddedFiles = 0
+
+ filePaths.filter { !it.rejected }.forEach { filePath ->
+ val existingFile = VfsUtil.findRelativeFile(filePath.zipFilePath, context.selectedSourceFolder)
+ val content = filePath.fileContent
+ totalAddedFiles += 1
+
+ if (existingFile != null && interactionType == DocGenerationInteractionType.UPDATE_README) {
+ val existingContent = existingFile.content()
+ val (addedChars, addedLines) = getDiffCharsAndLines(existingContent, content)
+ totalAddedChars += addedChars
+ totalAddedLines += addedLines
+ } else {
+ totalAddedChars += content.length
+ totalAddedLines += content.split('\n').size
+ }
+ }
+
+ return AddedContent(
+ totalAddedChars = totalAddedChars,
+ totalAddedLines = totalAddedLines,
+ totalAddedFiles = totalAddedFiles
+ )
+ }
+
+ suspend fun send(msg: String): Interaction {
+ // When the task/"thing to do" hasn't been set yet, we want it to be the incoming message
+ if (task.isEmpty() && msg.isNotEmpty()) {
+ task = msg
+ }
+
+ localLatestMessage = msg
+
+ return nextInteraction(msg)
+ }
+
+ private suspend fun nextInteraction(msg: String): Interaction {
+ var action = SessionStateAction(
+ task = task,
+ msg = msg,
+ )
+
+ val resp = sessionState.interact(action)
+ if (resp.nextState != null) {
+ // Approach may have been changed after the interaction
+ val newApproach = sessionState.approach
+
+ // Move to the next state
+ state = resp.nextState
+
+ // If approach was changed then we need to set it in the next state and this state
+ sessionState.approach = newApproach
+ }
+
+ return resp.interaction
+ }
+
+ fun getSessionStateConfig(): SessionStateConfigData = SessionStateConfigData(
+ conversationId = this.conversationId,
+ repoContext = this.context,
+ amazonQCodeGenService = this.amazonQCodeGenService,
+ )
+
+ val conversationId: String
+ get() {
+ if (localConversationId == null) {
+ conversationIdNotFound()
+ } else {
+ return localConversationId as String
+ }
+ }
+
+ val conversationIdUnsafe: String?
+ get() = localConversationId
+
+ val sessionState: SessionState
+ get() {
+ if (state == null) {
+ throw Error("State should be initialized before it's read")
+ } else {
+ return state as SessionState
+ }
+ }
+
+ val latestMessage: String
+ get() = this.localLatestMessage
+
+ val retries: Int
+ get() = codegenRetries
+
+ fun decreaseRetries() {
+ codegenRetries -= 1
+ }
+
+ fun sendDocGenerationEvent(docGenerationEvent: DocGenerationEvent) {
+ val sendDocGenerationEventResponse: SendTelemetryEventResponse
+ try {
+ sendDocGenerationEventResponse = proxyClient.sendDocGenerationTelemetryEvent(docGenerationEvent)
+ val requestId = sendDocGenerationEventResponse.responseMetadata().requestId()
+ logger.debug {
+ "${FEATURE_NAME}: succesfully sent doc generation telemetry: ConversationId: $conversationId RequestId: $requestId"
+ }
+ } catch (e: Exception) {
+ logger.warn(e) { "${FEATURE_NAME}: failed to send doc generation telemetry" }
+ }
+ }
+
+ fun getUserIdentity(): String = proxyClient.connection().id
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt
new file mode 100644
index 0000000000..1949950d23
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt
@@ -0,0 +1,105 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.session
+
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.common.session.SessionState
+import software.aws.toolkits.jetbrains.common.session.SessionStateConfig
+import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
+import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_NAME
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStateAction
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.deleteUploadArtifact
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
+import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
+import software.aws.toolkits.telemetry.AmazonqTelemetry
+import software.aws.toolkits.telemetry.AmazonqUploadIntent
+import software.aws.toolkits.telemetry.Result
+
+private val logger = getLogger()
+
+class PrepareDocGenerationState(
+ override var tabID: String,
+ override var approach: String,
+ private var config: SessionStateConfig,
+ val filePaths: List,
+ val deletedFiles: List,
+ val references: List,
+ var uploadId: String,
+ private val currentIteration: Int,
+ private var messenger: MessagePublisher,
+ var codeGenerationRemainingIterationCount: Int? = null,
+ var codeGenerationTotalIterationCount: Int? = null,
+ override var token: CancellationTokenSource?,
+) : SessionState {
+ override val phase = SessionStatePhase.CODEGEN
+ override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
+ val startTime = System.currentTimeMillis()
+ var result: Result = Result.Succeeded
+ var failureReason: String? = null
+ var failureReasonDesc: String? = null
+ var zipFileLength: Long? = null
+ val nextState: SessionState
+ try {
+ val repoZipResult = config.repoContext.getProjectZip()
+ val zipFileChecksum = repoZipResult.checksum
+ zipFileLength = repoZipResult.contentLength
+ val fileToUpload = repoZipResult.payload
+
+ val uploadUrlResponse = config.amazonQCodeGenService.createUploadUrl(
+ config.conversationId,
+ zipFileChecksum,
+ zipFileLength,
+ uploadId
+ )
+
+ uploadArtifactToS3(
+ uploadUrlResponse.uploadUrl(),
+ fileToUpload,
+ zipFileChecksum,
+ zipFileLength,
+ uploadUrlResponse.kmsKeyArn()
+ )
+ deleteUploadArtifact(fileToUpload)
+
+ this.uploadId = uploadUrlResponse.uploadId()
+
+ nextState = DocGenerationState(
+ tabID = this.tabID,
+ approach = "", // No approach needed,
+ config = this.config,
+ uploadId = this.uploadId,
+ currentIteration = this.currentIteration,
+ repositorySize = zipFileLength.toDouble(),
+ messenger = messenger,
+ phase = phase,
+ token = this.token
+ )
+ } catch (e: Exception) {
+ result = Result.Failed
+ failureReason = e.javaClass.simpleName
+ failureReasonDesc = e.message
+ logger.warn(e) { "$FEATURE_NAME: Code uploading failed: ${e.message}" }
+ throw e
+ } finally {
+ AmazonqTelemetry.createUpload(
+ amazonqConversationId = config.conversationId,
+ amazonqRepositorySize = zipFileLength?.toDouble(),
+ amazonqUploadIntent = AmazonqUploadIntent.TASKASSISTPLANNING,
+ result = result,
+ reason = failureReason,
+ reasonDesc = failureReasonDesc,
+ duration = (System.currentTimeMillis() - startTime).toDouble(),
+ credentialStartUrl = getStartUrl(config.amazonQCodeGenService.project)
+ )
+ }
+ // It is essential to interact with the next state outside of try-catch block for the telemetry to capture events for the states separately
+ return nextState.interact(action)
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/SessionStateTypes.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/SessionStateTypes.kt
new file mode 100644
index 0000000000..f4821d9027
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/SessionStateTypes.kt
@@ -0,0 +1,27 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.session
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import software.aws.toolkits.jetbrains.common.session.SessionState
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Interaction
+
+data class SessionStateInteraction(
+ val nextState: T? = null,
+ val interaction: Interaction,
+)
+
+data class DocGenerationStreamResult(
+ @JsonProperty("new_file_contents")
+ var newFileContents: Map,
+ @JsonProperty("deleted_files")
+ var deletedFiles: List?,
+ var references: List,
+)
+
+data class ExportDocTaskAssistResultArchiveStreamResult(
+ @JsonProperty("code_generation_result")
+ var codeGenerationResult: DocGenerationStreamResult,
+)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt
new file mode 100644
index 0000000000..2344dac5c9
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt
@@ -0,0 +1,26 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.storage
+
+import com.intellij.openapi.project.Project
+import software.aws.toolkits.jetbrains.services.amazonqDoc.session.DocSession
+
+class ChatSessionStorage {
+ private val sessions = mutableMapOf()
+
+ private fun createSession(tabId: String, project: Project): DocSession {
+ val session = DocSession(tabId, project)
+ sessions[tabId] = session
+ return session
+ }
+
+ @Synchronized fun getSession(tabId: String, project: Project): DocSession = sessions[tabId] ?: createSession(tabId, project)
+
+ fun deleteSession(tabId: String) {
+ sessions.remove(tabId)
+ }
+
+ // Find all sessions that are currently waiting to be authenticated
+ fun getAuthenticatingSessions(): List = this.sessions.values.filter { it.isAuthenticating }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/util/DocControllerUtil.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/util/DocControllerUtil.kt
new file mode 100644
index 0000000000..95a7e8235e
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/util/DocControllerUtil.kt
@@ -0,0 +1,33 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonqDoc.util
+
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUp
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpIcons
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpStatusType
+import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpTypes
+import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
+import software.aws.toolkits.resources.message
+
+fun getFollowUpOptions(phase: SessionStatePhase?): List {
+ when (phase) {
+ SessionStatePhase.CODEGEN -> {
+ return listOf(
+ FollowUp(
+ pillText = message("amazonqDoc.follow_up.insert_code"),
+ type = FollowUpTypes.INSERT_CODE,
+ icon = FollowUpIcons.Ok,
+ status = FollowUpStatusType.Success
+ ),
+ FollowUp(
+ pillText = message("amazonqDoc.follow_up.provide_feedback_and_regenerate"),
+ type = FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE,
+ icon = FollowUpIcons.Refresh,
+ status = FollowUpStatusType.Info
+ )
+ )
+ }
+ else -> return emptyList()
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt
index bc5bcfc6a7..7169e39150 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt
@@ -11,6 +11,9 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
+import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.controller.FeatureDevController
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.AuthenticationUpdateMessage
@@ -62,7 +65,10 @@ class FeatureDevApp : AmazonQApp {
AuthenticationUpdateMessage(
featureDevEnabled = isFeatureDevAvailable(context.project),
codeTransformEnabled = isCodeTransformAvailable(context.project),
- authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabID }
+ codeScanEnabled = isCodeScanAvailable(context.project),
+ codeTestEnabled = isCodeTestAvailable(context.project),
+ docEnabled = isDocAvailable(context.project),
+ authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabID },
)
)
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt
index 9483b320bd..cdb0f6d6fa 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt
@@ -27,6 +27,7 @@ import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.common.util.selectFolder
import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
@@ -70,7 +71,6 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Sessio
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
-import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt
index be8be6d18a..77c6dc094d 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt
@@ -181,6 +181,9 @@ data class AuthenticationUpdateMessage(
val authenticatingTabIDs: List,
val featureDevEnabled: Boolean,
val codeTransformEnabled: Boolean,
+ val codeScanEnabled: Boolean,
+ val codeTestEnabled: Boolean,
+ val docEnabled: Boolean,
val message: String? = null,
val messageId: String = UUID.randomUUID().toString(),
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt
index f0aa2e8f41..9ce974bbef 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt
@@ -26,7 +26,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.readFileT
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AmazonqTelemetry
-import software.aws.toolkits.telemetry.Result
+import software.aws.toolkits.telemetry.MetricResult
import java.util.UUID
private val logger = getLogger()
@@ -47,9 +47,9 @@ class CodeGenerationState(
) : SessionState {
override val phase = SessionStatePhase.CODEGEN
- override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
+ override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
val startTime = System.currentTimeMillis()
- var result: Result = Result.Succeeded
+ var result: MetricResult = MetricResult.Succeeded
var failureReason: String? = null
var failureReasonDesc: String? = null
var codeGenerationWorkflowStatus: CodeGenerationWorkflowStatus = CodeGenerationWorkflowStatus.COMPLETE
@@ -145,7 +145,7 @@ class CodeGenerationState(
)
} catch (e: Exception) {
logger.warn(e) { "$FEATURE_NAME: Code generation failed: ${e.message}" }
- result = Result.Failed
+ result = MetricResult.Failed
failureReason = e.javaClass.simpleName
if (e is FeatureDevException) {
failureReason = e.reason()
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/ConversationNotStartedState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/ConversationNotStartedState.kt
index 2b687d144d..587fa02c91 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/ConversationNotStartedState.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/ConversationNotStartedState.kt
@@ -16,7 +16,7 @@ class ConversationNotStartedState(
) : SessionState {
override val phase = SessionStatePhase.INIT
- override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
+ override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
error("Illegal transition between states, restart the conversation")
}
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt
index 61cf36737b..41a2ee2079 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt
@@ -37,7 +37,7 @@ class PrepareCodeGenerationState(
override var diffMetricsProcessed: DiffMetricsProcessed,
) : SessionState {
override val phase = SessionStatePhase.CODEGEN
- override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
+ override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
val startTime = System.currentTimeMillis()
var result: Result = Result.Succeeded
var failureReason: String? = null
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt
index 7a2b42110e..1a431857ce 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt
@@ -6,6 +6,8 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
+import software.aws.toolkits.jetbrains.common.util.resolveAndCreateOrUpdateFile
+import software.aws.toolkits.jetbrains.common.util.resolveAndDeleteFile
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CODE_GENERATION_RETRY_LIMIT
@@ -21,8 +23,6 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDe
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getChangeIdentifier
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getDiffMetrics
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.readFileToString
-import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndCreateOrUpdateFile
-import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndDeleteFile
import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController
import java.util.HashSet
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionState.kt
index c1248a74dd..936b7a88d3 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionState.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionState.kt
@@ -14,5 +14,5 @@ interface SessionState {
var currentIteration: Int?
var approach: String
var diffMetricsProcessed: DiffMetricsProcessed
- suspend fun interact(action: SessionStateAction): SessionStateInteraction
+ suspend fun interact(action: SessionStateAction): SessionStateInteraction
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt
index 0567bf980a..a8fd7af3f8 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt
@@ -20,8 +20,8 @@ data class Interaction(
val interactionSucceeded: Boolean,
)
-data class SessionStateInteraction(
- val nextState: SessionState? = null,
+data class SessionStateInteraction(
+ val nextState: T? = null,
val interaction: Interaction,
)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Responses.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Responses.kt
index 2cb7878e7d..6a246ca45f 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Responses.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Responses.kt
@@ -23,6 +23,9 @@ enum class FollowUpType {
Generated,
StopCodeTransform,
NewCodeTransform,
+ CreateDocumentation,
+ NewCodeScan,
+ ViewDiff,
}
data class SuggestedFollowUp(
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/GenerateUnitTestsAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/GenerateUnitTestsAction.kt
index 2c43378d45..e48cbe4df3 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/GenerateUnitTestsAction.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/GenerateUnitTestsAction.kt
@@ -3,20 +3,4 @@
package software.aws.toolkits.jetbrains.services.cwc.commands
-import com.intellij.openapi.actionSystem.ActionUpdateThread
-import com.intellij.openapi.actionSystem.AnActionEvent
-import com.intellij.openapi.actionSystem.CommonDataKeys
-import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
-import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
-import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
-import software.aws.toolkits.jetbrains.core.credentials.sono.isInternalUser
-
-class GenerateUnitTestsAction : CustomAction(EditorContextCommand.GenerateUnitTests) {
- override fun getActionUpdateThread() = ActionUpdateThread.BGT
-
- override fun update(e: AnActionEvent) {
- val project = e.getData(CommonDataKeys.PROJECT) ?: return
- val connection = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
- e.presentation.isEnabledAndVisible = isInternalUser(connection?.startUrl)
- }
-}
+class GenerateUnitTestsAction : CustomAction(EditorContextCommand.GenerateUnitTests)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanCompleteAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanCompleteAction.kt
new file mode 100644
index 0000000000..41160fe348
--- /dev/null
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanCompleteAction.kt
@@ -0,0 +1,21 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions
+
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.CommonDataKeys
+import com.intellij.openapi.components.service
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanMessageListener
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.scanResultsKey
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.scanScopeKey
+
+class CodeScanCompleteAction : AnAction() {
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.getData(CommonDataKeys.PROJECT) ?: return
+ val result = e.getData(scanResultsKey)
+ val scope = e.getData(scanScopeKey) ?: return
+ service().onScanResult(result, scope, project)
+ }
+}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt
index f9039f9a06..4c034c3127 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt
@@ -41,6 +41,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.CHAT_IMPLICIT_PROJECT_CO
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthNeededState
+import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType
@@ -84,6 +85,12 @@ import software.aws.toolkits.telemetry.CwsprChatCommandType
import java.time.Instant
import java.util.UUID
+data class TestCommandMessage(
+ val sender: String = "codetest",
+ val command: String = "test",
+ val type: String = "addAnswer",
+) : AmazonQMessage
+
class ChatController private constructor(
private val context: AmazonQAppInitContext,
private val chatSessionStorage: ChatSessionStorage,
@@ -158,7 +165,7 @@ class ChatController private constructor(
triggerId = triggerId,
message = prompt,
activeFileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage),
- userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message.chatMessage, startUrl),
+ userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message.chatMessage),
TriggerType.Click,
projectContextQueryResult = queryResult,
shouldAddIndexInProgressMessage = shouldAddIndexInProgressMessage,
@@ -299,7 +306,7 @@ class ChatController private constructor(
}
override suspend fun processCodeScanIssueAction(message: CodeScanIssueActionMessage) {
- logger.info { "Code Scan Explain issue with Q message received for issue: ${message.issue["title"]}" }
+ logger.info { "Code Review Explain issue with Q message received for issue: ${message.issue["title"]}" }
// Extract context
val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.CodeScanButton)
val triggerId = UUID.randomUUID().toString()
@@ -337,15 +344,15 @@ class ChatController private constructor(
)
return
}
-
- // Create prompt
- val prompt = if (EditorContextCommand.GenerateUnitTests == message.command) {
- "${message.command.verb} the following part of my code for me: $codeSelection"
+ if (message.command == EditorContextCommand.GenerateUnitTests) {
+ // Publish an event to "codetest" tab with command as "test" and type as "addAnswer"
+ val messageToPublish = TestCommandMessage()
+ context.messagesFromAppToUi.publish(messageToPublish)
} else {
- "${message.command} the following part of my code for me: $codeSelection"
+ // Create prompt
+ val prompt = "${message.command} the following part of my code for me: $codeSelection"
+ processPromptActions(prompt, message, triggerId, fileContext)
}
-
- processPromptActions(prompt, message, triggerId, fileContext)
}
private suspend fun processPromptActions(
@@ -385,7 +392,11 @@ class ChatController private constructor(
}
override suspend fun processLinkClick(message: IncomingCwcMessage.ClickedLink) {
- BrowserUtil.browse(message.link)
+ processLinkClick(message, message.link)
+ }
+
+ private suspend fun processLinkClick(message: IncomingCwcMessage, link: String) {
+ BrowserUtil.browse(link)
telemetryHelper.recordInteractWithMessage(message)
}
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 39725dfc46..6d910aa729 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
@@ -4,7 +4,6 @@
package software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent
import software.amazon.awssdk.services.codewhispererstreaming.model.UserIntent
-import software.aws.toolkits.jetbrains.core.credentials.sono.isInternalUser
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.FollowUpType
@@ -21,12 +20,12 @@ class UserIntentRecognizer {
EditorContextCommand.SendToPrompt -> null
}
- fun getUserIntentFromPromptChatMessage(prompt: String, startUrl: String?) = when {
+ fun getUserIntentFromPromptChatMessage(prompt: String) = when {
prompt.startsWith("Explain") -> UserIntent.EXPLAIN_CODE_SELECTION
prompt.startsWith("Refactor") -> UserIntent.SUGGEST_ALTERNATE_IMPLEMENTATION
prompt.startsWith("Fix") -> UserIntent.APPLY_COMMON_BEST_PRACTICES
prompt.startsWith("Optimize") -> UserIntent.IMPROVE_CODE
- prompt.startsWith("Generate unit tests") && isInternalUser(startUrl) -> UserIntent.GENERATE_UNIT_TESTS
+ prompt.startsWith("Generate unit tests") -> UserIntent.GENERATE_UNIT_TESTS
else -> null
}
@@ -41,6 +40,9 @@ class UserIntentRecognizer {
FollowUpType.Generated -> null
FollowUpType.StopCodeTransform -> null
FollowUpType.NewCodeTransform -> null
+ FollowUpType.CreateDocumentation -> null
+ FollowUpType.NewCodeScan -> null
+ FollowUpType.ViewDiff -> UserIntent.GENERATE_UNIT_TESTS
}
fun getUserIntentFromOnboardingPageInteraction(interaction: OnboardingPageInteraction) = when (interaction.type) {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt
index 22a3dd4640..bcadef007a 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt
@@ -630,7 +630,7 @@ class InlineChatController(
tabId = tabId,
message = message,
activeFileContext = fileContext,
- userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message, null),
+ userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message),
triggerType = TriggerType.Inline,
customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(project),
relevantTextDocuments = emptyList(),
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt
index 958db5076b..85b3075c96 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt
@@ -125,6 +125,10 @@ sealed interface IncomingCwcMessage : CwcMessage {
val type: FocusType,
) : IncomingCwcMessage
+ data class OpenUserGuide(
+ val userGuideLink: String,
+ ) : IncomingCwcMessage
+
data class ClickedLink(
@JsonProperty("command") val type: LinkType,
@JsonProperty("tabID") override val tabId: String,
@@ -200,6 +204,7 @@ data class FollowUp(
val type: FollowUpType,
val pillText: String,
val prompt: String,
+ val status: String? = null,
)
data class Suggestion(
diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt
index 3a3c2025d3..cf8f69f84b 100644
--- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt
+++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt
@@ -30,6 +30,7 @@ import org.mockito.kotlin.reset
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.whenever
+import software.aws.toolkits.jetbrains.common.util.selectFolder
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
@@ -62,7 +63,6 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.Cancellat
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.InsertAction
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
-import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AmazonqTelemetry
@@ -570,7 +570,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
whenever(featureDevClient.sendFeatureDevTelemetryEvent(any())).thenReturn(exampleSendTelemetryEventResponse)
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
- mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
+ mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt")
every { selectFolder(any(), any()) } returns null
spySession.preloader(userMessage, messenger)
@@ -601,7 +601,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
whenever(featureDevClient.sendFeatureDevTelemetryEvent(any())).thenReturn(exampleSendTelemetryEventResponse)
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
- mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
+ mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt")
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")
spySession.preloader(userMessage, messenger)
@@ -638,7 +638,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
val folder = LightVirtualFile("${spySession.context.projectRoot.name}/path/to/sub/folder")
- mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
+ mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt")
every { selectFolder(any(), any()) } returns folder
spySession.preloader(userMessage, messenger)
diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt
index 070abddb97..ca05443e64 100644
--- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt
+++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt
@@ -23,11 +23,11 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import software.aws.toolkits.jetbrains.common.util.resolveAndCreateOrUpdateFile
+import software.aws.toolkits.jetbrains.common.util.resolveAndDeleteFile
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevTestBase
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.clients.FeatureDevClient
-import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndCreateOrUpdateFile
-import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndDeleteFile
import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController
import kotlin.io.path.Path
@@ -72,7 +72,7 @@ class SessionTest : FeatureDevTestBase() {
mockkObject(ReferenceLogController)
every { ReferenceLogController.addReferenceLog(any(), any()) } just runs
- mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
+ mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt")
every { resolveAndDeleteFile(any(), any()) } just runs
every { resolveAndCreateOrUpdateFile(any(), any(), any()) } just runs
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt
index 22e0a1a2dd..1c32f2f07c 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt
@@ -15,6 +15,9 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
+import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
+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.commands.CodeTransformActionMessage
import software.aws.toolkits.jetbrains.services.codemodernizer.commands.CodeTransformMessageListener
@@ -106,7 +109,10 @@ class CodeTransformChatApp : AmazonQApp {
AuthenticationUpdateMessage(
featureDevEnabled = isFeatureDevAvailable(context.project),
codeTransformEnabled = isCodeTransformAvailable(context.project),
- authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabId }
+ codeScanEnabled = isCodeScanAvailable(context.project),
+ codeTestEnabled = isCodeTestAvailable(context.project),
+ docEnabled = isDocAvailable(context.project),
+ authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabId },
)
)
@@ -164,6 +170,7 @@ class CodeTransformChatApp : AmazonQApp {
is IncomingCodeTransformMessage.CodeTransformSelectSQLMetadata -> inboundAppMessagesHandler.processCodeTransformSelectSQLMetadataAction(message)
is IncomingCodeTransformMessage.CodeTransformSelectSQLModuleSchema ->
inboundAppMessagesHandler.processCodeTransformSelectSQLModuleSchemaAction(message)
+
is IncomingCodeTransformMessage.CodeTransformCancel -> inboundAppMessagesHandler.processCodeTransformCancelAction(message)
is IncomingCodeTransformMessage.CodeTransformStop -> inboundAppMessagesHandler.processCodeTransformStopAction(message.tabId)
is IncomingCodeTransformMessage.ChatPrompt -> inboundAppMessagesHandler.processChatPromptMessage(message)
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt
index 96ca9db9cd..9a32e82aff 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt
@@ -189,7 +189,10 @@ data class AuthenticationUpdateMessage(
val authenticatingTabIDs: List,
val featureDevEnabled: Boolean,
val codeTransformEnabled: Boolean,
+ val codeScanEnabled: Boolean,
val message: String? = null,
+ val codeTestEnabled: Boolean,
+ val docEnabled: Boolean,
) : CodeTransformUiMessage(
null,
type = "authenticationUpdateMessage",
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/it/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererIntegrationTestBase.kt b/plugins/amazonq/codewhisperer/jetbrains-community/it/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererIntegrationTestBase.kt
index 779efabcea..5ff82bdae5 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/it/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererIntegrationTestBase.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/it/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererIntegrationTestBase.kt
@@ -98,7 +98,8 @@ open class CodeWhispererIntegrationTestBase(val projectRule: CodeInsightTestFixt
)
scanManager = spy(CodeWhispererCodeScanManager.getInstance(projectRule.project))
- doNothing().whenever(scanManager).addCodeScanUI(any())
+ doNothing().whenever(scanManager).buildCodeScanUI()
+ doNothing().whenever(scanManager).showCodeScanUI()
projectRule.project.replaceService(CodeWhispererCodeScanManager::class.java, scanManager, disposableRule.disposable)
telemetryServiceSpy = spy(CodeWhispererTelemetryService.getInstance())
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 0c3e05ea1f..e58ad7d9f1 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
@@ -38,6 +38,7 @@
+
@@ -111,13 +112,12 @@
+
-
-
+
+
+
+
+ class="software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions.ExplainCodeIssueAction"/>
+
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/AmazonQCodeFixSession.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/AmazonQCodeFixSession.kt
new file mode 100644
index 0000000000..f733da6aef
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/AmazonQCodeFixSession.kt
@@ -0,0 +1,235 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.codescan
+import com.intellij.openapi.project.Project
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.withTimeout
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeFixJobStatus
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeFixUploadContext
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.Position
+import software.amazon.awssdk.services.codewhispererruntime.model.Range
+import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeFixJobRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeFixJobResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext
+import software.amazon.awssdk.services.codewhispererruntime.model.UploadIntent
+import software.aws.toolkits.core.utils.createTemporaryZipFile
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.core.utils.putNextEntry
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererZipUploadManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.getTelemetryErrorMessage
+import software.aws.toolkits.resources.message
+import java.io.File
+import java.nio.file.Path
+import java.time.Instant
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.io.path.pathString
+
+class AmazonQCodeFixSession(val project: Project) {
+ private fun now() = Instant.now().toEpochMilli()
+
+ private val clientAdaptor get() = CodeWhispererClientAdaptor.getInstance(project)
+
+ suspend fun runCodeFixWorkflow(issue: CodeWhispererCodeScanIssue): CodeFixResponse {
+ val currentCoroutineContext = coroutineContext
+
+ try {
+ currentCoroutineContext.ensureActive()
+
+ /**
+ * * Step 1: Generate zip
+ * */
+ val sourceZip = withTimeout(CodeWhispererConstants.CODE_FIX_CREATE_PAYLOAD_TIMEOUT_IN_SECONDS) {
+ zipFile(issue.file.toNioPath())
+ }
+
+ /**
+ * * Step 2: Create upload URL and upload the zip
+ * */
+
+ currentCoroutineContext.ensureActive()
+ val codeFixName = issue.findingId
+ if (!sourceZip.exists()) {
+ return CodeFixResponse(
+ getCodeFixJobResponse = null,
+ failureResponse = message("codewhisperer.codefix.invalid_zip_error")
+ )
+ }
+
+ val sourceZipUploadResponse =
+ CodeWhispererZipUploadManager.getInstance(project).createUploadUrlAndUpload(
+ sourceZip,
+ "SourceCode",
+ CodeWhispererConstants.UploadTaskType.CODE_FIX,
+ codeFixName
+ )
+
+ /**
+ * * Step 3: Create code fix
+ * */
+ currentCoroutineContext.ensureActive()
+ val issueRange = Range.builder().start(
+ Position.builder()
+ .line(issue.startLine)
+ .character(0)
+ .build()
+ )
+ .end(
+ Position.builder()
+ .line(issue.endLine)
+ .character(0)
+ .build()
+ )
+ .build()
+
+ val createCodeFixResponse = createCodeFixJob(
+ sourceZipUploadResponse.uploadId(),
+ issueRange,
+ issue.description.toString(),
+ codeFixName,
+ issue.ruleId
+ )
+ val codeFixJobId = createCodeFixResponse.jobId()
+ LOG.info { "Code fix job created: $codeFixJobId" }
+ /**
+ * * Step 4: polling for code fix
+ */
+ currentCoroutineContext.ensureActive()
+ val jobStatus = pollCodeFixJobStatus(createCodeFixResponse.jobId(), currentCoroutineContext)
+ if (jobStatus == CodeFixJobStatus.FAILED) {
+ LOG.debug { "Code fix generation failed." }
+ return CodeFixResponse(
+ getCodeFixJobResponse = null,
+ failureResponse = message("codewhisperer.codefix.create_code_fix_error")
+ )
+ }
+ /**
+ * Step 5: Process and render code fix results
+ */
+ currentCoroutineContext.ensureActive()
+ LOG.debug { "Code fix job succeeded and start processing result." }
+ val getCodeFixJobResponse = getCodeFixJob(createCodeFixResponse.jobId())
+ return CodeFixResponse(
+ getCodeFixJobResponse = getCodeFixJobResponse,
+ failureResponse = null,
+ jobId = codeFixJobId
+ )
+ } catch (e: Exception) {
+ LOG.error(e) { "Code scan session failed: ${e.message}" }
+ val timeoutMessage = message("codewhisperer.codefix.code_fix_job_timed_out")
+ return CodeFixResponse(
+ getCodeFixJobResponse = null,
+ failureResponse = when {
+ e is CodeWhispererCodeFixException && e.message == timeoutMessage -> timeoutMessage
+ else -> message("codewhisperer.codefix.create_code_fix_error")
+ }
+ )
+ }
+ }
+
+ fun createUploadUrl(md5Content: String, artifactType: String, codeFixName: String): CreateUploadUrlResponse = try {
+ clientAdaptor.createUploadUrl(
+ CreateUploadUrlRequest.builder()
+ .contentMd5(md5Content)
+ .artifactType(artifactType)
+ .uploadIntent(UploadIntent.CODE_FIX_GENERATION)
+ .uploadContext(UploadContext.fromCodeFixUploadContext(CodeFixUploadContext.builder().codeFixName(codeFixName).build()))
+ .build()
+ )
+ } catch (e: Exception) {
+ LOG.debug { "Create Upload URL failed: ${e.message}" }
+ val errorMessage = getTelemetryErrorMessage(e)
+ throw codeScanServerException("CreateUploadUrlException: $errorMessage")
+ }
+
+ private fun createCodeFixJob(
+ uploadId: String,
+ snippetRange: Range,
+ description: String,
+ codeFixName: String? = null,
+ ruleId: String? = null,
+ ): StartCodeFixJobResponse {
+ val request = StartCodeFixJobRequest.builder()
+ .uploadId(uploadId)
+ .snippetRange(snippetRange)
+ .codeFixName(codeFixName)
+ .ruleId(ruleId)
+ .description(description)
+ .build()
+
+ return try {
+ val response = clientAdaptor.startCodeFixJob(request)
+ LOG.debug {
+ "Code Fix Request id: ${response.responseMetadata().requestId()} " +
+ "and Code Fix Job id: ${response.jobId()}"
+ }
+ response
+ } catch (e: Exception) {
+ LOG.error { "Failed creating code fix job ${e.message}" }
+ throw CodeWhispererCodeFixException(message("codewhisperer.codefix.create_code_fix_error"))
+ }
+ }
+
+ private suspend fun pollCodeFixJobStatus(jobId: String, currentCoroutineContext: CoroutineContext): CodeFixJobStatus {
+ val pollingStartTime = now()
+ delay(CodeWhispererConstants.CODE_FIX_POLLING_INTERVAL_IN_SECONDS)
+ var status = CodeFixJobStatus.IN_PROGRESS
+ while (true) {
+ currentCoroutineContext.ensureActive()
+
+ val request = GetCodeFixJobRequest.builder()
+ .jobId(jobId)
+ .build()
+
+ val response = clientAdaptor.getCodeFixJob(request)
+ LOG.debug { "Request id: ${response.responseMetadata().requestId()}" }
+
+ if (response.jobStatus() != CodeFixJobStatus.IN_PROGRESS) {
+ status = response.jobStatus()
+ LOG.debug { "Code fix job status: ${status.name}" }
+ LOG.debug { "Complete polling code fix job status." }
+ break
+ }
+
+ currentCoroutineContext.ensureActive()
+ delay(CodeWhispererConstants.CODE_FIX_POLLING_INTERVAL_IN_SECONDS)
+
+ val elapsedTime = (now() - pollingStartTime) / CodeWhispererConstants.TOTAL_MILLIS_IN_SECOND // In seconds
+ if (elapsedTime > CodeWhispererConstants.CODE_FIX_TIMEOUT_IN_SECONDS) {
+ LOG.debug { "Code fix job status: ${status.name}" }
+ LOG.debug { "Code fix job failed. Amazon Q timed out." }
+ throw CodeWhispererCodeFixException(message("codewhisperer.codefix.code_fix_job_timed_out"))
+ }
+ }
+ return status
+ }
+
+ private fun getCodeFixJob(jobId: String): GetCodeFixJobResponse {
+ val response = clientAdaptor.getCodeFixJob(GetCodeFixJobRequest.builder().jobId(jobId).build())
+ return response
+ }
+ private fun zipFile(file: Path): File = createTemporaryZipFile {
+ try {
+ LOG.debug { "Selected file for truncation: $file" }
+ it.putNextEntry(file.toString(), file)
+ } catch (e: Exception) {
+ cannotFindFile("Zipping error: ${e.message}", file.pathString)
+ }
+ }.toFile()
+
+ companion object {
+ private val LOG = getLogger()
+ }
+ data class CodeFixResponse(val getCodeFixJobResponse: GetCodeFixJobResponse? = null, val failureResponse: String? = null, val jobId: String? = null)
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanException.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanException.kt
index 9407aaecf0..0380ca5fae 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanException.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanException.kt
@@ -7,6 +7,8 @@ import software.aws.toolkits.resources.message
open class CodeWhispererCodeScanException(override val message: String?) : RuntimeException()
+open class CodeWhispererCodeFixException(override val message: String?) : RuntimeException()
+
open class CodeWhispererCodeScanServerException(override val message: String?) : RuntimeException()
internal fun noFileOpenError(): Nothing =
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanHighlightingFilesPanel.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanHighlightingFilesPanel.kt
index 3bd8e425d9..59f6a7ad25 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanHighlightingFilesPanel.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanHighlightingFilesPanel.kt
@@ -31,7 +31,7 @@ internal class CodeWhispererCodeScanHighlightingFilesPanel(private val project:
init {
removeAll()
- val scannedFilesTreeNodeRoot = DefaultMutableTreeNode("CodeWhisperer scanned files for security scan")
+ val scannedFilesTreeNodeRoot = DefaultMutableTreeNode("Amazon Q reviewed files for code issues")
files.forEach {
scannedFilesTreeNodeRoot.add(DefaultMutableTreeNode(it))
}
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
new file mode 100644
index 0000000000..70bf1bbd0f
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanIssueDetailsPanel.kt
@@ -0,0 +1,306 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.codescan
+
+import com.intellij.icons.AllIcons
+import com.intellij.ide.BrowserUtil
+import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.runInEdt
+import com.intellij.openapi.editor.colors.EditorColorsManager
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileEditor.OpenFileDescriptor
+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.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.codeBlockBackgroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBorderColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockForegroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.deletionBackgroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.deletionForegroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.explainIssue
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.getCodeScanIssueDetailsHtml
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.getSeverityIcon
+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.CodeWhispererColorUtil.getHexString
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+import software.aws.toolkits.resources.message
+import software.aws.toolkits.telemetry.Component
+import java.awt.BorderLayout
+import java.awt.Dimension
+import java.awt.datatransfer.StringSelection
+import javax.swing.BorderFactory
+import javax.swing.Box
+import javax.swing.BoxLayout
+import javax.swing.JButton
+import javax.swing.JEditorPane
+import javax.swing.JLabel
+import javax.swing.JPanel
+import javax.swing.ScrollPaneConstants
+import javax.swing.event.HyperlinkEvent
+import javax.swing.text.html.HTMLEditorKit
+
+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) {
+ editorPane.text = getCodeScanIssueDetailsHtml(
+ issue, CodeScanIssueDetailsDisplayType.DetailsPane, CodeWhispererConstants.FixGenerationState.GENERATING,
+ project = project
+ )
+ editorPane.revalidate()
+ editorPane.repaint()
+
+ 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()
+ }
+ } 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()
+ }
+
+ 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)
+ }
+ }
+ }
+
+ private val editorPane = JEditorPane().apply {
+ contentType = "text/html"
+ putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true)
+ border = BorderFactory.createCompoundBorder(
+ BorderFactory.createEmptyBorder(),
+ BorderFactory.createEmptyBorder(3, 10, 8, 11)
+ )
+ val editorColorsScheme = EditorColorsManager.getInstance().globalScheme
+ background = editorColorsScheme.defaultBackground
+ isEditable = false
+ addHyperlinkListener { he ->
+ if (he.eventType == HyperlinkEvent.EventType.ACTIVATED) {
+ when {
+ he.description.startsWith("amazonq://issue/openDiff-") -> {
+ openDiff(issue)
+ }
+ he.description.startsWith("amazonq://issue/copyDiff-") -> {
+ text = getCodeScanIssueDetailsHtml(
+ issue,
+ CodeScanIssueDetailsDisplayType.DetailsPane,
+ CodeWhispererConstants.FixGenerationState.COMPLETED,
+ true,
+ project = project
+ )
+ CopyPasteManager.getInstance().setContents(StringSelection(issue.suggestedFixes.first().code))
+ val alarm = Alarm()
+ alarm.addRequest({
+ ApplicationManager.getApplication().invokeLater {
+ text = getCodeScanIssueDetailsHtml(
+ issue,
+ CodeScanIssueDetailsDisplayType.DetailsPane,
+ CodeWhispererConstants.FixGenerationState.COMPLETED,
+ false,
+ project = project
+ )
+ }
+ }, 500)
+ }
+ he.description.startsWith("amazonq://issue/openFile-") -> {
+ runInEdt {
+ FileEditorManager.getInstance(project).openTextEditor(
+ OpenFileDescriptor(project, issue.file),
+ true
+ )
+ }
+ }
+ else -> {
+ BrowserUtil.browse(he.url)
+ }
+ }
+ }
+ }
+ editorKit = kit
+ document = doc
+ text = getCodeScanIssueDetailsHtml(issue, CodeScanIssueDetailsDisplayType.DetailsPane, project = project)
+ caretPosition = 0
+ }
+
+ private val scrollPane = JBScrollPane(editorPane).apply {
+ verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
+ horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED
+ }
+ private val severityLabel = JLabel(truncateIssueTitle(issue.title)).apply {
+ icon = getSeverityIcon(issue)
+ horizontalTextPosition = JLabel.LEFT
+ 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)
+ 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)
+ }
+ }
+ }
+ private val explainIssueButton = JButton(message("codewhisperer.codescan.explain_button_label")).apply {
+ addActionListener {
+ explainIssue(issue)
+ }
+ }
+ private val ignoreIssueButton = JButton(message("codewhisperer.codescan.ignore_button")).apply {
+ addActionListener {
+ codeScanManager.ignoreSingleIssue(issue)
+ ApplicationManager.getApplication().executeOnPooledThread {
+ CodeWhispererTelemetryService.getInstance().sendCodeScanIssueIgnore(Component.Webview, issue, false)
+ }
+ }
+ }
+ private val ignoreIssuesButton = JButton(message("codewhisperer.codescan.ignore_all_button")).apply {
+ addActionListener {
+ codeScanManager.ignoreAllIssues(issue)
+ ApplicationManager.getApplication().executeOnPooledThread {
+ CodeWhispererTelemetryService.getInstance().sendCodeScanIssueIgnore(Component.Webview, issue, true)
+ }
+ }
+ }
+ private val closeDetailsButton = JButton(AllIcons.Actions.CloseDarkGrey).apply {
+ border = null
+ margin = null
+ isContentAreaFilled = false
+ putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
+ addActionListener {
+ hideIssueDetails()
+ }
+ }
+ private val titlePane = JPanel().apply {
+ layout = BoxLayout(this, BoxLayout.X_AXIS)
+ preferredSize = Dimension(this.width, 30)
+ add(Box.createHorizontalStrut(10))
+ add(severityLabel)
+ add(Box.createHorizontalGlue())
+ add(closeDetailsButton)
+ }
+ 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(ignoreIssueButton)
+ add(ignoreIssuesButton)
+ add(Box.createHorizontalGlue())
+ }
+ private fun hideIssueDetails() {
+ isVisible = false
+ revalidate()
+ repaint()
+ }
+
+ init {
+ removeAll()
+ kit.styleSheet.apply {
+ addRule("h1, h3 { margin-bottom: 0 }")
+ addRule("th { text-align: left; }")
+ addRule(".code-block { background-color: ${codeBlockBackgroundColor.getHexString()}; border: 1px solid ${codeBlockBorderColor.getHexString()}; }")
+ addRule(".code-block pre { margin: 0; }")
+ addRule(".code-block div { color: ${codeBlockForegroundColor.getHexString()}; }")
+ addRule(
+ ".code-block div.deletion { background-color: ${deletionBackgroundColor.getHexString()}; color: ${deletionForegroundColor.getHexString()}; }"
+ )
+ addRule(
+ ".code-block div.addition { background-color: ${additionBackgroundColor.getHexString()}; color: ${additionForegroundColor.getHexString()}; }"
+ )
+ addRule(".code-block div.meta { background-color: ${metaBackgroundColor.getHexString()}; color: ${metaForegroundColor.getHexString()}; }")
+ }
+
+ add(BorderLayout.NORTH, titlePane)
+ add(BorderLayout.CENTER, scrollPane)
+ add(BorderLayout.SOUTH, buttonPane)
+ isVisible = true
+ revalidate()
+ }
+}
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 78481866a8..8f4befc183 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
@@ -7,11 +7,21 @@ import com.intellij.analysis.problemsView.toolWindow.ProblemsView
import com.intellij.codeHighlighting.HighlightDisplayLevel
import com.intellij.codeInspection.util.InspectionMessage
import com.intellij.icons.AllIcons
+import com.intellij.lang.Commenter
+import com.intellij.lang.Language
+import com.intellij.lang.LanguageCommenters
+import com.intellij.openapi.actionSystem.ActionManager
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.CommonDataKeys
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.components.service
+import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel
@@ -20,13 +30,13 @@ import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.MarkupModel
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
-import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl
import com.intellij.openapi.project.Project
-import com.intellij.openapi.ui.MessageDialogBuilder
+import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.isFile
+import com.intellij.psi.PsiDocumentManager
import com.intellij.refactoring.suggested.range
import com.intellij.ui.content.ContentManagerEvent
import com.intellij.ui.content.ContentManagerListener
@@ -51,6 +61,7 @@ import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
@@ -59,6 +70,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners.CodeWhispererCodeScanEditorMouseMotionListener
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listeners.CodeWhispererCodeScanFileListener
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.CodeScanSessionConfig
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.AmazonQCodeReviewGitUtils.isInsideWorkTree
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil.overlaps
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
@@ -66,13 +79,19 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isUserBui
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPlainText
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanTelemetryEvent
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.INACTIVE_TEXT_COLOR
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.ISSUE_HIGHLIGHT_TEXT_ATTRIBUTES
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.amazonqIgnoreNextLine
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.scanResultsKey
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.scanScopeKey
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth
import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled
+import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
import software.aws.toolkits.jetbrains.utils.isQConnected
import software.aws.toolkits.jetbrains.utils.isQExpired
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
@@ -89,8 +108,9 @@ import kotlin.coroutines.CoroutineContext
private val LOG = getLogger()
class CodeWhispererCodeScanManager(val project: Project) {
+ private val defaultScope = projectCoroutineScope(project)
private val codeScanResultsPanel by lazy {
- CodeWhispererCodeScanResultsView(project)
+ CodeWhispererCodeScanResultsView(project, defaultScope)
}
private val codeScanIssuesContent by lazy {
val contentManager = getProblemsWindow().contentManager
@@ -108,16 +128,24 @@ class CodeWhispererCodeScanManager(val project: Project) {
}
}
- private val fileNodeLookup = mutableMapOf()
+ private var autoScanIssues = emptyList()
+ private var ondemandScanIssues = emptyList()
+ private fun getCombinedScanIssues() = getUniqueIssues(autoScanIssues + ondemandScanIssues)
+ private val severityNodeLookup = mapOf(
+ IssueSeverity.CRITICAL.displayName to DefaultMutableTreeNode(IssueSeverity.CRITICAL.displayName),
+ IssueSeverity.HIGH.displayName to DefaultMutableTreeNode(IssueSeverity.HIGH.displayName),
+ IssueSeverity.MEDIUM.displayName to DefaultMutableTreeNode(IssueSeverity.MEDIUM.displayName),
+ IssueSeverity.LOW.displayName to DefaultMutableTreeNode(IssueSeverity.LOW.displayName),
+ IssueSeverity.INFO.displayName to DefaultMutableTreeNode(IssueSeverity.INFO.displayName)
+ )
private val scanNodesLookup = mutableMapOf>()
+ private val selectedSeverityValues = IssueSeverity.entries.associate { it.displayName to true }.toMutableMap()
private val documentListener = CodeWhispererCodeScanDocumentListener(project)
private val editorMouseListener = CodeWhispererCodeScanEditorMouseMotionListener(project)
private val fileListener = CodeWhispererCodeScanFileListener(project)
- private val isProjectScanInProgress = AtomicBoolean(false)
-
- private val defaultScope = projectCoroutineScope(project)
+ private val isOnDemandScanInProgress = AtomicBoolean(false)
private lateinit var codeScanJob: Job
private lateinit var debouncedCodeScanJob: Job
@@ -126,33 +154,154 @@ class CodeWhispererCodeScanManager(val project: Project) {
* Returns true if the code scan is in progress.
* This function will return true for a cancelled code scan job which is in cancellation state.
*/
- fun isProjectScanInProgress(): Boolean = isProjectScanInProgress.get()
+ fun isOnDemandScanInProgress(): Boolean = isOnDemandScanInProgress.get()
/**
* Code scan job is active when the [Job] is started and is in active state.
*/
- fun isCodeScanJobActive(): Boolean = this::codeScanJob.isInitialized && codeScanJob.isActive && isProjectScanInProgress()
+ fun isCodeScanJobActive(): Boolean = this::codeScanJob.isInitialized && codeScanJob.isActive && isOnDemandScanInProgress()
- fun getRunActionButtonIcon(): Icon = if (isProjectScanInProgress()) AllIcons.Process.Step_1 else AllIcons.Actions.Execute
+ fun getRunActionButtonIcon(): Icon = if (isOnDemandScanInProgress()) AllIcons.Process.Step_1 else AllIcons.Actions.Execute
- fun getActionButtonIconForExplorerNode(): Icon = if (isProjectScanInProgress()) AllIcons.Actions.Suspend else AllIcons.Actions.Execute
+ fun getActionButtonIconForExplorerNode(): Icon = if (isOnDemandScanInProgress()) AllIcons.Actions.Suspend else AllIcons.Actions.Execute
- fun getActionButtonText(): String = if (!isProjectScanInProgress()) {
+ fun getActionButtonText(): String = if (!isOnDemandScanInProgress()) {
message(
- "codewhisperer.codescan.run_scan"
+ "codewhisperer.codescan.run_scan",
+ INACTIVE_TEXT_COLOR
)
} else {
message("codewhisperer.codescan.stop_scan")
}
+ private fun isIgnoredIssueTitle(title: String) = getIgnoredIssueTitles().contains(title)
+
+ fun isIgnoredIssue(title: String, document: Document, file: VirtualFile, startLine: Int) = isIgnoredIssueTitle(title) ||
+ detectSingleIssueIgnored(document, file, startLine)
+
+ private fun getIgnoredIssueTitles() = CodeWhispererSettings.getInstance().getIgnoredCodeReviewIssues().split(";").toMutableSet()
+
+ fun ignoreAllIssues(issue: CodeWhispererCodeScanIssue) {
+ val ignoredIssueTitles = getIgnoredIssueTitles()
+ ignoredIssueTitles.add(issue.title)
+ CodeWhispererSettings.getInstance().setIgnoredCodeReviewIssues(ignoredIssueTitles.joinToString(separator = ";"))
+ // update the in memory copy and UI
+ ondemandScanIssues = ondemandScanIssues.filter { it.title != issue.title }
+ autoScanIssues = autoScanIssues.filter { it.title != issue.title }
+ updateCodeScanIssuesTree()
+ }
+
+ private fun detectSingleIssueIgnored(document: Document, file: VirtualFile, startLine: Int): Boolean = runReadAction {
+ try {
+ if (startLine == 0) return@runReadAction false
+ val commenter = getLanguageCommenter(document, project)
+ val linePrefix: String? = commenter?.lineCommentPrefix ?: file.programmingLanguage().lineCommentPrefix()
+ val blockPrefix: String? = commenter?.blockCommentPrefix ?: file.programmingLanguage().blockCommentPrefix()
+ val blockSuffix: String? = commenter?.blockCommentSuffix ?: file.programmingLanguage().blockCommentSuffix()
+
+ for (i in (startLine - 1) downTo 0) {
+ val lineStart = document.getLineStartOffset(i)
+ val lineEnd = document.getLineEndOffset(i)
+ val targetRange = TextRange(lineStart, lineEnd)
+ val lineText = document.getText(targetRange)
+ if (lineText.isEmpty()) {
+ continue
+ }
+ if (linePrefix != null &&
+ lineText.trimIndent().startsWith(linePrefix) &&
+ lineText.contains(amazonqIgnoreNextLine)
+ ) {
+ return@runReadAction true
+ }
+ if (blockPrefix != null &&
+ blockSuffix != null &&
+ lineText.trimIndent().startsWith(blockPrefix) &&
+ lineText.contains(amazonqIgnoreNextLine) &&
+ lineText.trimEnd().endsWith(blockSuffix)
+ ) {
+ return@runReadAction true
+ }
+ return@runReadAction false
+ }
+ return@runReadAction false
+ } catch (e: Exception) {
+ LOG.warn { "Failed to detect if scan issue is ignored: ${e.stackTraceToString()}" }
+ return@runReadAction false
+ }
+ }
+
+ fun ignoreSingleIssue(issue: CodeWhispererCodeScanIssue) {
+ val document = issue.document
+ var commentString: String? = null
+ var insertOffset: Int? = null
+ try {
+ runReadAction {
+ if (!issue.isVisible) return@runReadAction
+ val lineNumber = issue.startLine
+ val issueRange = TextRange(document.getLineStartOffset(lineNumber - 1), document.getLineEndOffset(lineNumber - 1))
+ val lineContent = document.getText(issueRange)
+ val indentation = lineContent.takeWhile { it.isWhitespace() }
+ insertOffset = document.getLineStartOffset(lineNumber - 1)
+ val commenter = getLanguageCommenter(document, project)
+ val linePrefix: String? = commenter?.lineCommentPrefix ?: issue.file.programmingLanguage().lineCommentPrefix()
+ val blockPrefix: String? = commenter?.blockCommentPrefix ?: issue.file.programmingLanguage().blockCommentPrefix()
+ val blockSuffix: String? = commenter?.blockCommentSuffix ?: issue.file.programmingLanguage().blockCommentSuffix()
+ if (linePrefix != null) {
+ commentString = "$indentation$linePrefix $amazonqIgnoreNextLine\n"
+ } else if (blockPrefix != null && blockSuffix != null) {
+ commentString = "$indentation$blockPrefix $amazonqIgnoreNextLine $blockSuffix\n"
+ }
+ }
+ val finalOffset = insertOffset ?: return
+ val finalCommentString = commentString ?: return
+ ApplicationManager.getApplication().invokeLater {
+ WriteCommandAction.runWriteCommandAction(project) {
+ document.insertString(finalOffset, finalCommentString)
+ }
+ }
+ } catch (e: Exception) {
+ LOG.warn { "Failed to insert ignore comment ${e.stackTraceToString()}" }
+ }
+ ondemandScanIssues = ondemandScanIssues.filter { it.findingId != issue.findingId }
+ autoScanIssues = autoScanIssues.filter { it.findingId != issue.findingId }
+ removeIssueByFindingId(issue, issue.findingId)
+ }
+
+ private fun getLanguageCommenter(document: Document, project: Project): Commenter? {
+ // TODO: need to implement fall back for languages not supported by IDE
+ val language: Language = document
+ .let { PsiDocumentManager.getInstance(project).getPsiFile(it) }
+ ?.language ?: return null
+ return LanguageCommenters.INSTANCE.forLanguage(language)
+ }
+
+ fun isSeveritySelected(severity: String): Boolean = selectedSeverityValues[severity] ?: true
+ fun setSeveritySelected(severity: String, selected: Boolean) {
+ selectedSeverityValues[severity] = selected
+ updateCodeScanIssuesTree()
+ }
+
+ /**
+ * Returns true if there are any code scan issues.
+ */
+ fun hasCodeScanIssues(): Boolean = getCombinedScanIssues().isNotEmpty()
+
+ /**
+ * Clears all filters and updates the code scan issues tree.
+ */
+ fun clearFilters() {
+ selectedSeverityValues.keys.forEach { selectedSeverityValues[it] = true }
+ updateCodeScanIssuesTree()
+ }
+
/**
* Triggers a code scan and displays results in the new tab in problems view panel.
*/
- fun runCodeScan(scope: CodeWhispererConstants.CodeAnalysisScope, isPluginStarting: Boolean = false) {
+ fun runCodeScan(scope: CodeWhispererConstants.CodeAnalysisScope, isPluginStarting: Boolean = false, initiatedByChat: Boolean = false) {
if (!isQConnected(project)) return
// Return if a scan is already in progress.
- if (isProjectScanInProgress() && scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) return
+ if (isOnDemandScanInProgress()) return
val connectionExpired = if (isPluginStarting) {
isQExpired(project)
@@ -160,24 +309,30 @@ class CodeWhispererCodeScanManager(val project: Project) {
promptReAuth(project)
}
if (connectionExpired) {
- isProjectScanInProgress.set(false)
+ isOnDemandScanInProgress.set(false)
return
}
// If scope is project
if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
- // Prepare for a project code scan
- beforeCodeScan()
// launch code scan coroutine
- codeScanJob = launchCodeScanCoroutine(CodeWhispererConstants.CodeAnalysisScope.PROJECT)
+ try {
+ codeScanJob = launchCodeScanCoroutine(CodeWhispererConstants.CodeAnalysisScope.PROJECT, initiatedByChat)
+ } catch (e: CancellationException) {
+ notifyChat(codeScanResponse = null, scope = scope)
+ }
} else {
- if (CodeWhispererExplorerActionManager.getInstance().isAutoEnabledForCodeScan() and !isUserBuilderId(project)) {
+ if (CodeWhispererExplorerActionManager.getInstance().isAutoEnabledForCodeScan() and !isUserBuilderId(project) || initiatedByChat) {
// cancel if a file scan is running.
- if (!isProjectScanInProgress() && this::codeScanJob.isInitialized && codeScanJob.isActive) {
+ if (!isOnDemandScanInProgress() && this::codeScanJob.isInitialized && codeScanJob.isActive) {
codeScanJob.cancel()
}
// Add File Scan
- codeScanJob = launchCodeScanCoroutine(CodeWhispererConstants.CodeAnalysisScope.FILE)
+ try {
+ codeScanJob = launchCodeScanCoroutine(CodeWhispererConstants.CodeAnalysisScope.FILE, initiatedByChat)
+ } catch (e: CancellationException) {
+ notifyChat(codeScanResponse = null, scope = scope)
+ }
}
}
}
@@ -200,25 +355,37 @@ class CodeWhispererCodeScanManager(val project: Project) {
}
}
- fun stopCodeScan() {
+ fun stopCodeScan(scope: CodeWhispererConstants.CodeAnalysisScope) {
// Return if code scan job is not active.
- if (!codeScanJob.isActive) return
- if (isProjectScanInProgress() && confirmCancelCodeScan()) {
- LOG.info { "Security scan stopped by user..." }
+ if (!codeScanJob.isActive) {
+ notifyChat(codeScanResponse = null, scope = scope)
+ return
+ }
+ // TODO: need to check if we need to ask for user's confirmation again
+ if (isOnDemandScanInProgress()) {
+ LOG.info { "Code Review stopped by user..." }
// Checking `codeScanJob.isActive` to ensure that the job is not already completed by the time user confirms.
if (codeScanJob.isActive) {
codeScanResultsPanel.setStoppingCodeScan()
+ notifyChat(codeScanResponse = null, scope = scope)
codeScanJob.cancel(CancellationException("User requested cancellation"))
}
}
}
- private fun confirmCancelCodeScan(): Boolean = MessageDialogBuilder
- .okCancel(message("codewhisperer.codescan.stop_scan"), message("codewhisperer.codescan.stop_scan_confirm_message"))
- .yesText(message("codewhisperer.codescan.stop_scan_confirm_button"))
- .ask(project)
-
- private fun launchCodeScanCoroutine(scope: CodeWhispererConstants.CodeAnalysisScope) = projectCoroutineScope(project).launch {
+ private suspend fun isInValidFile(
+ selectedFile: VirtualFile?,
+ language: CodeWhispererProgrammingLanguage,
+ codeScanSessionConfig: CodeScanSessionConfig,
+ ): Boolean =
+ selectedFile == null ||
+ !language.isAutoFileScanSupported() ||
+ !selectedFile.isFile ||
+ selectedFile.fileSystem.protocol == "remoteDeploymentFS" ||
+ readAction { codeScanSessionConfig.fileIndex.isInLibrarySource(selectedFile) }
+
+ private fun launchCodeScanCoroutine(scope: CodeWhispererConstants.CodeAnalysisScope, initiatedByChat: Boolean) = projectCoroutineScope(project).launch {
+ if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT || initiatedByChat) beforeCodeScan()
var codeScanStatus: Result = Result.Failed
val startTime = Instant.now().toEpochMilli()
var codeScanResponseContext = defaultCodeScanResponseContext()
@@ -232,18 +399,14 @@ class CodeWhispererCodeScanManager(val project: Project) {
} else {
FileEditorManager.getInstance(project).selectedEditor?.file
}
- val codeScanSessionConfig = CodeScanSessionConfig.create(file, project, scope)
+ val codeScanSessionConfig = CodeScanSessionConfig.create(file, project, scope, initiatedByChat)
val selectedFile = codeScanSessionConfig.getSelectedFile()
language = codeScanSessionConfig.getProgrammingLanguage()
if (scope == CodeWhispererConstants.CodeAnalysisScope.FILE &&
- (
- selectedFile == null || !language.isAutoFileScanSupported() || !selectedFile.isFile ||
- runReadAction { (codeScanSessionConfig.fileIndex.isInLibrarySource(selectedFile)) } ||
- selectedFile.fileSystem.protocol == "remoteDeploymentFS"
- )
+ isInValidFile(selectedFile, language, codeScanSessionConfig) && !initiatedByChat
) {
skipTelemetry = true
- LOG.debug { "Language is unknown or plaintext, skipping code scan." }
+ LOG.debug { "Language is unknown or plaintext, skipping code review." }
codeScanStatus = Result.Cancelled
return@launch
} else {
@@ -257,41 +420,51 @@ class CodeWhispererCodeScanManager(val project: Project) {
codeScanResponseContext = codeScanResponse.responseContext
when (codeScanResponse) {
is CodeScanResponse.Success -> {
- val issues = codeScanResponse.issues
+ if (initiatedByChat) {
+ ondemandScanIssues = if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
+ codeScanResponse.issues
+ } else {
+ ondemandScanIssues + codeScanResponse.issues
+ }
+ } else {
+ autoScanIssues = codeScanResponse.issues
+ }
coroutineContext.ensureActive()
renderResponseOnUIThread(
- issues,
+ getCombinedScanIssues(),
codeScanResponse.responseContext.payloadContext.scannedFiles,
scope
)
codeScanStatus = Result.Succeeded
+ if (initiatedByChat) {
+ notifyChat(getChatMessageResponse(codeScanResponse, scope), scope = scope)
+ }
}
is CodeScanResponse.Failure -> {
if (codeScanResponse.failureReason !is TimeoutCancellationException && codeScanResponse.failureReason is CancellationException) {
codeScanStatus = Result.Cancelled
}
+ if (initiatedByChat) {
+ notifyChat(codeScanResponse, scope = scope)
+ }
throw codeScanResponse.failureReason
}
}
- LOG.info { "Security scan completed for jobID: ${codeScanResponseContext.codeScanJobId}." }
+ LOG.info { "Code review completed for jobID: ${codeScanResponseContext.codeScanJobId}." }
}
}
} catch (e: Error) {
- if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
- isProjectScanInProgress.set(false)
- }
- val errorMessage = handleError(coroutineContext, e, scope)
+ afterCodeScan(scope, initiatedByChat)
+ val errorMessage = handleError(coroutineContext, e)
codeScanResponseContext = codeScanResponseContext.copy(reason = errorMessage)
} catch (e: Exception) {
- if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
- isProjectScanInProgress.set(false)
- }
+ afterCodeScan(scope, initiatedByChat)
val errorMessage = handleException(coroutineContext, e, scope)
codeScanResponseContext = codeScanResponseContext.copy(reason = errorMessage)
} finally {
// After code scan
- afterCodeScan(scope)
+ afterCodeScan(scope, initiatedByChat)
if (!skipTelemetry) {
launch {
val duration = (Instant.now().toEpochMilli() - startTime).toDouble()
@@ -305,13 +478,70 @@ class CodeWhispererCodeScanManager(val project: Project) {
scope
)
)
- sendCodeScanTelemetryToServiceAPI(project, language, codeScanResponseContext.codeScanJobId, scope)
+ sendCodeScanTelemetryToServiceAPI(project, language, codeScanResponseContext, scope)
}
}
}
}
- fun handleError(coroutineContext: CoroutineContext, e: Error, scope: CodeWhispererConstants.CodeAnalysisScope): String {
+ private fun getChatMessageResponse(originalResponse: CodeScanResponse.Success, scope: CodeWhispererConstants.CodeAnalysisScope): CodeScanResponse {
+ if (originalResponse.issues.isEmpty()) return originalResponse
+ val chatIssues = if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
+ getCombinedScanIssues()
+ } else {
+ getCombinedScanIssues().filter { issue -> issue.file == originalResponse.issues.first().file }
+ }
+ val finalResponse = originalResponse.copy(issues = chatIssues)
+ return finalResponse
+ }
+
+ private fun refreshUi() {
+ val codeScanTreeModel = CodeWhispererCodeScanTreeModel(codeScanTreeNodeRoot)
+ val totalIssuesCount = codeScanTreeModel.getTotalIssuesCount()
+ if (totalIssuesCount > 0) {
+ codeScanIssuesContent.displayName =
+ message("codewhisperer.codescan.scan_display_with_issues", totalIssuesCount, INACTIVE_TEXT_COLOR)
+ } else {
+ codeScanIssuesContent.displayName = message("codewhisperer.codescan.scan_display")
+ }
+ codeScanResultsPanel.refreshUIWithUpdatedModel(codeScanTreeModel)
+ }
+
+ fun updateIssue(updatedIssue: CodeWhispererCodeScanIssue) {
+ autoScanIssues.find { it.findingId == updatedIssue.findingId }?.let { oldIssue ->
+ val updatedList = autoScanIssues.toMutableList()
+ val index = autoScanIssues.indexOf(oldIssue)
+ updatedList[index] = updatedIssue
+ autoScanIssues = updatedList
+ return
+ }
+ ondemandScanIssues.find { it.findingId == updatedIssue.findingId }?.let { oldIssue ->
+ val updatedList = ondemandScanIssues.toMutableList()
+ val index = ondemandScanIssues.indexOf(oldIssue)
+ updatedList[index] = updatedIssue
+ ondemandScanIssues = updatedList
+ }
+ }
+
+ fun removeIssue(issue: CodeWhispererCodeScanIssue) {
+ autoScanIssues = autoScanIssues.filter { it.findingId != issue.findingId }
+ ondemandScanIssues = ondemandScanIssues.filter { it.findingId != issue.findingId }
+ }
+
+ fun removeIssueByFindingId(issue: CodeWhispererCodeScanIssue, findingId: String) {
+ scanNodesLookup[issue.file]?.forEach { node ->
+ val issueNode = node.userObject as CodeWhispererCodeScanIssue
+ if (issueNode.findingId == findingId) {
+ issueNode.rangeHighlighter?.textAttributes = null
+ issueNode.rangeHighlighter?.dispose()
+ node.removeFromParent()
+ removeIssue(issue)
+ }
+ }
+ refreshUi()
+ }
+
+ fun handleError(coroutineContext: CoroutineContext, e: Error): String {
val errorMessage = when (e) {
is NoClassDefFoundError -> {
if (e.cause?.message?.contains("com.intellij.openapi.compiler.CompilerPaths") == true) {
@@ -326,9 +556,7 @@ class CodeWhispererCodeScanManager(val project: Project) {
if (!coroutineContext.isActive) {
codeScanResultsPanel.setDefaultUI()
} else {
- if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
- codeScanResultsPanel.showError(errorMessage)
- }
+ codeScanResultsPanel.showError(errorMessage)
}
return errorMessage
@@ -345,9 +573,13 @@ class CodeWhispererCodeScanManager(val project: Project) {
}
private fun getCodeScanServerExceptionMessage(e: CodeWhispererCodeScanServerException): String? =
- e.message?.takeIf { it.startsWith("UploadArtifactToS3Exception:") }
- ?.let { message("codewhisperer.codescan.upload_to_s3_failed") }
-
+ when {
+ e.message?.startsWith("UploadArtifactToS3Exception:") == true ->
+ message("codewhisperer.codescan.upload_to_s3_failed")
+ e.message?.startsWith("You've reached") == true ->
+ message("codewhisperer.codescan.quota_exceeded")
+ else -> null
+ }
fun handleException(coroutineContext: CoroutineContext, e: Exception, scope: CodeWhispererConstants.CodeAnalysisScope): String {
val errorMessage = when (e) {
is CodeWhispererException -> e.awsErrorDetails().errorMessage() ?: message("codewhisperer.codescan.run_scan_error")
@@ -365,9 +597,7 @@ class CodeWhispererCodeScanManager(val project: Project) {
if (!coroutineContext.isActive) {
codeScanResultsPanel.setDefaultUI()
} else {
- if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
- codeScanResultsPanel.showError(errorMessage)
- }
+ codeScanResultsPanel.showError(errorMessage)
}
if (
@@ -379,7 +609,7 @@ class CodeWhispererCodeScanManager(val project: Project) {
if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
LOG.error(e) {
- "Failed to run security scan and display results. Caused by: $errorMessage, status code: $errorCode, " +
+ "Failed to run code review and display results. Caused by: $errorMessage, status code: $errorCode, " +
"exception: ${e::class.simpleName}, request ID: $requestId " +
"Jetbrains IDE: ${ApplicationInfo.getInstance().fullApplicationName}, " +
"IDE version: ${ApplicationInfo.getInstance().apiVersion}, "
@@ -394,7 +624,10 @@ class CodeWhispererCodeScanManager(val project: Project) {
message("codewhisperer.codescan.file_too_large") -> message("codewhisperer.codescan.file_too_large_telemetry")
else -> e.message
}
- is CodeWhispererCodeScanServerException -> e.message
+ is CodeWhispererCodeScanServerException -> when (e.message) {
+ message("testgen.error.maximum_generations_reach") -> message("codewhisperer.codescan.quota_exceeded")
+ else -> e.message
+ }
is WaiterTimeoutException, is TimeoutCancellationException -> message("codewhisperer.codescan.scan_timed_out")
is CancellationException -> message("codewhisperer.codescan.cancelled_by_user_exception")
else -> e.message
@@ -406,21 +639,24 @@ class CodeWhispererCodeScanManager(val project: Project) {
/**
* The initial landing UI for the code scan results view panel.
* This method adds code content to the problems view if not already added.
- * When [setSelected] is true, code scan panel is set to be in focus.
*/
- fun addCodeScanUI(setSelected: Boolean = false) = runInEdt {
- reset()
+ fun buildCodeScanUI() = runInEdt {
val problemsWindow = getProblemsWindow()
if (!problemsWindow.contentManager.contents.contains(codeScanIssuesContent)) {
problemsWindow.contentManager.addContent(codeScanIssuesContent)
- }
- codeScanIssuesContent.displayName = message("codewhisperer.codescan.scan_display")
- if (setSelected) {
- problemsWindow.contentManager.setSelectedContent(codeScanIssuesContent)
- problemsWindow.show()
+ codeScanIssuesContent.displayName = message("codewhisperer.codescan.scan_display")
}
}
+ /**
+ * This method shows the code content panel in problems view
+ */
+ fun showCodeScanUI() = runInEdt {
+ val problemsWindow = getProblemsWindow()
+ problemsWindow.contentManager.setSelectedContent(codeScanIssuesContent)
+ problemsWindow.show()
+ }
+
fun removeCodeScanUI() = runInEdt {
val problemsWindow = getProblemsWindow()
if (problemsWindow.contentManager.contents.contains(codeScanIssuesContent)) {
@@ -455,6 +691,7 @@ class CodeWhispererCodeScanManager(val project: Project) {
getScanTree().model.valueForPathChanged(TreePath(node.path), newIssue)
node.userObject = newIssue
}
+ updateIssue(newIssue)
}
}
}
@@ -464,9 +701,14 @@ class CodeWhispererCodeScanManager(val project: Project) {
scanNodesLookup[file]?.forEach { node ->
val issue = node.userObject as CodeWhispererCodeScanIssue
if (document.getLineNumber(editedTextRange.startOffset) <= issue.startLine) {
- issue.startLine = issue.startLine + lineOffset
- issue.endLine = issue.endLine + lineOffset
- issue.suggestedFixes = issue.suggestedFixes.map { fix -> offsetSuggestedFix(fix, lineOffset) }
+ val newIssue = issue.copy()
+ newIssue.startLine = issue.startLine + lineOffset
+ newIssue.endLine = issue.endLine + lineOffset
+ newIssue.suggestedFixes = issue.suggestedFixes.map { fix -> offsetSuggestedFix(fix, lineOffset) }.toMutableList()
+ synchronized(node) {
+ node.userObject = newIssue
+ }
+ updateIssue(issue)
}
}
}
@@ -492,8 +734,9 @@ class CodeWhispererCodeScanManager(val project: Project) {
synchronized(codeScanTreeNodeRoot) {
codeScanTreeNodeRoot.removeAllChildren()
}
- // Remove previous document listeners before starting a new scan.
- fileNodeLookup.clear()
+ severityNodeLookup.onEach { (_, node) ->
+ node.removeAllChildren()
+ }
// Erase all range highlighter before cleaning up.
scanNodesLookup.apply {
forEach { (_, nodes) ->
@@ -518,31 +761,80 @@ class CodeWhispererCodeScanManager(val project: Project) {
}
}
- private fun beforeCodeScan() {
- isProjectScanInProgress.set(true)
- addCodeScanUI(setSelected = true)
+ private suspend fun beforeCodeScan() {
+ isOnDemandScanInProgress.set(true)
// Show in progress indicator
+ buildCodeScanUI()
codeScanResultsPanel.showInProgressIndicator()
- (FileDocumentManagerImpl.getInstance() as FileDocumentManagerImpl).saveAllDocuments(false)
+ withContext(EDT) {
+ FileDocumentManager.getInstance().saveAllDocuments()
+ }
}
- private fun afterCodeScan(scope: CodeWhispererConstants.CodeAnalysisScope) {
- if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
- isProjectScanInProgress.set(false)
+ private fun getUniqueIssues(codeScanResponse: List): List {
+ val uniqueIssues = codeScanResponse.distinctBy { issue ->
+ Triple(issue.file.path, issue.title, issue.startLine)
+ }
+ val uniqueIssueList: MutableList = mutableListOf()
+
+ uniqueIssues.forEach { issue ->
+ val isValid = runReadAction {
+ FileDocumentManager.getInstance().getDocument(issue.file)?.let { document ->
+ val documentLines = document.getText().split("\n")
+ val (startLine, endLine) = issue.run { startLine to endLine }
+ checkIssueCodeSnippet(issue.codeSnippet, startLine, endLine, documentLines)
+ } ?: false
+ }
+ if (isValid) {
+ uniqueIssueList.add(issue)
+ }
+ }
+ return uniqueIssueList
+ }
+
+ private fun afterCodeScan(scope: CodeWhispererConstants.CodeAnalysisScope, initiatedByChat: Boolean) {
+ if (initiatedByChat || scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
+ isOnDemandScanInProgress.set(false)
+ showCodeScanUI()
}
}
private fun sendCodeScanTelemetryToServiceAPI(
project: Project,
programmingLanguage: CodeWhispererProgrammingLanguage,
- codeScanJobId: String?,
+ codeScanResponseContext: CodeScanResponseContext,
scope: CodeWhispererConstants.CodeAnalysisScope,
) {
runIfIdcConnectionOrTelemetryEnabled(project) {
try {
- val response = CodeWhispererClientAdaptor.getInstance(project)
- .sendCodeScanTelemetry(programmingLanguage, codeScanJobId, scope)
- LOG.debug { "Successfully sent code scan telemetry. RequestId: ${response.responseMetadata().requestId()} for ${scope.value} scan" }
+ val client = CodeWhispererClientAdaptor.getInstance(project)
+ val response = client.sendCodeScanTelemetry(programmingLanguage, codeScanResponseContext.codeScanJobId, scope)
+ LOG.debug { "Successfully sent code review telemetry. RequestId: ${response.responseMetadata().requestId()} for ${scope.value} scan" }
+
+ if (codeScanResponseContext.reason == "Succeeded") {
+ val codeScanSuccessResponse = client.sendCodeScanSucceededTelemetry(
+ programmingLanguage,
+ codeScanResponseContext.codeScanJobId,
+ scope,
+ codeScanResponseContext.codeScanTotalIssues
+ )
+ LOG.debug {
+ "Successfully sent code review succeeded telemetry. RequestId: ${
+ codeScanSuccessResponse.responseMetadata().requestId()
+ } for ${scope.value} review"
+ }
+ } else {
+ val codeScanFailureResponse = client.sendCodeScanFailedTelemetry(
+ programmingLanguage,
+ codeScanResponseContext.codeScanJobId,
+ scope
+ )
+ LOG.debug {
+ "Successfully sent code review failed telemetry. RequestId: ${
+ codeScanFailureResponse.responseMetadata().requestId()
+ } for ${scope.value} review"
+ }
+ }
} catch (e: Exception) {
val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null
LOG.debug {
@@ -552,7 +844,7 @@ class CodeWhispererCodeScanManager(val project: Project) {
}
}
- private val codeScanTreeNodeRoot = DefaultMutableTreeNode("CodeWhisperer Code scan results")
+ private val codeScanTreeNodeRoot = DefaultMutableTreeNode("Amazon Q code review results")
/**
* Creates a CodeWhisperer code scan issues tree.
@@ -562,112 +854,68 @@ class CodeWhispererCodeScanManager(val project: Project) {
* [scanNodesLookup] for receiving the editor events and updating the corresponding scan nodes.
*/
private fun createCodeScanIssuesTree(codeScanIssues: List): DefaultMutableTreeNode {
- LOG.debug { "Rendering response from the scan API" }
+ LOG.debug { "Rendering response from the code review API" }
synchronized(codeScanTreeNodeRoot) {
codeScanTreeNodeRoot.removeAllChildren()
}
- // clear file node lookup and scan node lookup
- synchronized(fileNodeLookup) {
- fileNodeLookup.clear()
- }
synchronized(scanNodesLookup) {
scanNodesLookup.clear()
}
-
- codeScanIssues.forEach { issue ->
- val fileNode = synchronized(fileNodeLookup) {
- fileNodeLookup.getOrPut(issue.file) {
- val node = DefaultMutableTreeNode(issue.file)
- synchronized(codeScanTreeNodeRoot) {
- codeScanTreeNodeRoot.add(node)
- }
- node
- }
+ synchronized(severityNodeLookup) {
+ severityNodeLookup.forEach { (_, node) ->
+ node.removeAllChildren()
}
-
- val scanNode = DefaultMutableTreeNode(issue)
- fileNode.add(scanNode)
- scanNodesLookup.getOrPut(issue.file) {
- mutableListOf()
- }.add(scanNode)
}
- return codeScanTreeNodeRoot
- }
- /**
- * Updates a CodeWhisperer code scan issues tree for a file
- * For a given file, looks up its file node:
- * 1. Remove all its existing children scan nodes
- * 2. add the new issues as new scan nodes
- * [scanNodesLookup] for receiving the editor events and updating the corresponding scan nodes.
- */
- fun updateFileIssues(file: VirtualFile, newIssues: List): DefaultMutableTreeNode {
- val fileNode = synchronized(fileNodeLookup) {
- fileNodeLookup.getOrPut(file) {
- val node = DefaultMutableTreeNode(file)
+ severityNodeLookup.forEach { (severity, node) ->
+ if (selectedSeverityValues[severity] == true) {
synchronized(codeScanTreeNodeRoot) {
codeScanTreeNodeRoot.add(node)
}
- node
}
}
- // Remove the underline for old issues
- scanNodesLookup[file]?.forEach { node ->
- val issue = node.userObject as CodeWhispererCodeScanIssue
- issue.rangeHighlighter?.textAttributes = null
- issue.rangeHighlighter?.dispose()
- }
- // Remove the old scan nodes from the file node.
- synchronized(fileNode) {
- fileNode.removeAllChildren()
- }
- // Remove the entry for the file from the scan nodes lookup.
- synchronized(scanNodesLookup) {
- if (scanNodesLookup.containsKey(file)) {
- scanNodesLookup.remove(file)
- }
- }
-
- // Add new issues to the file node.
- newIssues.forEach { issue ->
- val scanNode = DefaultMutableTreeNode(issue)
- fileNode.add(scanNode)
- scanNodesLookup.getOrPut(issue.file) {
- mutableListOf()
- }.add(scanNode)
- }
- if (fileNode.childCount == 0) {
- fileNode.removeFromParent()
- fileNodeLookup.remove(file)
+ codeScanIssues.forEach { issue ->
+ if (selectedSeverityValues[issue.severity] == true) {
+ val scanNode = DefaultMutableTreeNode(issue)
+ severityNodeLookup[issue.severity]?.add(scanNode)
+ scanNodesLookup.getOrPut(issue.file) {
+ mutableListOf()
+ }.add(scanNode)
+ }
}
return codeScanTreeNodeRoot
}
- fun removeIssueByFindingId(file: VirtualFile, findingId: String) {
- scanNodesLookup[file]?.forEach { node ->
- val issue = node.userObject as CodeWhispererCodeScanIssue
- if (issue.findingId == findingId) {
- issue.rangeHighlighter?.textAttributes = null
- issue.rangeHighlighter?.dispose()
- node.removeFromParent()
- }
- }
- fileNodeLookup[file]?.let {
- if (it.childCount == 0) {
- it.removeFromParent()
- fileNodeLookup.remove(file)
+ private fun checkIssueCodeSnippet(codeSnippet: List, startLine: Int, endLine: Int, documentLines: List): Boolean = try {
+ codeSnippet
+ .asSequence()
+ .filter { it.number in startLine..endLine }
+ .all { codeBlock ->
+ val lineNumber = codeBlock.number - 1
+ val documentLine = documentLines.getOrNull(lineNumber) ?: return@all false
+
+ when {
+ codeBlock.content.trim().replace(" ", "").all { it == '*' } ->
+ documentLine.length == codeBlock.content.length
+
+ else ->
+ documentLine == codeBlock.content
+ }
}
- }
+ } catch (e: Exception) {
+ false
+ }
+
+ private fun updateCodeScanIssuesTree() {
+ val codeScanTreeNodeRoot = createCodeScanIssuesTree(getCombinedScanIssues())
val codeScanTreeModel = CodeWhispererCodeScanTreeModel(codeScanTreeNodeRoot)
val totalIssuesCount = codeScanTreeModel.getTotalIssuesCount()
if (totalIssuesCount > 0) {
codeScanIssuesContent.displayName =
message("codewhisperer.codescan.scan_display_with_issues", totalIssuesCount, INACTIVE_TEXT_COLOR)
- } else {
- codeScanIssuesContent.displayName = message("codewhisperer.codescan.scan_display")
}
codeScanResultsPanel.refreshUIWithUpdatedModel(codeScanTreeModel)
}
@@ -678,15 +926,7 @@ class CodeWhispererCodeScanManager(val project: Project) {
scope: CodeWhispererConstants.CodeAnalysisScope,
) {
withContext(getCoroutineUiContext()) {
- var root: DefaultMutableTreeNode? = null
- when (scope) {
- CodeWhispererConstants.CodeAnalysisScope.FILE -> {
- val file = scannedFiles.first()
- root = updateFileIssues(file, issues)
- } else -> {
- root = createCodeScanIssuesTree(issues)
- }
- }
+ val root = createCodeScanIssuesTree(issues)
val codeScanTreeModel = CodeWhispererCodeScanTreeModel(root)
val totalIssuesCount = codeScanTreeModel.getTotalIssuesCount()
if (totalIssuesCount > 0) {
@@ -697,12 +937,32 @@ class CodeWhispererCodeScanManager(val project: Project) {
}
}
+ private fun notifyChat(codeScanResponse: CodeScanResponse?, scope: CodeWhispererConstants.CodeAnalysisScope) {
+ // We can't use CodeScanMessageListener directly since it causes circular dependency between plugin-amazonq and plugin-core
+ // Workaround: send an action performed event to notify Q of scan result
+ val dataContext = SimpleDataContext.builder()
+ .add(scanResultsKey, codeScanResponse)
+ .add(CommonDataKeys.PROJECT, project)
+ .add(scanScopeKey, scope)
+ .build()
+ val actionEvent = AnActionEvent.createFromDataContext("", null, dataContext)
+ ActionManager.getInstance().getAction("aws.amazonq.codeScanComplete").actionPerformed(actionEvent)
+ }
+
@TestOnly
suspend fun testRenderResponseOnUIThread(issues: List, scannedFiles: List) {
assert(ApplicationManager.getApplication().isUnitTestMode)
renderResponseOnUIThread(issues, scannedFiles, CodeWhispererConstants.CodeAnalysisScope.PROJECT)
}
+ fun isInsideWorkTree(): Boolean {
+ val projectDir = project.guessProjectDir() ?: run {
+ LOG.error { "Failed to guess project directory" }
+ return false
+ }
+ return isInsideWorkTree(projectDir)
+ }
+
companion object {
fun getInstance(project: Project): CodeWhispererCodeScanManager = project.service()
}
@@ -735,6 +995,7 @@ data class CodeWhispererCodeScanIssue(
val issueSeverity: HighlightDisplayLevel = HighlightDisplayLevel.WARNING,
val isInvalid: Boolean = false,
var rangeHighlighter: RangeHighlighterEx? = null,
+ var isVisible: Boolean = true,
) {
override fun toString(): String = title
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 bb066970ea..47d9fabb6b 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
@@ -10,13 +10,17 @@ import com.intellij.openapi.actionSystem.ActionToolbar
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.AnimatedIcon
+import com.intellij.ui.ClickListener
import com.intellij.ui.OnePixelSplitter
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.border.CustomLineBorder
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.jetbrains.services.codewhisperer.codescan.listeners.CodeWhispererCodeScanTreeMouseListener
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.INACTIVE_TEXT_COLOR
@@ -26,27 +30,44 @@ import java.awt.BorderLayout
import java.awt.Component
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
+import java.awt.event.MouseEvent
import java.time.Instant
import java.time.format.DateTimeFormatter
import javax.swing.BorderFactory
-import javax.swing.JButton
+import javax.swing.Icon
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTree
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.TreeCellRenderer
+import javax.swing.tree.TreePath
/**
* Create a Code Scan results view that displays the code scan results.
*/
-internal class CodeWhispererCodeScanResultsView(private val project: Project) : JPanel(BorderLayout()) {
+internal class CodeWhispererCodeScanResultsView(private val project: Project, private val defaultScope: CoroutineScope) : JPanel(BorderLayout()) {
private val codeScanTree: Tree = Tree().apply {
isRootVisible = false
CodeWhispererCodeScanTreeMouseListener(project).installOn(this)
+ object : ClickListener() {
+ override fun onClick(event: MouseEvent, clickCount: Int): Boolean {
+ val issueNode = (event.source as Tree).selectionPath?.lastPathComponent as? DefaultMutableTreeNode
+ val issue = issueNode?.userObject as? CodeWhispererCodeScanIssue ?: return false
+ showIssueDetails(issue, defaultScope)
+ return true
+ }
+ }.installOn(this)
cellRenderer = ColoredTreeCellRenderer()
}
+ private fun expandItems() {
+ val criticalTreePath = TreePath(arrayOf(codeScanTree.model.root, codeScanTree.model.getChild(codeScanTree.model.root, 0)))
+ val highTreePath = TreePath(arrayOf(codeScanTree.model.root, codeScanTree.model.getChild(codeScanTree.model.root, 1)))
+ codeScanTree.expandPath(criticalTreePath)
+ codeScanTree.expandPath(highTreePath)
+ }
+
private val scrollPane = ScrollPaneFactory.createScrollPane(codeScanTree, true)
private val splitter = OnePixelSplitter(CODE_SCAN_SPLITTER_PROPORTION_KEY, 1.0f).apply {
firstComponent = scrollPane
@@ -70,6 +91,20 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
showScannedFiles(scannedFiles)
}
}
+
+ private val filtersAppliedToResultsLabel = JLabel(message("codewhisperer.codescan.scan_results_hidden_by_filters")).apply {
+ border = BorderFactory.createEmptyBorder(7, 7, 7, 7)
+ }
+ private val clearFiltersLink = ActionLink(message("codewhisperer.codescan.clear_filters")).apply {
+ addActionListener {
+ CodeWhispererCodeScanManager.getInstance(project).clearFilters()
+ }
+ }
+ private val filtersAppliedIndicator = JPanel(GridBagLayout()).apply {
+ add(filtersAppliedToResultsLabel, GridBagConstraints())
+ add(clearFiltersLink, GridBagConstraints().apply { gridy = 1 })
+ }
+
private val learnMoreLabelLink = ActionLink().apply {
border = BorderFactory.createEmptyBorder(0, 7, 0, 0)
}
@@ -89,15 +124,9 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
private val progressIndicatorLabel = JLabel(message("codewhisperer.codescan.scan_in_progress"), AnimatedIcon.Default(), JLabel.CENTER).apply {
border = BorderFactory.createEmptyBorder(7, 7, 7, 7)
}
- private val stopCodeScanButton = JButton(message("codewhisperer.codescan.stop_scan")).apply {
- addActionListener {
- CodeWhispererCodeScanManager.getInstance(project).stopCodeScan()
- }
- }
private val progressIndicator = JPanel(GridBagLayout()).apply {
add(progressIndicatorLabel, GridBagConstraints())
- add(stopCodeScanButton, GridBagConstraints().apply { gridy = 1 })
}
// Results panel containing info label and progressIndicator/scrollPane
@@ -126,6 +155,7 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
model = scanTreeModel
repaint()
}
+ expandItems()
if (scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
this.scannedFiles = scannedFiles
@@ -146,15 +176,34 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
}
fun refreshUIWithUpdatedModel(scanTreeModel: CodeWhispererCodeScanTreeModel) {
- codeScanTree.apply {
- model = scanTreeModel
- repaint()
+ changeInfoLabelToDisplayScanCompleted(scannedFiles.size)
+ val codeScanManager = CodeWhispererCodeScanManager.getInstance(project)
+ if (scanTreeModel.getTotalIssuesCount() == 0 && codeScanManager.hasCodeScanIssues()) {
+ resultsPanel.apply {
+ if (components.contains(splitter)) remove(splitter)
+ add(BorderLayout.CENTER, filtersAppliedIndicator)
+ revalidate()
+ repaint()
+ }
+ } else {
+ codeScanTree.apply {
+ model = scanTreeModel
+ repaint()
+ }
+ expandItems()
+ resultsPanel.apply {
+ if (components.contains(filtersAppliedIndicator)) remove(filtersAppliedIndicator)
+ add(BorderLayout.CENTER, splitter)
+ splitter.proportion = 1.0f
+ splitter.secondComponent = null
+ revalidate()
+ repaint()
+ }
}
}
fun setStoppingCodeScan() {
completeInfoLabel.isVisible = false
- stopCodeScanButton.isVisible = false
resultsPanel.apply {
if (components.contains(splitter)) remove(splitter)
progressIndicatorLabel.apply {
@@ -197,7 +246,6 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
fun showInProgressIndicator() {
completeInfoLabel.isVisible = false
- stopCodeScanButton.isVisible = true
progressIndicatorLabel.text = message("codewhisperer.codescan.scan_in_progress")
resultsPanel.apply {
if (components.contains(splitter)) remove(splitter)
@@ -241,6 +289,20 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
}
}
+ private fun showIssueDetails(issue: CodeWhispererCodeScanIssue, defaultScope: CoroutineScope) {
+ val issueDetailsViewPanel = CodeWhispererCodeScanIssueDetailsPanel(project, issue, defaultScope)
+ issueDetailsViewPanel.apply {
+ isVisible = true
+ revalidate()
+ }
+ splitter.apply {
+ secondComponent = issueDetailsViewPanel
+ proportion = 0.5f
+ revalidate()
+ repaint()
+ }
+ }
+
private fun changeInfoLabelToDisplayScanCompleted(numScannedFiles: Int) {
completeInfoLabel.isVisible = true
infoLabelPrefix.icon = AllIcons.Actions.Commit
@@ -265,6 +327,15 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
}
private class ColoredTreeCellRenderer : TreeCellRenderer {
+ private fun getSeverityIcon(severity: String): Icon? = when (severity) {
+ IssueSeverity.LOW.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_LOW
+ IssueSeverity.MEDIUM.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_MEDIUM
+ IssueSeverity.HIGH.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_HIGH
+ IssueSeverity.CRITICAL.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_CRITICAL
+ IssueSeverity.INFO.displayName -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INITIAL_INFO
+ else -> null
+ }
+
override fun getTreeCellRendererComponent(
tree: JTree?,
value: Any?,
@@ -278,12 +349,16 @@ internal class CodeWhispererCodeScanResultsView(private val project: Project) :
val cell = JLabel()
synchronized(value) {
when (val obj = value.userObject) {
+ is String -> {
+ cell.text = message("codewhisperer.codescan.severity_issues_count", obj, value.childCount, INACTIVE_TEXT_COLOR)
+ cell.icon = this.getSeverityIcon(obj)
+ }
is VirtualFile -> {
cell.text = message("codewhisperer.codescan.file_name_issues_count", obj.name, obj.path, value.childCount, INACTIVE_TEXT_COLOR)
cell.icon = obj.fileType.icon
}
is CodeWhispererCodeScanIssue -> {
- val cellText = "${obj.title}: ${obj.description.text}"
+ val cellText = "${obj.title.trimEnd('.')}: ${obj.file.name} "
if (obj.isInvalid) {
cell.text = message("codewhisperer.codescan.scan_recommendation_invalid", obj.title, obj.displayTextRange(), INACTIVE_TEXT_COLOR)
cell.toolTipText = message("codewhisperer.codescan.scan_recommendation_invalid.tooltip_text")
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanSession.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanSession.kt
index 45f629206e..4257e91100 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanSession.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanSession.kt
@@ -12,13 +12,11 @@ import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
-import com.intellij.util.io.HttpRequests
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext
-import org.apache.commons.codec.digest.DigestUtils
import software.amazon.awssdk.services.codewhisperer.model.ArtifactType
import software.amazon.awssdk.services.codewhisperer.model.CodeScanFindingsSchema
import software.amazon.awssdk.services.codewhisperer.model.CodeScanStatus
@@ -29,24 +27,18 @@ import software.amazon.awssdk.services.codewhisperer.model.GetCodeScanRequest
import software.amazon.awssdk.services.codewhisperer.model.GetCodeScanResponse
import software.amazon.awssdk.services.codewhisperer.model.ListCodeScanFindingsRequest
import software.amazon.awssdk.services.codewhisperer.model.ListCodeScanFindingsResponse
-import software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisUploadContext
-import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest
import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
-import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext
-import software.amazon.awssdk.services.codewhispererruntime.model.UploadIntent
-import software.amazon.awssdk.utils.IoUtils
+import software.amazon.awssdk.services.codewhispererruntime.model.Reference
import software.aws.toolkits.core.utils.Waiters.waitUntil
import software.aws.toolkits.core.utils.debug
-import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
-import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.CodeScanSessionConfig
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadContext
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanResponseContext
-import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanServiceInvocationContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.model.CreateUploadUrlServiceInvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CODE_SCAN_POLLING_INTERVAL_IN_SECONDS
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.FILE_SCANS_THROTTLING_MESSAGE
@@ -56,17 +48,14 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_BYTES_IN_KB
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_MILLIS_IN_SECOND
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererZipUploadManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.getTelemetryErrorMessage
import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodewhispererLanguage
-import java.io.File
-import java.io.FileInputStream
-import java.io.IOException
-import java.net.HttpURLConnection
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
-import java.util.Base64
import java.util.UUID
import kotlin.coroutines.coroutineContext
@@ -104,7 +93,7 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
"Total size of source payload in KB: ${payloadContext.srcPayloadSize * 1.0 / TOTAL_BYTES_IN_KB} \n" +
"Total size of build payload in KB: ${(payloadContext.buildPayloadSize ?: 0) * 1.0 / TOTAL_BYTES_IN_KB} \n" +
"Total size of source zip file in KB: ${payloadContext.srcZipFileSize * 1.0 / TOTAL_BYTES_IN_KB} \n" +
- "Total number of lines scanned: ${payloadContext.totalLines} \n" +
+ "Total number of lines reviewed: ${payloadContext.totalLines} \n" +
"Total number of files included in payload: ${payloadContext.totalFiles} \n" +
"Total time taken for creating payload: ${payloadContext.totalTimeInMilliseconds * 1.0 / TOTAL_MILLIS_IN_SECOND} seconds\n" +
"Payload context language: ${payloadContext.language}"
@@ -116,7 +105,20 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
currentCoroutineContext.ensureActive()
val artifactsUploadStartTime = now()
val codeScanName = UUID.randomUUID().toString()
- val sourceZipUploadResponse = createUploadUrlAndUpload(sourceZip, "SourceCode", codeScanName)
+
+ val taskType = if (sessionContext.codeAnalysisScope == CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
+ CodeWhispererConstants.UploadTaskType.SCAN_PROJECT
+ } else {
+ CodeWhispererConstants.UploadTaskType.SCAN_FILE
+ }
+
+ val sourceZipUploadResponse =
+ CodeWhispererZipUploadManager.getInstance(sessionContext.project).createUploadUrlAndUpload(
+ sourceZip,
+ "SourceCode",
+ taskType,
+ codeScanName
+ )
if (isProjectScope()) {
LOG.debug {
"Successfully uploaded source zip to s3: " +
@@ -137,7 +139,7 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
val createCodeScanResponse = createCodeScan(payloadContext.language.toString(), codeScanName)
if (isProjectScope()) {
LOG.debug {
- "Successfully created security scan with " +
+ "Successfully created code review with " +
"status: ${createCodeScanResponse.status()} " +
"for request id: ${createCodeScanResponse.responseMetadata().requestId()}"
}
@@ -146,7 +148,7 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
if (codeScanStatus == CodeScanStatus.FAILED) {
if (isProjectScope()) {
LOG.debug {
- "CodeWhisperer service error occurred. Something went wrong when creating a security scan: $createCodeScanResponse " +
+ "CodeWhisperer service error occurred. Something went wrong when creating a code review: $createCodeScanResponse " +
"Status: ${createCodeScanResponse.status()} for request id: ${createCodeScanResponse.responseMetadata().requestId()}"
}
}
@@ -169,13 +171,13 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
currentCoroutineContext.ensureActive()
val elapsedTime = (now() - startTime) * 1.0 / TOTAL_MILLIS_IN_SECOND
if (isProjectScope()) {
- LOG.debug { "Waiting for security scan to complete. Elapsed time: $elapsedTime sec." }
+ LOG.debug { "Waiting for code review to complete. Elapsed time: $elapsedTime sec." }
}
val getCodeScanResponse = getCodeScan(jobId)
codeScanStatus = getCodeScanResponse.status()
if (isProjectScope()) {
LOG.debug {
- "Get security scan status: ${getCodeScanResponse.status()}, " +
+ "Get code review status: ${getCodeScanResponse.status()}, " +
"request id: ${getCodeScanResponse.responseMetadata().requestId()}"
}
}
@@ -183,7 +185,7 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
if (codeScanStatus == CodeScanStatus.FAILED) {
if (isProjectScope()) {
LOG.debug {
- "CodeWhisperer service error occurred. Something went wrong fetching results for security scan: $getCodeScanResponse " +
+ "CodeWhisperer service error occurred. Something went wrong fetching results for code review: $getCodeScanResponse " +
"Status: ${getCodeScanResponse.status()} for request id: ${getCodeScanResponse.responseMetadata().requestId()}"
}
}
@@ -192,7 +194,7 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
}
}
- LOG.debug { "Security scan completed successfully by CodeWhisperer." }
+ LOG.debug { "Code review completed successfully by Amazon Q." }
// 6. Return the results from the ListCodeScan API.
currentCoroutineContext.ensureActive()
@@ -213,10 +215,10 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
}
if (isProjectScope()) {
- LOG.debug { "Rendering response to display security scan results." }
+ LOG.debug { "Rendering response to display code review results." }
}
currentCoroutineContext.ensureActive()
- val issues = mapToCodeScanIssues(documents)
+ val issues = mapToCodeScanIssues(documents, sessionContext.project).filter { it.isVisible }
codeScanResponseContext = codeScanResponseContext.copy(codeScanTotalIssues = issues.count())
codeScanResponseContext = codeScanResponseContext.copy(codeScanIssuesWithFixes = issues.count { it.suggestedFixes.isNotEmpty() })
codeScanResponseContext = codeScanResponseContext.copy(reason = "Succeeded")
@@ -228,16 +230,16 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
if (awsError != null) {
if (awsError.errorCode() == "ThrottlingException" && awsError.errorMessage() != null) {
if (awsError.errorMessage()!!.contains(PROJECT_SCANS_THROTTLING_MESSAGE)) {
- LOG.info { "Project Scans limit reached" }
+ LOG.info { "Project reviews limit reached" }
notifyErrorCodeWhispererUsageLimit(sessionContext.project, true)
} else if (awsError.errorMessage()!!.contains(FILE_SCANS_THROTTLING_MESSAGE)) {
- LOG.info { "File Scans limit reached" }
+ LOG.info { "File reviews limit reached" }
CodeWhispererExplorerActionManager.getInstance().setMonthlyQuotaForCodeScansExceeded(true)
}
}
}
- LOG.error(e) {
- "Failed to run security scan and display results. Caused by: ${e.message}, status code: ${awsError?.errorCode()}, " +
+ LOG.debug(e) {
+ "Failed to run code review and display results. Caused by: ${e.message}, status code: ${awsError?.errorCode()}, " +
"exception: ${e::class.simpleName}, request ID: ${exception?.requestId()}" +
"Jetbrains IDE: ${ApplicationInfo.getInstance().fullApplicationName}, " +
"IDE version: ${ApplicationInfo.getInstance().apiVersion}, "
@@ -246,101 +248,32 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
}
}
- /**
- * Creates an upload URL and uplaods the zip file to the presigned URL
- */
- fun createUploadUrlAndUpload(zipFile: File, artifactType: String, codeScanName: String): CreateUploadUrlResponse {
- // Throw error if zipFile is invalid.
- if (!zipFile.exists()) {
- invalidSourceZipError()
- }
- val fileMd5: String = Base64.getEncoder().encodeToString(DigestUtils.md5(FileInputStream(zipFile)))
- val createUploadUrlResponse = createUploadUrl(fileMd5, artifactType, codeScanName)
- val url = createUploadUrlResponse.uploadUrl()
- if (isProjectScope()) {
- LOG.debug { "Uploading $artifactType using the presigned URL." }
- }
- uploadArtifactToS3(
- url,
- createUploadUrlResponse.uploadId(),
- zipFile,
- fileMd5,
- createUploadUrlResponse.kmsKeyArn(),
- createUploadUrlResponse.requestHeaders()
- )
- return createUploadUrlResponse
- }
-
- fun createUploadUrl(md5Content: String, artifactType: String, codeScanName: String): CreateUploadUrlResponse = try {
- clientAdaptor.createUploadUrl(
- CreateUploadUrlRequest.builder()
- .contentMd5(md5Content)
- .artifactType(artifactType)
- .uploadIntent(getUploadIntent(sessionContext.codeAnalysisScope))
- .uploadContext(UploadContext.fromCodeAnalysisUploadContext(CodeAnalysisUploadContext.builder().codeScanName(codeScanName).build()))
- .build()
- )
- } catch (e: Exception) {
- LOG.debug { "Create Upload URL failed: ${e.message}" }
- val errorMessage = getTelemetryErrorMessage(e)
- throw codeScanServerException("CreateUploadUrlException: $errorMessage")
- }
-
- private fun getUploadIntent(scope: CodeWhispererConstants.CodeAnalysisScope): UploadIntent = when (scope) {
- CodeWhispererConstants.CodeAnalysisScope.FILE -> UploadIntent.AUTOMATIC_FILE_SECURITY_SCAN
- CodeWhispererConstants.CodeAnalysisScope.PROJECT -> UploadIntent.FULL_PROJECT_SECURITY_SCAN
- }
-
- @Throws(IOException::class)
- fun uploadArtifactToS3(url: String, uploadId: String, fileToUpload: File, md5: String, kmsArn: String?, requestHeaders: Map?) {
- try {
- val uploadIdJson = """{"uploadId":"$uploadId"}"""
- HttpRequests.put(url, "application/zip").userAgent(AwsClientManager.getUserAgent()).tuner {
- if (requestHeaders.isNullOrEmpty()) {
- it.setRequestProperty(CONTENT_MD5, md5)
- it.setRequestProperty(CONTENT_TYPE, APPLICATION_ZIP)
- it.setRequestProperty(SERVER_SIDE_ENCRYPTION, AWS_KMS)
- if (kmsArn?.isNotEmpty() == true) {
- it.setRequestProperty(SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, kmsArn)
- }
- it.setRequestProperty(SERVER_SIDE_ENCRYPTION_CONTEXT, Base64.getEncoder().encodeToString(uploadIdJson.toByteArray()))
- } else {
- requestHeaders.forEach { entry ->
- it.setRequestProperty(entry.key, entry.value)
- }
- }
- }.connect {
- val connection = it.connection as HttpURLConnection
- connection.setFixedLengthStreamingMode(fileToUpload.length())
- IoUtils.copy(fileToUpload.inputStream(), connection.outputStream)
- }
- } catch (e: Exception) {
- LOG.debug { "Artifact failed to upload in the S3 bucket: ${e.message}" }
- val errorMessage = getTelemetryErrorMessage(e)
- throw codeScanServerException("UploadArtifactToS3Exception: $errorMessage")
- }
- }
-
fun createCodeScan(language: String, codeScanName: String): CreateCodeScanResponse {
val artifactsMap = mapOf(
ArtifactType.SOURCE_CODE to urlResponse[ArtifactType.SOURCE_CODE]?.uploadId(),
ArtifactType.BUILT_JARS to urlResponse[ArtifactType.BUILT_JARS]?.uploadId()
).filter { (_, v) -> v != null }
+ val scope = when {
+ sessionContext.codeAnalysisScope == CodeWhispererConstants.CodeAnalysisScope.FILE &&
+ !sessionContext.sessionConfig.isInitiatedByChat() -> CodeWhispererConstants.CodeAnalysisScope.FILE
+ else -> CodeWhispererConstants.CodeAnalysisScope.PROJECT
+ }
+
try {
return clientAdaptor.createCodeScan(
CreateCodeScanRequest.builder()
.clientToken(clientToken.toString())
.programmingLanguage { it.languageName(language) }
.artifacts(artifactsMap)
- .scope(sessionContext.codeAnalysisScope.value)
+ .scope(scope.value)
.codeScanName(codeScanName)
.build()
)
} catch (e: Exception) {
- LOG.debug { "Creating security scan failed: ${e.message}" }
+ LOG.debug { "Creating code review failed: ${e.message}" }
val errorMessage = getTelemetryErrorMessage(e)
- throw codeScanServerException("CreateCodeScanException: $errorMessage")
+ throw codeScanServerException(errorMessage)
}
}
@@ -351,9 +284,9 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
.build()
)
} catch (e: Exception) {
- LOG.debug { "Getting security scan failed: ${e.message}" }
+ LOG.debug { "Getting code review failed: ${e.message}" }
val errorMessage = getTelemetryErrorMessage(e)
- throw codeScanServerException("GetCodeScanException: $errorMessage")
+ throw codeScanServerException("GetCodeReviewException: $errorMessage")
}
fun listCodeScanFindings(jobId: String, nextToken: String?): ListCodeScanFindingsResponse = try {
@@ -365,20 +298,20 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
.build()
)
} catch (e: Exception) {
- LOG.debug { "Listing security scan failed: ${e.message}" }
+ LOG.debug { "Listing code review failed: ${e.message}" }
val errorMessage = getTelemetryErrorMessage(e)
- throw codeScanServerException("ListCodeScanFindingsException: $errorMessage")
+ throw codeScanServerException("ListCodeReviewFindingsException: $errorMessage")
}
- fun mapToCodeScanIssues(recommendations: List): List {
+ fun mapToCodeScanIssues(recommendations: List, project: Project): List {
val scanRecommendations = recommendations.flatMap { MAPPER.readValue>(it) }
if (isProjectScope()) {
- LOG.debug { "Total code scan issues returned from service: ${scanRecommendations.size}" }
+ LOG.debug { "Total code review issues returned from service: ${scanRecommendations.size}" }
}
return scanRecommendations.mapNotNull { recommendation ->
val file = try {
LocalFileSystem.getInstance().findFileByIoFile(
- Path.of(sessionContext.sessionConfig.projectRoot.path, recommendation.filePath).toFile()
+ Path.of(sessionContext.sessionConfig.projectRoot.path, recommendation.filePath.substringAfter(sessionContext.project.name)).toFile()
)
} catch (e: Exception) {
LOG.debug { "Cannot find file at location ${recommendation.filePath}" }
@@ -389,48 +322,31 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
runReadAction {
FileDocumentManager.getInstance().getDocument(file)
}?.let { document ->
-
- val documentLines = document.getText().split("\n")
- val (startLine, endLine) = recommendation.run { startLine to endLine }
- var shouldDisplayIssue = true
-
- for (codeBlock in recommendation.codeSnippet) {
- val lineNumber = codeBlock.number - 1
- if (codeBlock.number in startLine..endLine) {
- val documentLine = documentLines.getOrNull(lineNumber)
- if (documentLine != codeBlock.content) {
- shouldDisplayIssue = false
- break
- }
- }
- }
-
- if (shouldDisplayIssue) {
- val endLineInDocument = minOf(maxOf(0, recommendation.endLine - 1), document.lineCount - 1)
- val endCol = document.getLineEndOffset(endLineInDocument) - document.getLineStartOffset(endLineInDocument) + 1
-
- CodeWhispererCodeScanIssue(
- startLine = recommendation.startLine,
- startCol = 1,
- endLine = recommendation.endLine,
- endCol = endCol,
- file = file,
- project = sessionContext.project,
- title = recommendation.title,
- description = recommendation.description,
- detectorId = recommendation.detectorId,
- detectorName = recommendation.detectorName,
- findingId = recommendation.findingId,
- ruleId = recommendation.ruleId,
- relatedVulnerabilities = recommendation.relatedVulnerabilities,
- severity = recommendation.severity,
- recommendation = recommendation.remediation.recommendation,
- suggestedFixes = recommendation.remediation.suggestedFixes,
- codeSnippet = recommendation.codeSnippet
- )
- } else {
- null
- }
+ val endLineInDocument = minOf(maxOf(0, recommendation.endLine - 1), document.lineCount - 1)
+ val endCol = document.getLineEndOffset(endLineInDocument) - document.getLineStartOffset(endLineInDocument) + 1
+ val manager = CodeWhispererCodeScanManager.getInstance(project)
+ val isIssueIgnored = manager.isIgnoredIssue(recommendation.title, document, file, recommendation.startLine - 1)
+
+ CodeWhispererCodeScanIssue(
+ startLine = recommendation.startLine,
+ startCol = 1,
+ endLine = recommendation.endLine,
+ endCol = endCol,
+ file = file,
+ project = sessionContext.project,
+ title = recommendation.title,
+ description = recommendation.description,
+ detectorId = recommendation.detectorId,
+ detectorName = recommendation.detectorName,
+ findingId = recommendation.findingId,
+ ruleId = recommendation.ruleId,
+ relatedVulnerabilities = recommendation.relatedVulnerabilities,
+ severity = recommendation.severity,
+ recommendation = recommendation.remediation.recommendation,
+ suggestedFixes = recommendation.remediation.suggestedFixes,
+ codeSnippet = recommendation.codeSnippet,
+ isVisible = !isIssueIgnored,
+ )
}
} else {
null
@@ -443,18 +359,6 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
}
}
- fun getTelemetryErrorMessage(e: Exception): String = when {
- e.message?.contains("Resource not found.") == true -> "Resource not found."
- e.message?.contains("Service returned HTTP status code 407") == true -> "Service returned HTTP status code 407"
- e.message?.contains("Improperly formed request") == true -> "Improperly formed request"
- e.message?.contains("Service returned HTTP status code 403") == true -> "Service returned HTTP status code 403"
- e.message?.contains("invalid_grant: Invalid token provided") == true -> "invalid_grant: Invalid token provided"
- e.message?.contains("Connect timed out") == true -> "Unable to execute HTTP request: Connect timed out" // Error: Connect to host failed
- e.message?.contains("Encountered an unexpected error when processing the request, please try again.") == true ->
- "Encountered an unexpected error when processing the request, please try again."
- else -> e.message ?: message("codewhisperer.codescan.run_scan_error_telemetry")
- }
-
private fun isProjectScope(): Boolean = sessionContext.codeAnalysisScope == CodeWhispererConstants.CodeAnalysisScope.PROJECT
companion object {
@@ -508,7 +412,7 @@ data class Remediation(val recommendation: Recommendation, val suggestedFixes: L
data class Recommendation(val text: String, val url: String?)
-data class SuggestedFix(val description: String, val code: String)
+data class SuggestedFix(val description: String, val code: String, val codeFixJobId: String? = null, val references: MutableList = mutableListOf())
data class CodeLine(val number: Int, val content: String)
@@ -520,6 +424,6 @@ data class CodeScanSessionContext(
internal fun defaultPayloadContext() = PayloadContext(CodewhispererLanguage.Unknown, 0, 0, 0, listOf(), 0, 0)
-internal fun defaultServiceInvocationContext() = CodeScanServiceInvocationContext(0, 0)
+internal fun defaultCreateUploadUrlServiceInvocationContext() = CreateUploadUrlServiceInvocationContext()
-internal fun defaultCodeScanResponseContext() = CodeScanResponseContext(defaultPayloadContext(), defaultServiceInvocationContext())
+internal fun defaultCodeScanResponseContext() = CodeScanResponseContext(defaultPayloadContext(), defaultCreateUploadUrlServiceInvocationContext())
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanFilterGroup.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanFilterGroup.kt
new file mode 100644
index 0000000000..25f31a9f15
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanFilterGroup.kt
@@ -0,0 +1,39 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions
+
+import com.intellij.openapi.actionSystem.ActionGroup
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.ex.CheckboxAction
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity
+
+class CodeWhispererCodeScanFilterGroup : ActionGroup() {
+ override fun getChildren(e: AnActionEvent?): Array =
+ IssueSeverity.entries.map { FilterBySeverityAction(e, it.displayName) }.toTypedArray()
+
+ private class FilterBySeverityAction(event: AnActionEvent?, severity: String) : CheckboxAction() {
+ override fun getActionUpdateThread() = ActionUpdateThread.BGT
+ private val severity = severity
+
+ override fun isSelected(event: AnActionEvent): Boolean {
+ val project = event.project ?: return false
+ return CodeWhispererCodeScanManager.getInstance(project).isSeveritySelected(severity)
+ }
+
+ override fun setSelected(event: AnActionEvent, state: Boolean) {
+ val project = event.project
+ if (project != null) {
+ CodeWhispererCodeScanManager.getInstance(project).setSeveritySelected(severity, state)
+ }
+ }
+
+ override fun update(e: AnActionEvent) {
+ super.update(e)
+ e.presentation.text = severity
+ }
+ }
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanRunAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanRunAction.kt
index da115041d3..c0a86ca2c4 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanRunAction.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererCodeScanRunAction.kt
@@ -4,16 +4,19 @@
package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions
import com.intellij.icons.AllIcons
+import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
-import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.INACTIVE_TEXT_COLOR
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.runScanKey
import software.aws.toolkits.resources.message
class CodeWhispererCodeScanRunAction : DumbAwareAction(
- message("codewhisperer.codescan.run_scan"),
+ message("codewhisperer.codescan.run_scan", INACTIVE_TEXT_COLOR),
null,
AllIcons.Actions.Execute
) {
@@ -27,7 +30,11 @@ class CodeWhispererCodeScanRunAction : DumbAwareAction(
}
override fun actionPerformed(event: AnActionEvent) {
- val project = event.project ?: return
- CodeWhispererCodeScanManager.getInstance(project).runCodeScan(CodeWhispererConstants.CodeAnalysisScope.PROJECT)
+ val dataContext = SimpleDataContext.builder()
+ .setParent(event.dataContext)
+ .add(runScanKey, true)
+ .build()
+ val actionEvent = AnActionEvent.createFromDataContext("", null, dataContext)
+ ActionManager.getInstance().getAction("q.openchat").actionPerformed(actionEvent)
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererStopCodeScanAction.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererStopCodeScanAction.kt
deleted file mode 100644
index 06dfed3793..0000000000
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/actions/CodeWhispererStopCodeScanAction.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions
-
-import com.intellij.icons.AllIcons
-import com.intellij.openapi.actionSystem.ActionUpdateThread
-import com.intellij.openapi.actionSystem.AnActionEvent
-import com.intellij.openapi.project.DumbAwareAction
-import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
-import software.aws.toolkits.resources.message
-
-class CodeWhispererStopCodeScanAction : DumbAwareAction(
- message("codewhisperer.codescan.stop_scan"),
- null,
- AllIcons.Actions.Suspend
-) {
- override fun getActionUpdateThread() = ActionUpdateThread.BGT
-
- override fun update(event: AnActionEvent) {
- val project = event.project ?: return
- val scanManager = CodeWhispererCodeScanManager.getInstance(project)
- event.presentation.isEnabled = scanManager.isCodeScanJobActive()
- }
-
- override fun actionPerformed(event: AnActionEvent) {
- val project = event.project ?: return
- CodeWhispererCodeScanManager.getInstance(project).stopCodeScan()
- }
-}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/context/CodeScanIssueDetailsDisplayType.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/context/CodeScanIssueDetailsDisplayType.kt
new file mode 100644
index 0000000000..4e148d08a3
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/context/CodeScanIssueDetailsDisplayType.kt
@@ -0,0 +1,8 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.context
+
+enum class CodeScanIssueDetailsDisplayType {
+ EditorPopup, DetailsPane
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanDocumentListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanDocumentListener.kt
index 4ac4bc7ada..265a573275 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanDocumentListener.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanDocumentListener.kt
@@ -45,6 +45,7 @@ internal class CodeWhispererCodeScanDocumentListener(val project: Project) : Doc
}
issue.rangeHighlighter?.textAttributes = null
issue.rangeHighlighter?.dispose()
+ scanManager.removeIssue(issue)
}
scanManager.updateScanNodes(file)
if (activeEditor != null && activeEditor.file == file &&
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 587da3f963..5e993efc54 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
@@ -6,50 +6,51 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.listener
import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
-import com.intellij.openapi.actionSystem.ActionManager
-import com.intellij.openapi.actionSystem.AnActionEvent
-import com.intellij.openapi.actionSystem.CommonDataKeys
-import com.intellij.openapi.actionSystem.DataKey
-import com.intellij.openapi.actionSystem.impl.SimpleDataContext
-import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.event.EditorMouseEventArea
import com.intellij.openapi.editor.event.EditorMouseMotionListener
import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
-import com.intellij.psi.PsiDocumentManager
-import com.intellij.ui.JBColor
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.JBScrollPane
-import icons.AwsIcons
-import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
-import software.aws.toolkits.core.utils.convertMarkdownToHTML
+import com.intellij.util.Alarm
import software.aws.toolkits.core.utils.debug
-import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
-import software.aws.toolkits.jetbrains.ToolkitPlaces
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.credentials.CodeWhispererClientAdaptor
-import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
-import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
+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.codeBlockBackgroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBorderColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockForegroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.deletionBackgroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.deletionForegroundColor
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.explainIssue
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.getCodeScanIssueDetailsHtml
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.getSeverityIcon
+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.sendCodeRemediationTelemetryToServiceApi
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.truncateIssueTitle
import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.getHexString
-import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CODE_SCAN_ISSUE_TITLE_MAX_LENGTH
-import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled
-import software.aws.toolkits.jetbrains.utils.applyPatch
-import software.aws.toolkits.jetbrains.utils.notifyError
-import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.resources.message
-import software.aws.toolkits.telemetry.Result
+import software.aws.toolkits.telemetry.CodeFixAction
+import software.aws.toolkits.telemetry.MetricResult
import java.awt.Dimension
+import java.awt.datatransfer.StringSelection
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
-import javax.swing.Icon
import javax.swing.JButton
import javax.swing.JEditorPane
import javax.swing.JLabel
@@ -64,114 +65,10 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
*/
private var currentPopupContext: ScanIssuePopupContext? = null
- private val codeBlockBackgroundColor = JBColor.namedColor("Editor.background", JBColor(0xf7f8fa, 0x2b2d30))
- private val codeBlockForegroundColor = JBColor.namedColor("Editor.foreground", JBColor(0x808080, 0xdfe1e5))
- private val codeBlockBorderColor = JBColor.namedColor("borderColor", JBColor(0xebecf0, 0x1e1f22))
- private val deletionBackgroundColor = JBColor.namedColor("FileColor.Rose", JBColor(0xf5c2c2, 0x511e1e))
- private val deletionForegroundColor = JBColor.namedColor("Label.errorForeground", JBColor(0xb63e3e, 0xfc6479))
- private val additionBackgroundColor = JBColor.namedColor("FileColor.Green", JBColor(0xdde9c1, 0x394323))
- private val additionForegroundColor = JBColor.namedColor("Label.successForeground", JBColor(0x42a174, 0xacc49e))
- private val metaBackgroundColor = JBColor.namedColor("FileColor.Blue", JBColor(0xeaf6ff, 0x4f556b))
- private val metaForegroundColor = JBColor.namedColor("Label.infoForeground", JBColor(0x808080, 0x8C8C8C))
-
private fun hidePopup() {
currentPopupContext?.popup?.cancel()
currentPopupContext = null
}
- private val issueDataKey = DataKey.create>("amazonq.codescan.explainissue")
-
- private fun getHtml(issue: CodeWhispererCodeScanIssue): String {
- // not sure why service team allows multiple remediations, but we only show one
- val suggestedFix = issue.suggestedFixes.firstOrNull()
-
- val cweLinks = if (issue.relatedVulnerabilities.isNotEmpty()) {
- issue.relatedVulnerabilities.joinToString(", ") { cwe ->
- "$cwe "
- }
- } else {
- "-"
- }
-
- val detectorLibraryLink = issue.recommendation.url?.let { "${issue.detectorName} " } ?: "-"
- val detectorSection = """
-
-
-
-
-
- ${message("codewhisperer.codescan.cwe_label")}
- ${message("codewhisperer.codescan.fix_available_label")}
- ${message("codewhisperer.codescan.detector_library_label")}
-
-
-
-
- $cweLinks
- ${if (suggestedFix != null) "Yes " else "No "}
- $detectorLibraryLink
-
-
-
- """.trimIndent()
-
- // add a link sections
- val explainButton = "${message(
- "codewhisperer.codescan.explain_button_label"
- )} "
- val linksSection = """
-
- •$explainButton
-
- """.trimIndent()
-
- val suggestedFixSection = suggestedFix?.let {
- val isFixDescriptionAvailable = it.description.isNotBlank() &&
- it.description.trim() != "Suggested remediation:"
- """
- |
- |
- |
- |## ${message("codewhisperer.codescan.suggested_fix_label")}
- |
- |```diff
- |${it.code}
- |```
- |
- |${
- if (isFixDescriptionAvailable) {
- "|### ${
- message(
- "codewhisperer.codescan.suggested_fix_description"
- )
- }\n${it.description}"
- } else {
- ""
- }
- }
- """.trimMargin()
- }
-
- return convertMarkdownToHTML(
- """
- |$linksSection
- |
- |${issue.recommendation.text}
- |
- |$detectorSection
- |
- |${suggestedFixSection.orEmpty()}
- """.trimMargin()
- )
- }
-
- private fun getSeverityIcon(issue: CodeWhispererCodeScanIssue): Icon? = when (issue.severity) {
- "Info" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INFO
- "Low" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_LOW
- "Medium" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_MEDIUM
- "High" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_HIGH
- "Critical" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_CRITICAL
- else -> null
- }
private fun showPopup(issues: List, e: EditorMouseEvent, issueIndex: Int = 0) {
if (issues.isEmpty()) {
@@ -182,7 +79,7 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
}
val issue = issues[issueIndex]
- val content = getHtml(issue)
+ val content = getCodeScanIssueDetailsHtml(issue, CodeScanIssueDetailsDisplayType.EditorPopup, project = project)
val kit = HTMLEditorKit()
kit.styleSheet.apply {
addRule("h1, h3 { margin-bottom: 0 }")
@@ -210,19 +107,34 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
addHyperlinkListener { he ->
if (he.eventType == HyperlinkEvent.EventType.ACTIVATED) {
when {
- he.description.startsWith("amazonq://issue/explain-") -> {
- val issueItemMap = mutableMapOf()
- issueItemMap["title"] = issue.title
- issueItemMap["description"] = issue.description.markdown
- issueItemMap["code"] = issue.codeText
- val myDataContext = SimpleDataContext.builder().add(issueDataKey, issueItemMap).add(CommonDataKeys.PROJECT, issue.project).build()
- val actionEvent = AnActionEvent.createFromInputEvent(
- he.inputEvent,
- ToolkitPlaces.EDITOR_PSI_REFERENCE,
- null,
- myDataContext
+ he.description.startsWith("amazonq://issue/openDiff-") -> {
+ openDiff(issue)
+ }
+ he.description.startsWith("amazonq://issue/copyDiff-") -> {
+ text = getCodeScanIssueDetailsHtml(
+ issue,
+ CodeScanIssueDetailsDisplayType.DetailsPane,
+ CodeWhispererConstants.FixGenerationState.COMPLETED,
+ true,
+ project = project
)
- ActionManager.getInstance().getAction("aws.amazonq.explainCodeScanIssue").actionPerformed(actionEvent)
+ CopyPasteManager.getInstance().setContents(StringSelection(issue.suggestedFixes.first().code))
+ val alarm = Alarm()
+ alarm.addRequest({
+ ApplicationManager.getApplication().invokeLater {
+ text = getCodeScanIssueDetailsHtml(
+ issue,
+ CodeScanIssueDetailsDisplayType.DetailsPane,
+ CodeWhispererConstants.FixGenerationState.COMPLETED,
+ false,
+ project = project
+ )
+ }
+ }, 500)
+ ApplicationManager.getApplication().executeOnPooledThread {
+ CodeWhispererTelemetryService.getInstance()
+ .sendCodeScanIssueApplyFixEvent(issue, MetricResult.Succeeded, codeFixAction = CodeFixAction.CopyDiff)
+ }
}
else -> {
BrowserUtil.browse(he.url)
@@ -240,7 +152,7 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED
}
- val label = JLabel(truncateTitle(issue.title)).apply {
+ val label = JLabel(truncateIssueTitle(issue.title)).apply {
icon = getSeverityIcon(issue)
horizontalTextPosition = JLabel.LEFT
}
@@ -249,7 +161,7 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
}
button.addActionListener {
- handleApplyFix(issue)
+ applySuggestedFix(project, issue)
button.isVisible = false
}
val nextButton = JButton(AllIcons.Actions.ArrowExpand).apply {
@@ -267,12 +179,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")
+ addActionListener {
+ hidePopup()
+ explainIssue(issue)
+ }
+ }
+
val titlePane = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
preferredSize = Dimension(this.width, 30)
- add(Box.createHorizontalGlue())
- add(label)
- add(Box.createHorizontalGlue())
+
+ // Add buttons first if they exist
if (issues.size > 1) {
add(prevButton)
add(JLabel("${issueIndex + 1} of ${issues.size}"))
@@ -282,6 +203,12 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
if (issue.suggestedFixes.isNotEmpty()) {
add(button)
}
+ add(explainButton)
+
+ // Add glue before and after label to center it
+ add(Box.createHorizontalGlue())
+ add(label)
+ add(Box.createHorizontalGlue())
}
val containerPane = JPanel().apply {
@@ -300,6 +227,7 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
CodeWhispererTelemetryService.getInstance().sendCodeScanIssueHoverEvent(issue)
sendCodeRemediationTelemetryToServiceApi(
+ project,
issue.file.programmingLanguage(),
"CODESCAN_ISSUE_HOVER",
issue.detectorId,
@@ -312,43 +240,6 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
)
}
- private fun sendCodeRemediationTelemetryToServiceApi(
- language: CodeWhispererProgrammingLanguage?,
- codeScanRemediationEventType: String?,
- detectorId: String?,
- findingId: String?,
- ruleId: String?,
- component: String?,
- reason: String?,
- result: String?,
- includesFix: Boolean?,
- ) {
- runIfIdcConnectionOrTelemetryEnabled(project) {
- pluginAwareExecuteOnPooledThread {
- try {
- val response = CodeWhispererClientAdaptor.getInstance(project)
- .sendCodeScanRemediationTelemetry(
- language,
- codeScanRemediationEventType,
- detectorId,
- findingId,
- ruleId,
- component,
- reason,
- result,
- includesFix
- )
- LOG.debug { "Successfully sent code scan remediation telemetry. RequestId: ${response.responseMetadata().requestId()}" }
- } catch (e: Exception) {
- val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null
- LOG.debug {
- "Failed to send code scan remediation telemetry. RequestId: $requestId, ErrorMessage: ${e.message}"
- }
- }
- }
- }
- }
-
override fun mouseMoved(e: EditorMouseEvent) {
val scanManager = CodeWhispererCodeScanManager.getInstance(project)
if (e.area != EditorMouseEventArea.EDITING_AREA || !e.isOverText) {
@@ -381,52 +272,4 @@ class CodeWhispererCodeScanEditorMouseMotionListener(private val project: Projec
companion object {
private val LOG = getLogger()
}
-
- private fun handleApplyFix(issue: CodeWhispererCodeScanIssue) {
- try {
- WriteCommandAction.runWriteCommandAction(issue.project) {
- val document = FileDocumentManager.getInstance().getDocument(issue.file) ?: return@runWriteCommandAction
-
- val documentContent = document.text
- val updatedContent = applyPatch(issue.suggestedFixes[0].code, documentContent, issue.file.name)
- document.replaceString(document.getLineStartOffset(0), document.getLineEndOffset(document.lineCount - 1), updatedContent)
- PsiDocumentManager.getInstance(issue.project).commitDocument(document)
- CodeWhispererTelemetryService.getInstance().sendCodeScanIssueApplyFixEvent(issue, Result.Succeeded)
- hidePopup()
- if (CodeWhispererExplorerActionManager.getInstance().isAutoEnabledForCodeScan()) {
- CodeWhispererCodeScanManager.getInstance(issue.project).removeIssueByFindingId(issue.file, issue.findingId)
- }
- }
- sendCodeRemediationTelemetryToServiceApi(
- issue.file.programmingLanguage(),
- "CODESCAN_ISSUE_APPLY_FIX",
- issue.detectorId,
- issue.findingId,
- issue.ruleId,
- null,
- null,
- Result.Succeeded.toString(),
- issue.suggestedFixes.isNotEmpty()
- )
- } catch (err: Error) {
- notifyError(message("codewhisperer.codescan.fix_applied_fail", err))
- LOG.error { "Apply fix command failed. $err" }
- CodeWhispererTelemetryService.getInstance().sendCodeScanIssueApplyFixEvent(issue, Result.Failed, err.message)
- sendCodeRemediationTelemetryToServiceApi(
- issue.file.programmingLanguage(),
- "CODESCAN_ISSUE_APPLY_FIX",
- issue.detectorId,
- issue.findingId,
- issue.ruleId,
- null,
- err.message,
- Result.Failed.toString(),
- issue.suggestedFixes.isNotEmpty()
- )
- }
- }
-
- private fun truncateTitle(title: String): String = title.takeUnless { it.length <= CODE_SCAN_ISSUE_TITLE_MAX_LENGTH }?.let {
- it.substring(0, CODE_SCAN_ISSUE_TITLE_MAX_LENGTH - 3) + "..."
- } ?: title
}
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 4360731fd3..3a217216ba 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
@@ -16,6 +16,7 @@ import com.intellij.openapi.vfs.isFile
import kotlinx.coroutines.runBlocking
import software.aws.toolkits.core.utils.createTemporaryZipFile
import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.putNextEntry
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
@@ -24,6 +25,10 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.cannotFin
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.fileTooLarge
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.noFileOpenError
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.noSupportedFilesError
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.AmazonQCodeReviewGitUtils.getGitRepositoryRoot
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.AmazonQCodeReviewGitUtils.getUnstagedFiles
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.AmazonQCodeReviewGitUtils.isGitRoot
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.AmazonQCodeReviewGitUtils.runGitDiffHead
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
@@ -46,6 +51,7 @@ class CodeScanSessionConfig(
private val selectedFile: VirtualFile?,
private val project: Project,
private val scope: CodeAnalysisScope,
+ private val initiatedByChat: Boolean,
) {
var projectRoot = project.basePath?.let { Path.of(it) }?.toFile()?.toVirtualFile() ?: run {
project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
@@ -56,6 +62,8 @@ class CodeScanSessionConfig(
val fileIndex = ProjectRootManager.getInstance(project).fileIndex
+ fun isInitiatedByChat(): Boolean = initiatedByChat
+
/**
* Timeout for the overall job - "Run Security Scan".
*/
@@ -99,7 +107,7 @@ class CodeScanSessionConfig(
else -> when (scope) {
CodeAnalysisScope.PROJECT -> getProjectPayloadMetadata()
CodeAnalysisScope.FILE -> if (selectedFile.path.startsWith(projectRoot.path)) {
- getFilePayloadMetadata(selectedFile)
+ getFilePayloadMetadata(selectedFile, true)
} else {
projectRoot = selectedFile.parent
getFilePayloadMetadata(selectedFile)
@@ -116,7 +124,7 @@ class CodeScanSessionConfig(
}
// Copy all the included source files to the source zip
- val srcZip = zipFiles(payloadMetadata.sourceFiles.map { Path.of(it) })
+ val srcZip = zipFiles(payloadMetadata.sourceFiles.map { Path.of(it) }, payloadMetadata.codeDiff)
val payloadContext = PayloadContext(
payloadMetadata.language,
payloadMetadata.linesScanned,
@@ -130,19 +138,69 @@ class CodeScanSessionConfig(
return Payload(payloadContext, srcZip)
}
- private fun getFilePayloadMetadata(file: VirtualFile): PayloadMetadata {
+ private fun getFilePayloadMetadata(file: VirtualFile, getCodeDiff: Boolean? = false): PayloadMetadata {
try {
+ val gitDiffContent = if (initiatedByChat && getCodeDiff == true) {
+ getFileGitDiffContent(file)
+ } else {
+ null
+ }
return PayloadMetadata(
setOf(file.path),
file.length,
countLinesInVirtualFile(file).toLong(),
- file.programmingLanguage().toTelemetryType()
+ file.programmingLanguage().toTelemetryType(),
+ gitDiffContent
)
} catch (e: Exception) {
cannotFindFile("File payload creation error: ${e.message}", file.path)
}
}
+ private fun getFileGitDiffContent(file: VirtualFile): String {
+ if (!file.exists()) {
+ LOG.debug { "File does not exist: ${file.path}" }
+ return ""
+ }
+ try {
+ getGitRepositoryRoot(file)?.let { root ->
+ // If it's a git repo, use git logic
+ val relativePath = runCatching {
+ file.toNioPath().relativeTo(root.toNioPath()).pathString
+ }.getOrElse {
+ LOG.debug { "Failed to calculate relative path: ${it.message}" }
+ return ""
+ }
+
+ return runGitDiffHead(
+ projectName = project.name,
+ root = root,
+ relativeFilePath = relativePath,
+ newFile = true
+ )
+ } ?: run {
+ // For non-git repos, use project root as base
+ val projectRootPath = projectRoot.toNioPath()
+ val relativePath = runCatching {
+ file.toNioPath().relativeTo(projectRootPath).pathString
+ }.getOrElse {
+ LOG.debug { "Failed to calculate relative path from project root: ${it.message}" }
+ return ""
+ }
+
+ return runGitDiffHead(
+ projectName = project.name,
+ root = projectRoot,
+ relativeFilePath = relativePath,
+ newFile = true // Always treat as new file for non-git repos
+ )
+ }
+ } catch (e: Exception) {
+ LOG.debug { "Failed to create git diff: ${e.message}" }
+ return ""
+ }
+ }
+
/**
* Timeout for creating the payload [createPayload]
*/
@@ -157,16 +215,29 @@ class CodeScanSessionConfig(
}
}
- private fun zipFiles(files: List): File = createTemporaryZipFile {
+ private fun zipFiles(files: List, codeDiff: String? = null): File = createTemporaryZipFile {
files.forEach { file ->
try {
- val relativePath = file.relativeTo(projectRoot.toNioPath())
+ val relativePath = "${project.name}/${file.relativeTo(projectRoot.toNioPath())}"
LOG.debug { "Selected file for truncation: $file" }
it.putNextEntry(relativePath.toString(), file)
} catch (e: Exception) {
cannotFindFile("Zipping error: ${e.message}", file.pathString)
}
}
+
+ codeDiff?.takeIf { diff ->
+ initiatedByChat && diff.isNotEmpty()
+ }?.let { diff ->
+ try {
+ LOG.debug { "Adding Code.Diff file to zip" }
+ diff.byteInputStream(Charsets.UTF_8).buffered().use { inputStream ->
+ it.putNextEntry("codeDiff/code.diff", inputStream)
+ }
+ } catch (e: Exception) {
+ LOG.error(e) { "Failed to add Code.Diff" }
+ }
+ }
}.toFile()
fun getProjectPayloadMetadata(): PayloadMetadata {
@@ -176,6 +247,7 @@ class CodeScanSessionConfig(
var currentTotalFileSize = 0L
var currentTotalLines = 0L
val languageCounts = mutableMapOf()
+ var gitDiffContent = ""
moduleLoop@ for (module in project.modules) {
val changeListManager = ChangeListManager.getInstance(module.project)
@@ -207,6 +279,26 @@ class CodeScanSessionConfig(
}
}
} else {
+ try {
+ if (isGitRoot(current)) {
+ LOG.debug { "$current is git directory" }
+ gitDiffContent = buildString {
+ append(runGitDiffHead(project.name, current))
+ getUnstagedFiles(current).takeIf { it.isNotEmpty() }?.let { unstagedFiles ->
+ unstagedFiles
+ .asSequence()
+ .map { relativePath -> runGitDiffHead(project.name, current, relativePath, true) }
+ .filter { it.isNotEmpty() }
+ .forEach { diff ->
+ if (isNotEmpty()) append('\n')
+ append(diff)
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ LOG.debug { "Error parsing the git diff for repository $current" }
+ }
// Directory case: only traverse if not ignored
if (!changeListManager.isIgnoredFile(current) &&
runBlocking { !featureDevSessionContext.ignoreFile(current) } &&
@@ -231,21 +323,19 @@ class CodeScanSessionConfig(
noSupportedFilesError()
}
programmingLanguage = maxCountLanguage
- return PayloadMetadata(files, currentTotalFileSize, currentTotalLines, maxCountLanguage.toTelemetryType())
- }
-
- fun getPath(root: String, relativePath: String = ""): Path? = try {
- Path.of(root, relativePath).normalize()
- } catch (e: Exception) {
- LOG.debug { "Cannot find file at path $relativePath relative to the root $root" }
- null
+ return PayloadMetadata(files, currentTotalFileSize, currentTotalLines, maxCountLanguage.toTelemetryType(), gitDiffContent)
}
fun File.toVirtualFile() = LocalFileSystem.getInstance().findFileByIoFile(this)
companion object {
private val LOG = getLogger()
- fun create(file: VirtualFile?, project: Project, scope: CodeAnalysisScope): CodeScanSessionConfig = CodeScanSessionConfig(file, project, scope)
+ fun create(file: VirtualFile?, project: Project, scope: CodeAnalysisScope, initiatedByChat: Boolean): CodeScanSessionConfig = CodeScanSessionConfig(
+ file,
+ project,
+ scope,
+ initiatedByChat
+ )
}
}
@@ -270,4 +360,5 @@ data class PayloadMetadata(
val payloadSize: Long,
val linesScanned: Long,
val language: CodewhispererLanguage,
+ val codeDiff: String? = null,
)
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/AmazonQCodeReviewGitUtils.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/AmazonQCodeReviewGitUtils.kt
new file mode 100644
index 0000000000..c4cab3fef3
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/AmazonQCodeReviewGitUtils.kt
@@ -0,0 +1,141 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils
+
+import com.intellij.execution.configurations.GeneralCommandLine
+import com.intellij.execution.util.ExecUtil
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.VirtualFile
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import java.io.File
+
+object AmazonQCodeReviewGitUtils {
+ private val LOG = getLogger()
+ private const val PROCESS_TIMEOUT_MS = 5000L
+
+ /**
+ * Executes a git command and returns the process output
+ */
+ private fun executeGitCommand(
+ workDir: File,
+ vararg parameters: String,
+ timeoutMs: Long = PROCESS_TIMEOUT_MS,
+ ): Pair {
+ val commandLine = GeneralCommandLine().apply {
+ workDirectory = workDir
+ exePath = "git"
+ addParameters(*parameters)
+ }
+
+ return try {
+ val output = ExecUtil.execAndGetOutput(commandLine, timeoutMs.toInt())
+ if (output.exitCode != 0) {
+ LOG.debug { "Git command failed with exit code ${output.exitCode}: ${output.stderr}" }
+ }
+ Pair(output.stdout.trim(), output.stderr.trim())
+ } catch (e: Exception) {
+ LOG.debug(e) { "Git command failed: ${commandLine.commandLineString}" }
+ Pair("", e.message ?: "Unknown error")
+ }
+ }
+
+ fun runGitDiffHead(
+ projectName: String,
+ root: VirtualFile,
+ relativeFilePath: String? = null,
+ newFile: Boolean? = false,
+ ): String {
+ if (!root.exists()) {
+ LOG.debug { "Root directory does not exist: ${root.path}" }
+ return ""
+ }
+
+ val prefixes = arrayOf(
+ "--src-prefix=a/$projectName/",
+ "--dst-prefix=b/$projectName/"
+ )
+ val ref = if (SystemInfo.isWindows) "NUL" else "/dev/null"
+
+ val parameters = when {
+ relativeFilePath == null -> arrayOf("diff", "HEAD") + prefixes
+ newFile == true -> arrayOf("diff", "--no-index", *prefixes, ref, relativeFilePath)
+ else -> arrayOf("diff", "HEAD", *prefixes, relativeFilePath)
+ }
+
+ val (output, error) = executeGitCommand(File(root.path), *parameters)
+
+ return when {
+ error.contains("Authentication failed") -> {
+ LOG.debug { "Git Authentication Failed" }
+ throw RuntimeException("Git Authentication Failed")
+ }
+ error.isNotEmpty() -> {
+ LOG.debug { "Git command failed: $error" }
+ ""
+ }
+ else -> output
+ }
+ }
+
+ fun isGitRoot(file: VirtualFile): Boolean {
+ if (!file.exists()) return false
+
+ val workDir = if (file.isDirectory) File(file.path) else File(file.parent.path)
+ val (output, _) = executeGitCommand(workDir, "rev-parse", "--git-dir")
+
+ return output == ".git"
+ }
+
+ fun getGitRepositoryRoot(file: VirtualFile): VirtualFile? {
+ if (!file.exists()) return null
+
+ val workDir = if (file.isDirectory) {
+ File(file.path)
+ } else {
+ File(file.parent.path)
+ }
+
+ if (!workDir.exists() || !workDir.isDirectory) {
+ LOG.debug { "Invalid working directory: ${workDir.path}" }
+ return null
+ }
+
+ val (output, error) = executeGitCommand(workDir, "rev-parse", "--show-toplevel")
+
+ return when {
+ error.isNotEmpty() -> {
+ LOG.debug { "Failed to get git root: $error" }
+ null
+ }
+ output.isEmpty() -> null
+ else -> LocalFileSystem.getInstance().findFileByPath(output)
+ }
+ }
+
+ fun getUnstagedFiles(root: VirtualFile): List {
+ if (!root.exists()) return emptyList()
+
+ val (output, error) = executeGitCommand(
+ File(root.path),
+ "ls-files",
+ "--others",
+ "--exclude-standard"
+ )
+
+ return when {
+ error.isNotEmpty() -> {
+ LOG.debug { "Failed to get unstaged files: $error" }
+ emptyList()
+ }
+ else -> output.split("\n").filter { it.isNotEmpty() }
+ }
+ }
+
+ fun isInsideWorkTree(folder: VirtualFile): Boolean {
+ val (output) = executeGitCommand(File(folder.path), "rev-parse", "--is-inside-work-tree")
+ return output == "true"
+ }
+}
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
new file mode 100644
index 0000000000..7848823da3
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/utils/CodeWhispererCodeScanIssueUtils.kt
@@ -0,0 +1,441 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils
+
+import com.intellij.diff.DiffContentFactory
+import com.intellij.diff.DiffManager
+import com.intellij.diff.requests.SimpleDiffRequest
+import com.intellij.diff.util.DiffUserDataKeys
+import com.intellij.openapi.actionSystem.ActionManager
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.CommonDataKeys
+import com.intellij.openapi.actionSystem.DataKey
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.vfs.VfsUtil
+import com.intellij.openapi.vfs.VfsUtilCore
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.psi.PsiDocumentManager
+import com.intellij.ui.JBColor
+import icons.AwsIcons
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
+import software.aws.toolkits.core.utils.convertMarkdownToHTML
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.ToolkitPlaces
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanHighlightingFilesPanel
+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.SuggestedFix
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.context.CodeScanIssueDetailsDisplayType
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
+import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
+import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CODE_SCAN_ISSUE_TITLE_MAX_LENGTH
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled
+import software.aws.toolkits.jetbrains.utils.applyPatch
+import software.aws.toolkits.jetbrains.utils.notifyError
+import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
+import software.aws.toolkits.resources.message
+import software.aws.toolkits.telemetry.CodeFixAction
+import software.aws.toolkits.telemetry.Result
+import javax.swing.Icon
+
+val codeBlockBackgroundColor = JBColor.namedColor("Editor.background", JBColor(0xf7f8fa, 0x2b2d30))
+val codeBlockForegroundColor = JBColor.namedColor("Editor.foreground", JBColor(0x808080, 0xdfe1e5))
+val codeBlockBorderColor = JBColor.namedColor("borderColor", JBColor(0xebecf0, 0x1e1f22))
+val deletionBackgroundColor = JBColor.namedColor("FileColor.Rose", JBColor(0xf5c2c2, 0x511e1e))
+val deletionForegroundColor = JBColor.namedColor("Label.errorForeground", JBColor(0xb63e3e, 0xfc6479))
+val additionBackgroundColor = JBColor.namedColor("FileColor.Green", JBColor(0xdde9c1, 0x394323))
+val additionForegroundColor = JBColor.namedColor("Label.successForeground", JBColor(0x42a174, 0xacc49e))
+val metaBackgroundColor = JBColor.namedColor("FileColor.Blue", JBColor(0xeaf6ff, 0x4f556b))
+val metaForegroundColor = JBColor.namedColor("Label.infoForeground", JBColor(0x808080, 0x8C8C8C))
+
+private val LOG = getLogger()
+private val explainIssueDataKey = DataKey.create>("amazonq.codescan.explainissue")
+
+enum class IssueSeverity(val displayName: String) {
+ CRITICAL("Critical"),
+ HIGH("High"),
+ MEDIUM("Medium"),
+ LOW("Low"),
+ INFO("Info"),
+}
+
+fun getCodeScanIssueDetailsHtml(
+ issue: CodeWhispererCodeScanIssue,
+ display: CodeScanIssueDetailsDisplayType,
+ fixGenerationState: CodeWhispererConstants.FixGenerationState = CodeWhispererConstants.FixGenerationState.COMPLETED,
+ isCopied: Boolean = false,
+ project: Project,
+ showReferenceWarning: Boolean? = false,
+): String {
+ val suggestedFix = issue.suggestedFixes.firstOrNull()
+
+ val cweLinks = if (issue.relatedVulnerabilities.isNotEmpty()) {
+ issue.relatedVulnerabilities.joinToString(", ") { cwe ->
+ "$cwe "
+ }
+ } else {
+ "-"
+ }
+
+ val projectRoot = project.basePath?.let { VirtualFileManager.getInstance().findFileByUrl(VfsUtilCore.pathToUrl(it)) } ?: project.guessProjectDir()
+ val filePathString = projectRoot?.let { VfsUtil.getRelativePath(issue.file, it) } ?: issue.file.path
+
+ val fileLink = "${ filePathString } [Ln ${issue.startLine}] "
+
+ val detectorLibraryLink = issue.recommendation.url?.let { "${issue.detectorName} " } ?: "-"
+ val detectorSection = """
+
+
+
+
+
+ ${message("codewhisperer.codescan.cwe_label")}
+ ${message("codewhisperer.codescan.detector_library_label")}
+
+
+
+
+ $cweLinks
+ $detectorLibraryLink
+
+
+
+
+
+
+ ${message("codewhisperer.codescan.file_path_label")}
+
+
+
+
+ $fileLink
+
+
+
+ """.trimIndent()
+
+ val suggestedFixSection = if (showReferenceWarning == false) {
+ createSuggestedFixSection(issue, suggestedFix, isCopied)
+ } else {
+ """
+
+ Your settings do not allow code generation with references.
+
+ """.trimIndent()
+ }
+
+ val fixLoadingSection = """
+
+
+ ...
+
+
+ """.trimIndent()
+
+ val fixFailureSection = """
+
+
+ Amazon Q failed to generate fix. Please try again
+
+
+ """.trimIndent()
+
+ val commonContent = """
+ |${issue.recommendation.text}
+ |
+ |$detectorSection
+ |
+ |${when (fixGenerationState) {
+ CodeWhispererConstants.FixGenerationState.COMPLETED -> suggestedFixSection.orEmpty()
+ CodeWhispererConstants.FixGenerationState.GENERATING -> fixLoadingSection
+ CodeWhispererConstants.FixGenerationState.FAILED -> fixFailureSection
+ }}
+ """.trimMargin()
+
+ if (display == CodeScanIssueDetailsDisplayType.EditorPopup) {
+ return convertMarkdownToHTML(
+ """
+ |$commonContent
+ """.trimMargin()
+ )
+ }
+
+ return convertMarkdownToHTML(commonContent)
+}
+
+private fun createSuggestedFixSection(issue: CodeWhispererCodeScanIssue, suggestedFix: SuggestedFix?, isCopied: Boolean = false): String? = suggestedFix?.let {
+ val isFixDescriptionAvailable = it.description.isNotBlank() &&
+ it.description.trim() != "Suggested remediation:"
+ """
+ |
+ |
+ |### ${message("codewhisperer.codescan.suggested_fix_label")}
+ |
+ |
+ |
+ |
+ |
+ |
+ |```diff
+ |${it.code.trim()}
+ |```
+ |
+ |
+ |
+ |
+ |${
+ if (isFixDescriptionAvailable) {
+ "|### ${
+ message(
+ "codewhisperer.codescan.suggested_fix_description"
+ )
+ }\n${it.description}"
+ } else {
+ ""
+ }
+ }
+ """.trimMargin()
+}
+
+fun explainIssue(issue: CodeWhispererCodeScanIssue) {
+ val explainIssueContext = mutableMapOf(
+ "title" to issue.title,
+ "description" to issue.description.markdown,
+ "code" to issue.codeText
+ )
+ val actionEvent = AnActionEvent.createFromInputEvent(
+ null,
+ ToolkitPlaces.EDITOR_PSI_REFERENCE,
+ null,
+ SimpleDataContext.builder().add(explainIssueDataKey, explainIssueContext).add(CommonDataKeys.PROJECT, issue.project).build()
+ )
+ ActionManager.getInstance().getAction("aws.amazonq.explainCodeScanIssue").actionPerformed(actionEvent)
+}
+
+fun openDiff(issue: CodeWhispererCodeScanIssue) {
+ val diffContentFactory = DiffContentFactory.getInstance()
+ val document = FileDocumentManager.getInstance().getDocument(issue.file)
+ document?.text?.let { documentContent ->
+ val updatedContent = applyPatch(issue.suggestedFixes[0].code, documentContent, issue.file.name)
+ val (originalContent, suggestedContent) = try {
+ diffContentFactory.create(documentContent) to
+ diffContentFactory.create(updatedContent)
+ } catch (e: Exception) {
+ ApplicationManager.getApplication().executeOnPooledThread {
+ CodeWhispererTelemetryService.getInstance().sendCodeScanIssueApplyFixEvent(
+ issue,
+ Result.Failed,
+ e.message,
+ codeFixAction = CodeFixAction.OpenDiff
+ )
+ }
+ return@let null
+ }
+
+ val request = SimpleDiffRequest(
+ "Amazon Q Code Suggestion Diff",
+ suggestedContent,
+ originalContent,
+ "Suggested fix",
+ "Original code"
+ ).apply {
+ putUserData(DiffUserDataKeys.MERGE_EDITOR_FLAG, true)
+
+ putUserData(DiffUserDataKeys.DO_NOT_IGNORE_WHITESPACES, true)
+
+ putUserData(DiffUserDataKeys.ENABLE_SEARCH_IN_CHANGES, true)
+ putUserData(DiffUserDataKeys.GO_TO_SOURCE_DISABLE, false)
+
+ putUserData(DiffUserDataKeys.ALIGNED_TWO_SIDED_DIFF, true)
+ putUserData(DiffUserDataKeys.FORCE_READ_ONLY_CONTENTS, booleanArrayOf(true, false))
+ putUserData(DiffUserDataKeys.FORCE_READ_ONLY, false)
+ }
+ ApplicationManager.getApplication().invokeLater {
+ DiffManager.getInstance().showDiff(
+ issue.project,
+ request
+ )
+ }
+ }
+ ApplicationManager.getApplication().executeOnPooledThread {
+ CodeWhispererTelemetryService.getInstance().sendCodeScanIssueApplyFixEvent(issue, Result.Succeeded, codeFixAction = CodeFixAction.OpenDiff)
+ }
+}
+
+fun truncateIssueTitle(title: String): String = title.takeUnless { it.length <= CODE_SCAN_ISSUE_TITLE_MAX_LENGTH }?.let {
+ it.substring(0, CODE_SCAN_ISSUE_TITLE_MAX_LENGTH - 3) + "..."
+} ?: title
+
+fun sendCodeRemediationTelemetryToServiceApi(
+ project: Project,
+ language: CodeWhispererProgrammingLanguage?,
+ codeScanRemediationEventType: String?,
+ detectorId: String?,
+ findingId: String?,
+ ruleId: String?,
+ component: String?,
+ reason: String?,
+ result: String?,
+ includesFix: Boolean?,
+) {
+ runIfIdcConnectionOrTelemetryEnabled(project) {
+ pluginAwareExecuteOnPooledThread {
+ try {
+ val response = CodeWhispererClientAdaptor.getInstance(project)
+ .sendCodeScanRemediationTelemetry(
+ language,
+ codeScanRemediationEventType,
+ detectorId,
+ findingId,
+ ruleId,
+ component,
+ reason,
+ result,
+ includesFix
+ )
+ LOG.debug { "Successfully sent code scan remediation telemetry. RequestId: ${response.responseMetadata().requestId()}" }
+ } catch (e: Exception) {
+ val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null
+ LOG.debug(e) {
+ "Failed to send code scan remediation telemetry. RequestId: $requestId"
+ }
+ }
+ }
+ }
+}
+
+fun applySuggestedFix(project: Project, issue: CodeWhispererCodeScanIssue) {
+ try {
+ val manager = CodeWhispererCodeReferenceManager.getInstance(issue.project)
+ WriteCommandAction.runWriteCommandAction(issue.project) {
+ val document = FileDocumentManager.getInstance().getDocument(issue.file) ?: return@runWriteCommandAction
+
+ val documentContent = document.text
+ val updatedContent = applyPatch(issue.suggestedFixes[0].code, documentContent, issue.file.name)
+ document.replaceString(document.getLineStartOffset(0), document.getLineEndOffset(document.lineCount - 1), updatedContent)
+ PsiDocumentManager.getInstance(issue.project).commitDocument(document)
+ issue.suggestedFixes[0].references.forEach { reference ->
+ LOG.debug { "Applied fix with reference: $reference" }
+ val originalContent = updatedContent.substring(reference.recommendationContentSpan().start(), reference.recommendationContentSpan().end())
+ LOG.debug { "Original content from reference span: $originalContent" }
+ manager.addReferenceLogPanelEntry(reference = reference, null, null, originalContent.split("\n"))
+ }
+ }
+ if (issue.suggestedFixes[0].references.isNotEmpty()) {
+ manager.toolWindow?.show()
+ }
+ if (CodeWhispererExplorerActionManager.getInstance().isAutoEnabledForCodeScan()) {
+ CodeWhispererCodeScanManager.getInstance(issue.project).removeIssueByFindingId(issue, issue.findingId)
+ }
+ ApplicationManager.getApplication().executeOnPooledThread {
+ CodeWhispererTelemetryService.getInstance().sendCodeScanIssueApplyFixEvent(issue, Result.Succeeded, codeFixAction = CodeFixAction.ApplyFix)
+ }
+ sendCodeRemediationTelemetryToServiceApi(
+ project,
+ issue.file.programmingLanguage(),
+ "CODESCAN_ISSUE_APPLY_FIX",
+ issue.detectorId,
+ issue.findingId,
+ issue.ruleId,
+ null,
+ null,
+ Result.Succeeded.toString(),
+ issue.suggestedFixes.isNotEmpty()
+ )
+ sendCodeFixGeneratedTelemetryToServiceAPI(issue, true)
+ } catch (e: Throwable) {
+ notifyError(message("codewhisperer.codescan.fix_applied_fail", e))
+ LOG.debug(e) { "Apply fix command failed." }
+ ApplicationManager.getApplication().executeOnPooledThread {
+ CodeWhispererTelemetryService.getInstance().sendCodeScanIssueApplyFixEvent(issue, Result.Failed, e.message, codeFixAction = CodeFixAction.ApplyFix)
+ sendCodeRemediationTelemetryToServiceApi(
+ project,
+ issue.file.programmingLanguage(),
+ "CODESCAN_ISSUE_APPLY_FIX",
+ issue.detectorId,
+ issue.findingId,
+ issue.ruleId,
+ null,
+ e.message,
+ Result.Failed.toString(),
+ issue.suggestedFixes.isNotEmpty()
+ )
+ }
+ }
+}
+
+fun getSeverityIcon(issue: CodeWhispererCodeScanIssue): Icon? = when (issue.severity) {
+ "Info" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_INFO
+ "Low" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_LOW
+ "Medium" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_MEDIUM
+ "High" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_HIGH
+ "Critical" -> AwsIcons.Resources.CodeWhisperer.SEVERITY_CRITICAL
+ else -> null
+}
+
+fun sendCodeFixGeneratedTelemetryToServiceAPI(
+ issue: CodeWhispererCodeScanIssue,
+ acceptFix: Boolean,
+) {
+ runIfIdcConnectionOrTelemetryEnabled(issue.project) {
+ pluginAwareExecuteOnPooledThread {
+ try {
+ val client = CodeWhispererClientAdaptor.getInstance(issue.project)
+ if (acceptFix) {
+ val acceptFixResponse = client.sendCodeFixAcceptanceTelemetry(
+ issue.file.programmingLanguage(),
+ issue.suggestedFixes.first().codeFixJobId,
+ issue.ruleId,
+ issue.detectorId,
+ issue.findingId,
+ issue.suggestedFixes.first().code.split("\n").size - 1,
+ issue.suggestedFixes.first().code.length
+ )
+ LOG.debug {
+ "Successfully sent code fix acceptance telemetry. RequestId: ${
+ acceptFixResponse.responseMetadata().requestId()
+ }"
+ }
+ } else {
+ val generateFixResponse = client.sendCodeFixGenerationTelemetry(
+ issue.file.programmingLanguage(),
+ issue.suggestedFixes.first().codeFixJobId,
+ issue.ruleId,
+ issue.detectorId,
+ issue.findingId,
+ issue.suggestedFixes.first().code.split("\n").size - 1,
+ issue.suggestedFixes.first().code.length
+ )
+ LOG.debug {
+ "Successfully sent code fix generated telemetry. RequestId: ${
+ generateFixResponse.responseMetadata().requestId()
+ }"
+ }
+ }
+ } catch (e: Exception) {
+ val requestId = if (e is CodeWhispererRuntimeException) e.requestId() else null
+ LOG.debug { "Failed to send code fix telemetry. RequestId: $requestId, ErrorMessage: ${e.message}" }
+ }
+ }
+ }
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt
new file mode 100644
index 0000000000..fcd2fbfcab
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt
@@ -0,0 +1,247 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.codetest.sessionconfig
+
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessModuleDir
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.project.modules
+import com.intellij.openapi.roots.ProjectRootManager
+import com.intellij.openapi.vcs.changes.ChangeListManager
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.isFile
+import kotlinx.coroutines.runBlocking
+import software.aws.toolkits.core.utils.createTemporaryZipFile
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.putNextEntry
+import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.cannotFindBuildArtifacts
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.cannotFindFile
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.fileTooLarge
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.noFileOpenError
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.Payload
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadContext
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadMetadata
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.CODE_SCAN_CREATE_PAYLOAD_TIMEOUT_IN_SECONDS
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.DEFAULT_CODE_SCAN_TIMEOUT_IN_SECONDS
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.DEFAULT_PAYLOAD_LIMIT_IN_BYTES
+import software.aws.toolkits.resources.message
+import java.io.File
+import java.nio.file.Path
+import java.time.Instant
+import java.util.Stack
+import java.util.zip.ZipEntry
+import kotlin.io.path.name
+import kotlin.io.path.relativeTo
+
+// TODO: share huge duplicates with CodeScanSessionConfig need to abstract to a ZipSessionConfig
+class CodeTestSessionConfig(
+ private val selectedFile: VirtualFile?,
+ private val project: Project,
+ private val buildAndExecuteLogFile: VirtualFile? = null,
+) {
+ val projectRoot = project.basePath?.let { Path.of(it) }?.toFile()?.toVirtualFile() ?: run {
+ project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
+ }
+
+ private val featureDevSessionContext = FeatureDevSessionContext(project)
+
+ val fileIndex = ProjectRootManager.getInstance(project).fileIndex
+
+ /**
+ * return default timeout
+ */
+ fun overallJobTimeoutInSeconds(): Long = DEFAULT_CODE_SCAN_TIMEOUT_IN_SECONDS
+
+ fun getPayloadLimitInBytes(): Long = DEFAULT_PAYLOAD_LIMIT_IN_BYTES
+
+ private fun willExceedPayloadLimit(currentTotalFileSize: Long, currentFileSize: Long): Boolean =
+ currentTotalFileSize.let { totalSize -> totalSize > (getPayloadLimitInBytes() - currentFileSize) }
+
+ private var programmingLanguage: CodeWhispererProgrammingLanguage = selectedFile?.programmingLanguage() ?: CodeWhispererUnknownLanguage.INSTANCE
+
+ fun getProgrammingLanguage(): CodeWhispererProgrammingLanguage = programmingLanguage
+
+ fun getSelectedFile(): VirtualFile? = selectedFile
+
+ fun createPayload(): Payload {
+ // Fail fast if the selected file is null for UTG
+ if (selectedFile == null) {
+ noFileOpenError()
+ }
+
+ // Fail fast if the selected file size is greater than the payload limit.
+ if (selectedFile.length > getPayloadLimitInBytes()) {
+ fileTooLarge()
+ }
+
+ val start = Instant.now().toEpochMilli()
+
+ LOG.debug { "Creating payload. File selected as root for the context truncation: ${projectRoot.path}" }
+
+ val payloadMetadata: PayloadMetadata = try {
+ getProjectPayloadMetadata()
+ } catch (e: Exception) {
+ val errorMessage = when {
+ e.message?.contains("Illegal repetition near index") == true -> "Illegal repetition near index"
+ else -> e.message
+ }
+ LOG.debug { "Error creating payload metadata: $errorMessage" }
+ throw cannotFindBuildArtifacts(errorMessage ?: message("codewhisperer.codescan.run_scan_error_telemetry"))
+ }
+
+ // Copy all the included source files to the source zip
+ val srcZip = zipFiles(payloadMetadata.sourceFiles.map { Path.of(it) })
+ val payloadContext = PayloadContext(
+ payloadMetadata.language,
+ payloadMetadata.linesScanned,
+ payloadMetadata.sourceFiles.size,
+ Instant.now().toEpochMilli() - start,
+ payloadMetadata.sourceFiles.mapNotNull { Path.of(it).toFile().toVirtualFile() },
+ payloadMetadata.payloadSize,
+ srcZip.length()
+ )
+
+ return Payload(payloadContext, srcZip)
+ }
+
+ /**
+ * Timeout for creating the payload [createPayload]
+ */
+ fun createPayloadTimeoutInSeconds(): Long = CODE_SCAN_CREATE_PAYLOAD_TIMEOUT_IN_SECONDS
+
+ private fun countLinesInVirtualFile(virtualFile: VirtualFile): Int {
+ try {
+ val bufferedReader = virtualFile.inputStream.bufferedReader()
+ return bufferedReader.useLines { lines -> lines.count() }
+ } catch (e: Exception) {
+ cannotFindFile("Line count error: ${e.message}", virtualFile.path)
+ }
+ }
+
+ private fun zipFiles(files: List): File = createTemporaryZipFile {
+ files.forEach { file ->
+ try {
+ val relativePath = file.relativeTo(projectRoot.toNioPath())
+ val projectBaseName = projectRoot.name
+ val zipEntryPath = "$projectBaseName/${relativePath.toString().replace("\\", "/")}"
+ LOG.debug { "Adding file to ZIP: $zipEntryPath" }
+ it.putNextEntry(zipEntryPath, file)
+ } catch (e: Exception) {
+ cannotFindFile("Zipping error: ${e.message}", file.toString())
+ }
+ }
+
+ // 2. Add the "utgRequiredArtifactsDir" directory
+ val utgDir = "utgRequiredArtifactsDir"
+ LOG.debug { "Adding directory to ZIP: $utgDir" }
+ val utgEntry = ZipEntry(utgDir)
+ it.putNextEntry(utgEntry)
+
+ // 3. Add the three empty subdirectories
+ val buildAndExecuteLogDir = "buildAndExecuteLogDir"
+ val subDirs = listOf(buildAndExecuteLogDir, "repoMapData", "testCoverageDir")
+ subDirs.forEach { subDir ->
+ val subDirPathString = Path.of(utgDir, subDir).name
+ LOG.debug { "Adding empty directory to ZIP: $subDirPathString" }
+ val zipEntry = ZipEntry(subDirPathString)
+ it.putNextEntry(zipEntry)
+ }
+ if (buildAndExecuteLogFile != null) {
+ it.putNextEntry(Path.of(utgDir, buildAndExecuteLogDir, "buildAndExecuteLog").name, buildAndExecuteLogFile.inputStream)
+ }
+ }.toFile()
+
+ fun getProjectPayloadMetadata(): PayloadMetadata {
+ val files = mutableSetOf()
+ val traversedDirectories = mutableSetOf()
+ val stack = Stack()
+ var currentTotalFileSize = 0L
+ var currentTotalLines = 0L
+ val languageCounts = mutableMapOf()
+
+ moduleLoop@ for (module in project.modules) {
+ val changeListManager = ChangeListManager.getInstance(module.project)
+ if (module.guessModuleDir() != null) {
+ stack.push(module.guessModuleDir())
+ while (stack.isNotEmpty()) {
+ val current = stack.pop()
+
+ if (!current.isDirectory) {
+ if (current.isFile && !changeListManager.isIgnoredFile(current) &&
+ runBlocking { !featureDevSessionContext.ignoreFile(current) } &&
+ runReadAction { !fileIndex.isInLibrarySource(current) }
+ ) {
+ if (willExceedPayloadLimit(currentTotalFileSize, current.length)) {
+ fileTooLarge()
+ } else {
+ try {
+ val language = current.programmingLanguage()
+ if (language !is CodeWhispererUnknownLanguage) {
+ languageCounts[language] = (languageCounts[language] ?: 0) + 1
+ }
+ files.add(current.path)
+ currentTotalFileSize += current.length
+ currentTotalLines += countLinesInVirtualFile(current)
+ } catch (e: Exception) {
+ LOG.debug { "Error parsing the file: ${current.path} with error: ${e.message}" }
+ continue
+ }
+ }
+ }
+ } else {
+ // Directory case: only traverse if not ignored
+ if (!changeListManager.isIgnoredFile(current) &&
+ runBlocking { !featureDevSessionContext.ignoreFile(current) } &&
+ !traversedDirectories.contains(current) && current.isValid &&
+ runReadAction { !fileIndex.isInLibrarySource(current) }
+ ) {
+ for (child in current.children) {
+ stack.push(child)
+ }
+ }
+ traversedDirectories.add(current)
+ }
+ }
+ }
+ }
+
+ val maxCount = languageCounts.maxByOrNull { it.value }?.value ?: 0
+ val maxCountLanguage = languageCounts.filter { it.value == maxCount }.keys.firstOrNull()
+
+ if (maxCountLanguage == null) {
+ programmingLanguage = CodeWhispererUnknownLanguage.INSTANCE
+ throw RuntimeException("Amazon Q: doesn't contain valid files to generate tests")
+ }
+ programmingLanguage = maxCountLanguage
+ return PayloadMetadata(files, currentTotalFileSize, currentTotalLines, maxCountLanguage.toTelemetryType())
+ }
+
+ fun getPath(root: String, relativePath: String = ""): Path? = try {
+ Path.of(root, relativePath).normalize()
+ } catch (e: Exception) {
+ LOG.debug { "Cannot find file at path $relativePath relative to the root $root" }
+ null
+ }
+
+ fun getRelativePath(): Path? = try {
+ selectedFile?.path?.let { Path.of(projectRoot.path).relativize(Path.of(it)).normalize() }
+ } catch (e: Exception) {
+ LOG.debug { "Cannot calculate relative path of $selectedFile with respect to $projectRoot" }
+ null
+ }
+
+ fun File.toVirtualFile() = LocalFileSystem.getInstance().findFileByIoFile(this)
+
+ companion object {
+ private val LOG = getLogger()
+ fun create(file: VirtualFile?, project: Project): CodeTestSessionConfig = CodeTestSessionConfig(file, project, null)
+ }
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt
index 27feadf917..d1ecae07fa 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt
@@ -24,11 +24,18 @@ import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUr
import software.amazon.awssdk.services.codewhispererruntime.model.Dimension
import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest
import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.GetTestGenerationResponse
import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision
import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsRequest
import software.amazon.awssdk.services.codewhispererruntime.model.ListFeatureEvaluationsResponse
import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryEventResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeFixJobRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeFixJobResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.StartTestGenerationResponse
import software.amazon.awssdk.services.codewhispererruntime.model.SuggestionState
+import software.amazon.awssdk.services.codewhispererruntime.model.TargetCode
import software.amazon.awssdk.services.codewhispererruntime.model.UserIntent
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
@@ -87,8 +94,16 @@ interface CodeWhispererClientAdaptor : Disposable {
isSigv4: Boolean = shouldUseSigv4Client(project),
): ListCodeScanFindingsResponse
+ fun startCodeFixJob(request: StartCodeFixJobRequest): StartCodeFixJobResponse
+
+ fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse
+
fun listAvailableCustomizations(): List
+ fun startTestGeneration(uploadId: String, targetCode: List, userInput: String): StartTestGenerationResponse
+
+ fun getTestGeneration(jobId: String, jobGroupName: String): GetTestGenerationResponse
+
fun sendUserTriggerDecisionTelemetry(
requestContext: RequestContext,
responseContext: ResponseContext,
@@ -135,6 +150,39 @@ interface CodeWhispererClientAdaptor : Disposable {
scope: CodeWhispererConstants.CodeAnalysisScope,
): SendTelemetryEventResponse
+ fun sendCodeScanSucceededTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeScanJobId: String?,
+ scope: CodeWhispererConstants.CodeAnalysisScope,
+ findings: Int,
+ ): SendTelemetryEventResponse
+
+ fun sendCodeScanFailedTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeScanJobId: String?,
+ scope: CodeWhispererConstants.CodeAnalysisScope,
+ ): SendTelemetryEventResponse
+
+ fun sendCodeFixGenerationTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeFixJobId: String?,
+ ruleId: String?,
+ detectorId: String?,
+ findingId: String?,
+ linesOfCodeGenerated: Int?,
+ charsOfCodeGenerated: Int?,
+ ): SendTelemetryEventResponse
+
+ fun sendCodeFixAcceptanceTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeFixJobId: String?,
+ ruleId: String?,
+ detectorId: String?,
+ findingId: String?,
+ linesOfCodeGenerated: Int?,
+ charsOfCodeGenerated: Int?,
+ ): SendTelemetryEventResponse
+
fun sendCodeScanRemediationTelemetry(
language: CodeWhispererProgrammingLanguage?,
codeScanRemediationEventType: String?,
@@ -146,6 +194,19 @@ interface CodeWhispererClientAdaptor : Disposable {
result: String?,
includesFix: Boolean?,
): SendTelemetryEventResponse
+
+ fun sendTestGenerationEvent(
+ jobId: String,
+ groupName: String,
+ language: CodeWhispererProgrammingLanguage?,
+ numberOfUnitTestCasesGenerated: Int?,
+ numberOfUnitTestCasesAccepted: Int?,
+ linesOfCodeGenerated: Int?,
+ linesOfCodeAccepted: Int?,
+ charsOfCodeGenerated: Int?,
+ charsOfCodeAccepted: Int?,
+ ): SendTelemetryEventResponse
+
fun listFeatureEvaluations(): ListFeatureEvaluationsResponse
fun sendMetricDataTelemetry(eventName: String, metadata: Map): SendTelemetryEventResponse
@@ -210,6 +271,7 @@ interface CodeWhispererClientAdaptor : Disposable {
CodeWhispererExplorerActionManager.getInstance().checkActiveCodeWhispererConnectionType(project) == CodeWhispererLoginType.Accountless
const val INVALID_CODESCANJOBID = "Invalid_CodeScanJobID"
+ const val INVALID_CODEFIXJOBID = "Invalid_CodeFixJobID"
}
}
@@ -281,6 +343,10 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
bearerClient().listCodeAnalysisFindings(request.transform()).transform()
}
+ override fun startCodeFixJob(request: StartCodeFixJobRequest): StartCodeFixJobResponse = bearerClient().startCodeFixJob(request)
+
+ override fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse = bearerClient().getCodeFixJob(request)
+
// DO NOT directly use this method to fetch customizations, use wrapper [CodeWhispererModelConfigurator.listCustomization()] instead
override fun listAvailableCustomizations(): List =
bearerClient().listAvailableCustomizationsPaginator(ListAvailableCustomizationsRequest.builder().build())
@@ -301,6 +367,20 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
}
}
+ override fun startTestGeneration(uploadId: String, targetCode: List, userInput: String): StartTestGenerationResponse =
+ bearerClient().startTestGeneration { builder ->
+ builder.uploadId(uploadId)
+ builder.targetCodeList(targetCode)
+ builder.userInput(userInput)
+ // TODO: client token
+ }
+
+ override fun getTestGeneration(jobId: String, jobGroupName: String): GetTestGenerationResponse =
+ bearerClient().getTestGeneration { builder ->
+ builder.testGenerationJobId(jobId)
+ builder.testGenerationJobGroupName(jobGroupName)
+ }
+
override fun sendUserTriggerDecisionTelemetry(
requestContext: RequestContext,
responseContext: ResponseContext,
@@ -459,6 +539,100 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
}
+
+ override fun sendCodeScanSucceededTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeScanJobId: String?,
+ scope: CodeWhispererConstants.CodeAnalysisScope,
+ findings: Int,
+ ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder ->
+ requestBuilder.telemetryEvent { telemetryEventBuilder ->
+ telemetryEventBuilder.codeScanSucceededEvent {
+ it.programmingLanguage { languageBuilder ->
+ languageBuilder.languageName(language.toCodeWhispererRuntimeLanguage().languageId)
+ }
+ it.codeScanJobId(if (codeScanJobId.isNullOrEmpty()) CodeWhispererClientAdaptor.INVALID_CODESCANJOBID else codeScanJobId)
+ it.timestamp(Instant.now())
+ it.codeAnalysisScope(scope.value)
+ it.numberOfFindings(findings)
+ it.timestamp(Instant.now())
+ }
+ }
+ requestBuilder.optOutPreference(getTelemetryOptOutPreference())
+ requestBuilder.userContext(codeWhispererUserContext())
+ }
+
+ override fun sendCodeScanFailedTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeScanJobId: String?,
+ scope: CodeWhispererConstants.CodeAnalysisScope,
+ ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder ->
+ requestBuilder.telemetryEvent { telemetryEventBuilder ->
+ telemetryEventBuilder.codeScanFailedEvent {
+ it.programmingLanguage { languageBuilder ->
+ languageBuilder.languageName(language.toCodeWhispererRuntimeLanguage().languageId)
+ }
+ it.codeScanJobId(if (codeScanJobId.isNullOrEmpty()) CodeWhispererClientAdaptor.INVALID_CODESCANJOBID else codeScanJobId)
+ it.timestamp(Instant.now())
+ it.codeAnalysisScope(scope.value)
+ }
+ }
+ requestBuilder.optOutPreference(getTelemetryOptOutPreference())
+ requestBuilder.userContext(codeWhispererUserContext())
+ }
+
+ override fun sendCodeFixGenerationTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeFixJobId: String?,
+ ruleId: String?,
+ detectorId: String?,
+ findingId: String?,
+ linesOfCodeGenerated: Int?,
+ charsOfCodeGenerated: Int?,
+ ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder ->
+ requestBuilder.telemetryEvent { telemetryEventBuilder ->
+ telemetryEventBuilder.codeFixGenerationEvent {
+ it.programmingLanguage { languageBuilder ->
+ languageBuilder.languageName(language.toCodeWhispererRuntimeLanguage().languageId)
+ }
+ it.jobId(if (codeFixJobId.isNullOrEmpty()) CodeWhispererClientAdaptor.INVALID_CODEFIXJOBID else codeFixJobId)
+ it.ruleId(ruleId)
+ it.detectorId(detectorId)
+ it.findingId(findingId)
+ it.linesOfCodeGenerated(linesOfCodeGenerated)
+ it.charsOfCodeGenerated(charsOfCodeGenerated)
+ }
+ }
+ requestBuilder.optOutPreference(getTelemetryOptOutPreference())
+ requestBuilder.userContext(codeWhispererUserContext())
+ }
+
+ override fun sendCodeFixAcceptanceTelemetry(
+ language: CodeWhispererProgrammingLanguage,
+ codeFixJobId: String?,
+ ruleId: String?,
+ detectorId: String?,
+ findingId: String?,
+ linesOfCodeGenerated: Int?,
+ charsOfCodeGenerated: Int?,
+ ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder ->
+ requestBuilder.telemetryEvent { telemetryEventBuilder ->
+ telemetryEventBuilder.codeFixAcceptanceEvent {
+ it.programmingLanguage { languageBuilder ->
+ languageBuilder.languageName(language.toCodeWhispererRuntimeLanguage().languageId)
+ }
+ it.jobId(if (codeFixJobId.isNullOrEmpty()) CodeWhispererClientAdaptor.INVALID_CODEFIXJOBID else codeFixJobId)
+ it.ruleId(ruleId)
+ it.detectorId(detectorId)
+ it.findingId(findingId)
+ it.linesOfCodeAccepted(linesOfCodeGenerated)
+ it.charsOfCodeAccepted(charsOfCodeGenerated)
+ }
+ }
+ requestBuilder.optOutPreference(getTelemetryOptOutPreference())
+ requestBuilder.userContext(codeWhispererUserContext())
+ }
+
override fun sendCodeScanRemediationTelemetry(
language: CodeWhispererProgrammingLanguage?,
codeScanRemediationEventType: String?,
@@ -490,6 +664,37 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
requestBuilder.userContext(codeWhispererUserContext())
}
+ override fun sendTestGenerationEvent(
+ jobId: String,
+ groupName: String,
+ language: CodeWhispererProgrammingLanguage?,
+ numberOfUnitTestCasesGenerated: Int?,
+ numberOfUnitTestCasesAccepted: Int?,
+ linesOfCodeGenerated: Int?,
+ linesOfCodeAccepted: Int?,
+ charsOfCodeGenerated: Int?,
+ charsOfCodeAccepted: Int?,
+ ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder ->
+ requestBuilder.telemetryEvent { telemetryEventBuilder ->
+ telemetryEventBuilder.testGenerationEvent {
+ it.programmingLanguage { languageBuilder ->
+ languageBuilder.languageName(language?.toCodeWhispererRuntimeLanguage()?.languageId)
+ }
+ it.jobId(jobId)
+ it.groupName(groupName)
+ it.numberOfUnitTestCasesGenerated(numberOfUnitTestCasesGenerated)
+ it.numberOfUnitTestCasesAccepted(numberOfUnitTestCasesAccepted)
+ it.linesOfCodeGenerated(linesOfCodeGenerated)
+ it.linesOfCodeAccepted(linesOfCodeAccepted)
+ it.charsOfCodeGenerated(charsOfCodeGenerated)
+ it.charsOfCodeAccepted(charsOfCodeAccepted)
+ it.timestamp(Instant.now())
+ }
+ }
+ requestBuilder.optOutPreference(getTelemetryOptOutPreference())
+ requestBuilder.userContext(codeWhispererUserContext())
+ }
+
override fun listFeatureEvaluations(): ListFeatureEvaluationsResponse = bearerClient().listFeatureEvaluations {
it.userContext(codeWhispererUserContext())
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt
index e03b749726..4f7833a1fc 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt
@@ -16,6 +16,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispe
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererLearnMoreAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererProvideFeedbackAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererShowSettingsAction
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.actions.CodeWhispererCodeScanRunAction
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.ActionProvider
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.Customize
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.actions.Learn
@@ -38,8 +39,7 @@ class QStatusBarLoggedInActionGroup : DefaultActionGroup() {
override val customize = Customize()
override val learn = Learn()
override val openChatPanel = ActionManager.getInstance().getAction("q.openchat")
- override val runScan = ActionManager.getInstance().getAction("codewhisperer.toolbar.security.scan")
- override val stopScan = ActionManager.getInstance().getAction("codewhisperer.toolbar.security.stopscan")
+ override val runScan = CodeWhispererCodeScanRunAction()
override val pauseAutoScans = PauseCodeScans()
override val resumeAutoScans = ResumeCodeScans()
override val sendFeedback = CodeWhispererProvideFeedbackAction()
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt
index 3b4979572c..cf342acf4c 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/actions/ActionFactory.kt
@@ -7,7 +7,6 @@ import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.credentials.sono.isSono
-import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isUserBuilderId
@@ -23,7 +22,6 @@ interface ActionProvider {
val pauseAutoScans: T
val resumeAutoScans: T
val runScan: T
- val stopScan: T
val sendFeedback: T
val connectOnGithub: T
val documentation: T
@@ -48,7 +46,6 @@ fun buildActionListForInlineSuggestions(project: Project, actionProvider: Ac
fun buildActionListForCodeScan(project: Project, actionProvider: ActionProvider): List =
buildList {
- val codeScanManager = CodeWhispererCodeScanManager.getInstance(project)
val manager = CodeWhispererExplorerActionManager.getInstance()
if (!isUserBuilderId(project)) {
if (manager.isAutoEnabledForCodeScan()) {
@@ -57,11 +54,7 @@ fun buildActionListForCodeScan(project: Project, actionProvider: ActionProvi
add(actionProvider.resumeAutoScans)
}
}
- if (codeScanManager.isProjectScanInProgress()) {
- add(actionProvider.stopScan)
- } else {
- add(actionProvider.runScan)
- }
+ add(actionProvider.runScan)
}
fun buildActionListForOtherFeatures(project: Project, actionProvider: ActionProvider): List =
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt
index 58167cb1ef..98e5486cd2 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/CodeWhispererProgrammingLanguage.kt
@@ -34,6 +34,12 @@ abstract class CodeWhispererProgrammingLanguage {
open fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = this
+ open fun lineCommentPrefix(): String? = "//"
+
+ open fun blockCommentPrefix(): String? = "/*"
+
+ open fun blockCommentSuffix(): String? = "*/"
+
final override fun equals(other: Any?): Boolean {
if (other !is CodeWhispererProgrammingLanguage) return false
return this.languageId == other.languageId
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt
index 5332d99145..aba531eb64 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererJson.kt
@@ -15,6 +15,12 @@ class CodeWhispererJson private constructor() : CodeWhispererProgrammingLanguage
override fun isAutoFileScanSupported(): Boolean = true
+ override fun lineCommentPrefix() = null
+
+ override fun blockCommentPrefix() = null
+
+ override fun blockCommentSuffix() = null
+
companion object {
const val ID = "json"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPlainText.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPlainText.kt
index 71f3d7b639..5fb15d737c 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPlainText.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPlainText.kt
@@ -13,6 +13,12 @@ class CodeWhispererPlainText private constructor() : CodeWhispererProgrammingLan
override fun isAutoFileScanSupported(): Boolean = false
+ override fun lineCommentPrefix() = null
+
+ override fun blockCommentPrefix() = null
+
+ override fun blockCommentSuffix() = null
+
companion object {
const val ID = "plaintext"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt
index 4a9964c1a0..c2b4ce25f4 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererPython.kt
@@ -24,6 +24,12 @@ class CodeWhispererPython private constructor() : CodeWhispererProgrammingLangua
override fun isSupplementalContextSupported() = true
+ override fun lineCommentPrefix() = "#"
+
+ override fun blockCommentPrefix() = "\"\"\""
+
+ override fun blockCommentSuffix() = "\"\"\""
+
companion object {
const val ID = "python"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt
index 15be78d83f..aef64cac8f 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererRuby.kt
@@ -15,6 +15,12 @@ class CodeWhispererRuby private constructor() : CodeWhispererProgrammingLanguage
override fun isAutoFileScanSupported(): Boolean = true
+ override fun lineCommentPrefix(): String = "#"
+
+ override fun blockCommentPrefix(): String = "=begin"
+
+ override fun blockCommentSuffix(): String = "=end"
+
companion object {
const val ID = "ruby"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt
index bd8fa3450f..a096ff1bb1 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererShell.kt
@@ -13,6 +13,12 @@ class CodeWhispererShell private constructor() : CodeWhispererProgrammingLanguag
override fun isCodeCompletionSupported(): Boolean = true
+ override fun lineCommentPrefix(): String = "#"
+
+ override fun blockCommentPrefix(): String = ": '"
+
+ override fun blockCommentSuffix(): String = "'"
+
companion object {
const val ID = "shell"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt
index 828c45cc25..0e5123b3cc 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererTf.kt
@@ -15,6 +15,8 @@ class CodeWhispererTf private constructor() : CodeWhispererProgrammingLanguage()
override fun isAutoFileScanSupported(): Boolean = true
+ override fun lineCommentPrefix(): String = "#"
+
companion object {
const val ID = "tf"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererUnknownLanguage.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererUnknownLanguage.kt
index dc9a2779ff..2723698f9a 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererUnknownLanguage.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererUnknownLanguage.kt
@@ -13,6 +13,12 @@ class CodeWhispererUnknownLanguage private constructor() : CodeWhispererProgramm
override fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = CodeWhispererPlainText.INSTANCE
+ override fun lineCommentPrefix() = null
+
+ override fun blockCommentPrefix() = null
+
+ override fun blockCommentSuffix() = null
+
companion object {
const val ID = "unknown"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt
index 986add8b8c..c110d797cb 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/language/languages/CodeWhispererYaml.kt
@@ -15,6 +15,12 @@ class CodeWhispererYaml private constructor() : CodeWhispererProgrammingLanguage
override fun isAutoFileScanSupported(): Boolean = true
+ override fun lineCommentPrefix(): String = "#"
+
+ override fun blockCommentPrefix(): String? = null
+
+ override fun blockCommentSuffix(): String? = null
+
companion object {
const val ID = "yaml"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt
index 91da779ab1..ac69c8b54d 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt
@@ -256,14 +256,14 @@ data class CodeScanTelemetryEvent(
val codeAnalysisScope: CodeWhispererConstants.CodeAnalysisScope,
)
-data class CodeScanServiceInvocationContext(
- val artifactsUploadDuration: Long,
- val serviceInvocationDuration: Long,
+data class CreateUploadUrlServiceInvocationContext(
+ val artifactsUploadDuration: Long = 0,
+ val serviceInvocationDuration: Long = 0,
)
data class CodeScanResponseContext(
val payloadContext: PayloadContext,
- val serviceInvocationContext: CodeScanServiceInvocationContext,
+ val serviceInvocationContext: CreateUploadUrlServiceInvocationContext,
val codeScanJobId: String? = null,
val codeScanTotalIssues: Int = 0,
val codeScanIssuesWithFixes: Int = 0,
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt
index 071f7505d3..5c642557a0 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt
@@ -11,10 +11,13 @@ import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.options.ex.Settings
import com.intellij.openapi.project.Project
import com.intellij.ui.components.ActionLink
+import com.intellij.ui.components.fields.ExpandableTextField
import com.intellij.ui.dsl.builder.bindIntText
import com.intellij.ui.dsl.builder.bindSelected
+import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.EdtExecutorService
+import com.intellij.util.execution.ParametersListUtil
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
@@ -179,6 +182,17 @@ class CodeWhispererConfigurable(private val project: Project) :
}
}
+ group(message("aws.settings.codewhisperer.code_review")) {
+ row {
+ ExpandableTextField(ParametersListUtil.COLON_LINE_PARSER, ParametersListUtil.COLON_LINE_JOINER).also {
+ cell(it)
+ .label(message("aws.settings.codewhisperer.code_review.title"))
+ .comment(message("aws.settings.codewhisperer.code_review.description"))
+ .bindText(codeWhispererSettings::getIgnoredCodeReviewIssues, codeWhispererSettings::setIgnoredCodeReviewIssues)
+ }
+ }
+ }
+
group(message("aws.settings.codewhisperer.group.data_sharing")) {
row {
checkBox(message("aws.settings.codewhisperer.configurable.opt_out.title")).apply {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererProjectStartupSettingsListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererProjectStartupSettingsListener.kt
index 24dbd5bd7f..78403cc952 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererProjectStartupSettingsListener.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/startup/CodeWhispererProjectStartupSettingsListener.kt
@@ -33,7 +33,7 @@ class CodeWhispererProjectStartupSettingsListener(private val project: Project)
CodeWhispererCodeReferenceManager.getInstance(project).toolWindow?.isAvailable = value
if (value) {
CodeWhispererSettings.getInstance().toggleIncludeCodeWithReference(true)
- CodeWhispererCodeScanManager.getInstance(project).addCodeScanUI()
+ CodeWhispererCodeScanManager.getInstance(project).buildCodeScanUI()
} else {
CodeWhispererCodeScanManager.getInstance(project).removeCodeScanUI()
}
@@ -43,7 +43,8 @@ class CodeWhispererProjectStartupSettingsListener(private val project: Project)
super.toolWindowShown(toolWindow)
if (toolWindow.id != ProblemsView.ID) return
if (!isCodeWhispererEnabled(project)) return
- CodeWhispererCodeScanManager.getInstance(project).addCodeScanUI()
+ CodeWhispererCodeScanManager.getInstance(project).buildCodeScanUI()
+ CodeWhispererCodeScanManager.getInstance(project).showCodeScanUI()
}
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
@@ -53,7 +54,7 @@ class CodeWhispererProjectStartupSettingsListener(private val project: Project)
CodeWhispererCodeReferenceManager.getInstance(project).toolWindow?.isAvailable = newConnection != null
}
if (newConnection != null) {
- CodeWhispererCodeScanManager.getInstance(project).addCodeScanUI()
+ CodeWhispererCodeScanManager.getInstance(project).buildCodeScanUI()
} else {
CodeWhispererCodeScanManager.getInstance(project).removeCodeScanUI()
}
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 77f62b65b9..4ff3c95cfb 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
@@ -38,6 +38,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer
import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled
import software.aws.toolkits.jetbrains.settings.AwsSettings
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+import software.aws.toolkits.telemetry.CodeFixAction
import software.aws.toolkits.telemetry.CodewhispererCodeScanScope
import software.aws.toolkits.telemetry.CodewhispererCompletionType
import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask
@@ -47,6 +48,7 @@ import software.aws.toolkits.telemetry.CodewhispererSuggestionState
import software.aws.toolkits.telemetry.CodewhispererTelemetry
import software.aws.toolkits.telemetry.CodewhispererTriggerType
import software.aws.toolkits.telemetry.Component
+import software.aws.toolkits.telemetry.CredentialSourceId
import software.aws.toolkits.telemetry.Result
import java.time.Duration
import java.time.Instant
@@ -342,7 +344,7 @@ class CodeWhispererTelemetryService {
)
}
- fun sendCodeScanIssueApplyFixEvent(issue: CodeWhispererCodeScanIssue, result: Result, reason: String? = null) {
+ fun sendCodeScanIssueApplyFixEvent(issue: CodeWhispererCodeScanIssue, result: Result, reason: String? = null, codeFixAction: CodeFixAction?) {
CodewhispererTelemetry.codeScanIssueApplyFix(
findingId = issue.findingId,
detectorId = issue.detectorId,
@@ -350,7 +352,44 @@ class CodeWhispererTelemetryService {
component = Component.Hover,
result = result,
reason = reason,
- credentialStartUrl = getCodeWhispererStartUrl(issue.project)
+ credentialStartUrl = getCodeWhispererStartUrl(issue.project),
+ codeFixAction = codeFixAction
+ )
+ }
+
+ fun sendCodeScanNewTabEvent(credentialSourceId: CredentialSourceId?) {
+ CodewhispererTelemetry.codeScanChatNewTab(
+ credentialSourceId = credentialSourceId
+ )
+ }
+
+ fun sendCodeScanIssueIgnore(
+ component: Component,
+ issue: CodeWhispererCodeScanIssue,
+ isIgnoreAll: Boolean,
+ ) {
+ CodewhispererTelemetry.codeScanIssueIgnore(
+ component = component,
+ credentialStartUrl = getCodeWhispererStartUrl(issue.project),
+ findingId = issue.findingId,
+ detectorId = issue.detectorId,
+ ruleId = issue.ruleId,
+ variant = if (isIgnoreAll) "all" else null
+ )
+ }
+
+ fun sendCodeScanIssueGenerateFix(
+ component: Component,
+ issue: CodeWhispererCodeScanIssue,
+ isRefresh: Boolean,
+ ) {
+ CodewhispererTelemetry.codeScanIssueGenerateFix(
+ component = component,
+ credentialStartUrl = getCodeWhispererStartUrl(issue.project),
+ findingId = issue.findingId,
+ detectorId = issue.detectorId,
+ ruleId = issue.ruleId,
+ variant = if (isRefresh) "refresh" else 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 eba9ce1641..1472d88f9e 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
@@ -3,12 +3,14 @@
package software.aws.toolkits.jetbrains.services.codewhisperer.util
+import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.JBColor
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException
import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava
import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask
import java.awt.Font
@@ -53,6 +55,10 @@ object CodeWhispererConstants {
// avoid ThrottlingException as much as possible.
const val INVOCATION_INTERVAL: Long = 2050
+ val runScanKey = DataKey.create("amazonq.codescan.run")
+ val scanResultsKey = DataKey.create("amazonq.codescan.result")
+ val scanScopeKey = DataKey.create("amazonq.codescan.scope")
+
const val Q_CUSTOM_LEARN_MORE_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/customizations.html"
const val Q_SUPPORTED_LANG_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html"
const val CODEWHISPERER_CODE_SCAN_LEARN_MORE_URI = "https://docs.aws.amazon.com/codewhisperer/latest/userguide/security-scans.html"
@@ -70,9 +76,12 @@ object CodeWhispererConstants {
const val FILE_SCAN_INITIAL_POLLING_INTERVAL_IN_SECONDS: Long = 10
const val PROJECT_SCAN_INITIAL_POLLING_INTERVAL_IN_SECONDS: Long = 30
const val CODE_SCAN_CREATE_PAYLOAD_TIMEOUT_IN_SECONDS: Long = 10
- const val FILE_SCAN_TIMEOUT_IN_SECONDS: Long = 60 // 60 seconds
+ const val FILE_SCAN_TIMEOUT_IN_SECONDS: Long = 60 * 10 // 10 minutes
const val FILE_SCAN_PAYLOAD_SIZE_LIMIT_IN_BYTES: Long = 1024 * 200 // 200KB
const val AUTO_SCAN_DEBOUNCE_DELAY_IN_SECONDS: Long = 30
+ const val CODE_FIX_CREATE_PAYLOAD_TIMEOUT_IN_SECONDS: Long = 10
+ const val CODE_FIX_POLLING_INTERVAL_IN_SECONDS: Long = 1
+ const val CODE_FIX_TIMEOUT_IN_SECONDS: Long = 60 // 60 seconds
const val TOTAL_BYTES_IN_KB = 1024
const val TOTAL_BYTES_IN_MB = 1024 * 1024
const val TOTAL_MILLIS_IN_SECOND = 1000
@@ -93,6 +102,7 @@ object CodeWhispererConstants {
const val PROJECT_SCANS_LIMIT_REACHED = "You have reached the monthly quota of project scans."
const val FILE_SCANS_THROTTLING_MESSAGE = "Maximum auto-scans count reached for this month"
const val PROJECT_SCANS_THROTTLING_MESSAGE = "Maximum project scan count reached for this month"
+ const val amazonqIgnoreNextLine = "amazonq-ignore-next-line"
// Date when Accountless is not supported
val EXPIRE_DATE = SimpleDateFormat("yyyy-MM-dd").parse("2023-01-31")
@@ -117,6 +127,19 @@ object CodeWhispererConstants {
PROJECT("PROJECT"),
}
+ enum class UploadTaskType(val value: String) {
+ SCAN_FILE("SCAN_FILE"),
+ SCAN_PROJECT("SCAN_PROJECT"),
+ UTG("UTG"),
+ CODE_FIX("CODE_FIX"),
+ }
+
+ enum class FixGenerationState(val value: String) {
+ GENERATING("GENERATING"),
+ COMPLETED("COMPLETED"),
+ FAILED("FAILED"),
+ }
+
object Config {
const val CODEWHISPERER_ENDPOINT = "https://codewhisperer.us-east-1.amazonaws.com/" // PROD
const val CODEWHISPERER_IDPOOL_ID = "us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9"
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt
index a9c2a212c6..d90dc02b03 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt
@@ -34,6 +34,10 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererCon
import software.aws.toolkits.jetbrains.core.credentials.reauthConnectionIfNeeded
import software.aws.toolkits.jetbrains.core.credentials.sono.isSono
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnection
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnectionType
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet
+import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.learn.LearnCodeWhispererManager.Companion.taskTypeToFilename
import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk
@@ -50,6 +54,7 @@ import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodewhispererCompletionType
import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask
+import software.aws.toolkits.telemetry.CredentialSourceId
// Controls the condition to send telemetry event to CodeWhisperer service, currently:
// 1. It will be sent for Builder ID users, only if they have optin telemetry sharing.
@@ -104,6 +109,17 @@ suspend fun String.toCodeChunk(path: String): List {
}
}
+fun getAuthType(project: Project): CredentialSourceId? {
+ val connection = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q)
+ var authType: CredentialSourceId? = null
+ if (connection.connectionType == ActiveConnectionType.IAM_IDC && connection is ActiveConnection.ValidBearer) {
+ authType = CredentialSourceId.IamIdentityCenter
+ } else if (connection.connectionType == ActiveConnectionType.BUILDER_ID && connection is ActiveConnection.ValidBearer) {
+ authType = CredentialSourceId.AwsId
+ }
+ return authType
+}
+
// we refer 10 lines of code as "Code Chunk"
// [[L1, L2, ...L10], [L11, L12, ...L20]...]
// use VirtualFile.toCodeChunk
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt
new file mode 100644
index 0000000000..ce31a86f71
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt
@@ -0,0 +1,156 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer.util
+
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.util.io.HttpRequests
+import org.apache.commons.codec.digest.DigestUtils
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisUploadContext
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeFixUploadContext
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext
+import software.amazon.awssdk.services.codewhispererruntime.model.UploadIntent
+import software.amazon.awssdk.utils.IoUtils
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.jetbrains.core.AwsClientManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.APPLICATION_ZIP
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.AWS_KMS
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.CONTENT_MD5
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.CONTENT_TYPE
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.SERVER_SIDE_ENCRYPTION
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.SERVER_SIDE_ENCRYPTION_CONTEXT
+import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.invalidSourceZipError
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
+import software.aws.toolkits.resources.message
+import java.io.File
+import java.io.FileInputStream
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.util.Base64
+
+@Service
+class CodeWhispererZipUploadManager(private val project: Project) {
+
+ fun createUploadUrlAndUpload(
+ zipFile: File,
+ artifactType: String,
+ taskType: CodeWhispererConstants.UploadTaskType,
+ taskName: String,
+ ): CreateUploadUrlResponse {
+ // Throw error if zipFile is invalid.
+ if (!zipFile.exists()) {
+ invalidSourceZipError()
+ }
+ val fileMd5: String = Base64.getEncoder().encodeToString(DigestUtils.md5(FileInputStream(zipFile)))
+ val createUploadUrlResponse = createUploadUrl(fileMd5, artifactType, taskType, taskName)
+ val url = createUploadUrlResponse.uploadUrl()
+
+ LOG.debug { "Uploading $artifactType using the presigned URL." }
+
+ uploadArtifactToS3(
+ url,
+ createUploadUrlResponse.uploadId(),
+ zipFile,
+ fileMd5,
+ createUploadUrlResponse.kmsKeyArn(),
+ createUploadUrlResponse.requestHeaders()
+ )
+ return createUploadUrlResponse
+ }
+
+ @Throws(IOException::class)
+ fun uploadArtifactToS3(
+ url: String,
+ uploadId: String,
+ fileToUpload: File,
+ md5: String,
+ kmsArn: String?,
+ requestHeaders: Map?,
+ ) {
+ try {
+ val uploadIdJson = """{"uploadId":"$uploadId"}"""
+ HttpRequests.put(url, "application/zip").userAgent(AwsClientManager.getUserAgent()).tuner {
+ if (requestHeaders.isNullOrEmpty()) {
+ it.setRequestProperty(CONTENT_MD5, md5)
+ it.setRequestProperty(CONTENT_TYPE, APPLICATION_ZIP)
+ it.setRequestProperty(SERVER_SIDE_ENCRYPTION, AWS_KMS)
+ if (kmsArn?.isNotEmpty() == true) {
+ it.setRequestProperty(SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, kmsArn)
+ }
+ it.setRequestProperty(SERVER_SIDE_ENCRYPTION_CONTEXT, Base64.getEncoder().encodeToString(uploadIdJson.toByteArray()))
+ } else {
+ requestHeaders.forEach { entry ->
+ it.setRequestProperty(entry.key, entry.value)
+ }
+ }
+ }.connect {
+ val connection = it.connection as HttpURLConnection
+ connection.setFixedLengthStreamingMode(fileToUpload.length())
+ IoUtils.copy(fileToUpload.inputStream(), connection.outputStream)
+ }
+ } catch (e: Exception) {
+ LOG.debug { "Artifact failed to upload in the S3 bucket: ${e.message}" }
+ val errorMessage = getTelemetryErrorMessage(e)
+ throw RuntimeException(errorMessage)
+ }
+ }
+
+ fun createUploadUrl(
+ md5Content: String,
+ artifactType: String,
+ uploadTaskType: CodeWhispererConstants.UploadTaskType,
+ taskName: String,
+ ): CreateUploadUrlResponse = try {
+ CodeWhispererClientAdaptor.getInstance(project).createUploadUrl(
+ CreateUploadUrlRequest.builder()
+ .contentMd5(md5Content)
+ .artifactType(artifactType)
+ .uploadIntent(getUploadIntent(uploadTaskType))
+ .uploadContext(
+ if (uploadTaskType == CodeWhispererConstants.UploadTaskType.CODE_FIX) {
+ UploadContext.fromCodeFixUploadContext(CodeFixUploadContext.builder().codeFixName(taskName).build())
+ } else {
+ UploadContext.fromCodeAnalysisUploadContext(CodeAnalysisUploadContext.builder().codeScanName(taskName).build())
+ }
+ )
+ .build()
+ )
+ } catch (e: Exception) {
+ LOG.debug { "Create Upload URL failed: ${e.message}" }
+ val errorMessage = getTelemetryErrorMessage(e)
+ throw RuntimeException(errorMessage)
+ }
+
+ private fun getUploadIntent(uploadTaskType: CodeWhispererConstants.UploadTaskType): UploadIntent = when (uploadTaskType) {
+ CodeWhispererConstants.UploadTaskType.SCAN_FILE -> UploadIntent.AUTOMATIC_FILE_SECURITY_SCAN
+ CodeWhispererConstants.UploadTaskType.SCAN_PROJECT -> UploadIntent.FULL_PROJECT_SECURITY_SCAN
+ CodeWhispererConstants.UploadTaskType.UTG -> UploadIntent.UNIT_TESTS_GENERATION
+ CodeWhispererConstants.UploadTaskType.CODE_FIX -> UploadIntent.CODE_FIX_GENERATION
+ }
+
+ companion object {
+ fun getInstance(project: Project) = project.service()
+ private val LOG = getLogger()
+ }
+}
+
+fun getTelemetryErrorMessage(e: Exception): String = when {
+ e.message?.contains("Resource not found.") == true -> "Resource not found."
+ e.message?.contains("Maximum com.amazon.aws.codewhisperer.StartCodeAnalysis reached for this month.") == true -> message(
+ "testgen.error.maximum_generations_reach"
+ )
+ e.message?.contains("Service returned HTTP status code 407") == true -> "Service returned HTTP status code 407"
+ e.message?.contains("Improperly formed request") == true -> "Improperly formed request"
+ e.message?.contains("Service returned HTTP status code 403") == true -> "Service returned HTTP status code 403"
+ e.message?.contains("invalid_grant: Invalid token provided") == true -> "invalid_grant: Invalid token provided"
+ e.message?.contains("Connect timed out") == true -> "Unable to execute HTTP request: Connect timed out" // Error: Connect to host failed
+ e.message?.contains("Encountered an unexpected error when processing the request, please try again.") == true ->
+ "Encountered an unexpected error when processing the request, please try again."
+ else -> e.message ?: message("codewhisperer.codescan.run_scan_error_telemetry")
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt
index 25a5cbb18f..5224284b42 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt
@@ -23,7 +23,8 @@ class CodeWhispererConfigurableTest : CodeWhispererTestBase() {
@Test
fun `test CodeWhisperer configurable`() {
val codeScanManagerSpy = Mockito.spy(CodeWhispererCodeScanManager.getInstance(projectRule.project))
- doNothing().`when`(codeScanManagerSpy).addCodeScanUI()
+ doNothing().`when`(codeScanManagerSpy).buildCodeScanUI()
+ doNothing().`when`(codeScanManagerSpy).showCodeScanUI()
doNothing().`when`(codeScanManagerSpy).removeCodeScanUI()
projectRule.project.replaceService(CodeWhispererCodeScanManager::class.java, codeScanManagerSpy, disposableRule.disposable)
val configurable = CodeWhispererConfigurable(projectRule.project)
@@ -48,7 +49,7 @@ class CodeWhispererConfigurableTest : CodeWhispererTestBase() {
)
val comments = panel.components.filterIsInstance()
- assertThat(comments.size).isEqualTo(7)
+ assertThat(comments.size).isEqualTo(8)
mockCodeWhispererEnabledStatus(false)
ApplicationManager.getApplication().messageBus.syncPublisher(ToolkitConnectionManagerListener.TOPIC)
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeFileScanTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeFileScanTest.kt
index aec774efbc..20c4c2d2d4 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeFileScanTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeFileScanTest.kt
@@ -16,10 +16,8 @@ import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mockito.mock
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
-import org.mockito.kotlin.doNothing
import org.mockito.kotlin.eq
import org.mockito.kotlin.inOrder
-import org.mockito.kotlin.isNull
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
@@ -103,7 +101,8 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
CodeScanSessionConfig.create(
pyPsiFile.virtualFile,
project,
- CodeWhispererConstants.CodeAnalysisScope.FILE
+ CodeWhispererConstants.CodeAnalysisScope.FILE,
+ false
)
)
setupResponse(pyPsiFile.virtualFile.toNioPath().relativeTo(pySession.projectRoot.toNioPath()))
@@ -115,7 +114,6 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
// Mock CodeWhispererClient needs to be setup before initializing CodeWhispererCodeScanSession
val pySessionContext = CodeScanSessionContext(project, pySession, CodeWhispererConstants.CodeAnalysisScope.FILE)
codeScanSessionSpy = spy(CodeWhispererCodeScanSession(pySessionContext))
- doNothing().whenever(codeScanSessionSpy).uploadArtifactToS3(any(), any(), any(), any(), isNull(), any())
mockClient.stub {
// setupResponse dynamically modifies these fake responses so this is very hard to follow and makes me question if we even need this
@@ -136,13 +134,13 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
CodeScanSessionConfig.create(
psiFile.virtualFile,
project,
- CodeWhispererConstants.CodeAnalysisScope.FILE
+ CodeWhispererConstants.CodeAnalysisScope.FILE,
+ false
)
)
setupResponse(psiFile.virtualFile.toNioPath().relativeTo(sessionConfig.projectRoot.toNioPath()))
val sessionContext = CodeScanSessionContext(project, sessionConfig, CodeWhispererConstants.CodeAnalysisScope.FILE)
val session = spy(CodeWhispererCodeScanSession(sessionContext))
- doNothing().whenever(session).uploadArtifactToS3(any(), any(), any(), any(), isNull(), any())
// Set up CPU and Memory monitoring
val runtime = Runtime.getRuntime()
@@ -186,13 +184,13 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
CodeScanSessionConfig.create(
psiFile.virtualFile,
project,
- CodeWhispererConstants.CodeAnalysisScope.FILE
+ CodeWhispererConstants.CodeAnalysisScope.FILE,
+ false
)
)
setupResponse(psiFile.virtualFile.toNioPath().relativeTo(sessionConfig.projectRoot.toNioPath()))
val sessionContext = CodeScanSessionContext(project, sessionConfig, CodeWhispererConstants.CodeAnalysisScope.FILE)
val session = spy(CodeWhispererCodeScanSession(sessionContext))
- doNothing().whenever(session).uploadArtifactToS3(any(), any(), any(), any(), isNull(), any())
// Set up CPU and Memory monitoring
val runtime = Runtime.getRuntime()
@@ -230,16 +228,26 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
fun `test createUploadUrlAndUpload()`() {
val file = pyPsiFile.virtualFile.toNioPath().toFile()
val fileMd5: String = Base64.getEncoder().encodeToString(DigestUtils.md5(FileInputStream(file)))
- codeScanSessionSpy.stub {
- onGeneric { codeScanSessionSpy.createUploadUrl(any(), any(), any()) }
+ zipUploadManagerSpy.stub {
+ onGeneric { zipUploadManagerSpy.createUploadUrl(any(), any(), any(), any()) }
.thenReturn(fakeCreateUploadUrlResponse)
}
- codeScanSessionSpy.createUploadUrlAndUpload(file, "artifactType", codeScanName)
+ zipUploadManagerSpy.createUploadUrlAndUpload(
+ file,
+ "artifactType",
+ CodeWhispererConstants.UploadTaskType.SCAN_FILE,
+ codeScanName
+ )
- val inOrder = inOrder(codeScanSessionSpy)
- inOrder.verify(codeScanSessionSpy).createUploadUrl(eq(fileMd5), eq("artifactType"), any())
- inOrder.verify(codeScanSessionSpy).uploadArtifactToS3(
+ val inOrder = inOrder(zipUploadManagerSpy)
+ inOrder.verify(zipUploadManagerSpy).createUploadUrl(
+ eq(fileMd5),
+ eq("artifactType"),
+ eq(CodeWhispererConstants.UploadTaskType.SCAN_FILE),
+ any()
+ )
+ inOrder.verify(zipUploadManagerSpy).uploadArtifactToS3(
eq(fakeCreateUploadUrlResponse.uploadUrl()),
eq(fakeCreateUploadUrlResponse.uploadId()),
eq(file),
@@ -251,7 +259,12 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
@Test
fun `test createUploadUrl()`() {
- val response = codeScanSessionSpy.createUploadUrl("md5", "type", codeScanName)
+ val response = zipUploadManagerSpy.createUploadUrl(
+ "md5",
+ "type",
+ CodeWhispererConstants.UploadTaskType.SCAN_FILE,
+ codeScanName
+ )
argumentCaptor().apply {
verify(mockClient).createUploadUrl(capture())
@@ -268,7 +281,7 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
fakeListCodeScanFindingsResponse.codeScanFindings(),
getFakeRecommendationsOnNonExistentFile()
)
- val res = codeScanSessionSpy.mapToCodeScanIssues(recommendations)
+ val res = codeScanSessionSpy.mapToCodeScanIssues(recommendations, project)
assertThat(res).hasSize(2)
}
@@ -284,8 +297,8 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
assertThat(it.responseContext.codeScanJobId).isEqualTo("jobId")
}
+ verify(zipUploadManagerSpy, times(1)).createUploadUrlAndUpload(eq(file), eq("SourceCode"), any(), anyString())
val inOrder = inOrder(codeScanSessionSpy)
- inOrder.verify(codeScanSessionSpy, times(1)).createUploadUrlAndUpload(eq(file), eq("SourceCode"), anyString())
inOrder.verify(codeScanSessionSpy, times(1)).createCodeScan(eq(CodewhispererLanguage.Python.toString()), anyString())
inOrder.verify(codeScanSessionSpy, times(1)).getCodeScan(any())
inOrder.verify(codeScanSessionSpy, times(1)).listCodeScanFindings(eq("jobId"), eq(null))
@@ -311,7 +324,8 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
CodeScanSessionConfig.create(
externalFile.virtualFile,
project,
- CodeWhispererConstants.CodeAnalysisScope.FILE
+ CodeWhispererConstants.CodeAnalysisScope.FILE,
+ false
)
)
@@ -337,8 +351,7 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
scanManagerSpy.runCodeScan(CodeWhispererConstants.CodeAnalysisScope.FILE)
// verify that function was run but none of the results/error handling methods were called.
- verify(scanManagerSpy, times(0)).updateFileIssues(any(), any())
- verify(scanManagerSpy, times(0)).handleError(any(), any(), any())
+ verify(scanManagerSpy, times(0)).handleError(any(), any())
verify(scanManagerSpy, times(0)).handleException(any(), any(), any())
}
@@ -347,7 +360,7 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
assertNotNull(pySession)
mockClient.stub {
- onGeneric { codeScanSessionSpy.createUploadUrlAndUpload(any(), any(), any()) }.thenThrow(
+ onGeneric { zipUploadManagerSpy.createUploadUrlAndUpload(any(), any(), any(), any()) }.thenThrow(
CodeWhispererException.builder()
.message("File Scan Monthly Exceeded")
.requestId("abc123")
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTest.kt
index 6b0112e8c9..976e7dcf21 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTest.kt
@@ -14,12 +14,11 @@ import org.mockito.ArgumentMatchers.anyString
import org.mockito.internal.verification.Times
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
-import org.mockito.kotlin.doNothing
import org.mockito.kotlin.eq
import org.mockito.kotlin.inOrder
-import org.mockito.kotlin.isNull
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
+import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.amazon.awssdk.awscore.exception.AwsErrorDetails
import software.amazon.awssdk.services.codewhisperer.model.CodeWhispererException
@@ -30,6 +29,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionco
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_MILLIS_IN_SECOND
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.getTelemetryErrorMessage
import software.aws.toolkits.jetbrains.utils.isInstanceOf
import software.aws.toolkits.jetbrains.utils.isInstanceOfSatisfying
import software.aws.toolkits.jetbrains.utils.rules.PythonCodeInsightTestFixtureRule
@@ -69,7 +69,8 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
CodeScanSessionConfig.create(
psifile.virtualFile,
project,
- CodeWhispererConstants.CodeAnalysisScope.PROJECT
+ CodeWhispererConstants.CodeAnalysisScope.PROJECT,
+ false
)
)
setupResponse(psifile.virtualFile.toNioPath().relativeTo(sessionConfigSpy.projectRoot.toNioPath()))
@@ -81,7 +82,6 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
// Mock CodeWhispererClient needs to be setup before initializing CodeWhispererCodeScanSession
codeScanSessionContext = CodeScanSessionContext(project, sessionConfigSpy, CodeWhispererConstants.CodeAnalysisScope.PROJECT)
codeScanSessionSpy = spy(CodeWhispererCodeScanSession(codeScanSessionContext))
- doNothing().`when`(codeScanSessionSpy).uploadArtifactToS3(any(), any(), any(), any(), isNull(), any())
mockClient.stub {
onGeneric { createUploadUrl(any()) }.thenReturn(fakeCreateUploadUrlResponse)
@@ -94,16 +94,21 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
@Test
fun `test createUploadUrlAndUpload()`() {
val fileMd5: String = Base64.getEncoder().encodeToString(DigestUtils.md5(FileInputStream(file)))
- codeScanSessionSpy.stub {
- onGeneric { codeScanSessionSpy.createUploadUrl(any(), any(), any()) }
+ zipUploadManagerSpy.stub {
+ onGeneric { zipUploadManagerSpy.createUploadUrl(any(), any(), any(), any()) }
.thenReturn(fakeCreateUploadUrlResponse)
}
- codeScanSessionSpy.createUploadUrlAndUpload(file, "artifactType", codeScanName)
+ zipUploadManagerSpy.createUploadUrlAndUpload(
+ file,
+ "artifactType",
+ CodeWhispererConstants.UploadTaskType.SCAN_FILE,
+ codeScanName
+ )
- val inOrder = inOrder(codeScanSessionSpy)
- inOrder.verify(codeScanSessionSpy).createUploadUrl(eq(fileMd5), eq("artifactType"), any())
- inOrder.verify(codeScanSessionSpy).uploadArtifactToS3(
+ val inOrder = inOrder(zipUploadManagerSpy)
+ inOrder.verify(zipUploadManagerSpy).createUploadUrl(eq(fileMd5), eq("artifactType"), any(), any())
+ inOrder.verify(zipUploadManagerSpy).uploadArtifactToS3(
eq(fakeCreateUploadUrlResponse.uploadUrl()),
eq(fakeCreateUploadUrlResponse.uploadId()),
eq(file),
@@ -118,13 +123,23 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
val invalidZipFile = File("/path/file.zip")
assertThrows {
- codeScanSessionSpy.createUploadUrlAndUpload(invalidZipFile, "artifactType", codeScanName)
+ zipUploadManagerSpy.createUploadUrlAndUpload(
+ invalidZipFile,
+ "artifactType",
+ CodeWhispererConstants.UploadTaskType.SCAN_FILE,
+ codeScanName
+ )
}
}
@Test
fun `test createUploadUrl()`() {
- val response = codeScanSessionSpy.createUploadUrl("md5", "type", codeScanName)
+ val response = zipUploadManagerSpy.createUploadUrl(
+ "md5",
+ "type",
+ CodeWhispererConstants.UploadTaskType.SCAN_FILE,
+ codeScanName
+ )
argumentCaptor().apply {
verify(mockClient).createUploadUrl(capture())
@@ -141,7 +156,7 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
fakeListCodeScanFindingsResponse.codeScanFindings(),
getFakeRecommendationsOnNonExistentFile()
)
- val res = codeScanSessionSpy.mapToCodeScanIssues(recommendations)
+ val res = codeScanSessionSpy.mapToCodeScanIssues(recommendations, project)
assertThat(res).hasSize(2)
}
@@ -150,7 +165,7 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
val recommendations = listOf(
fakeListCodeScanFindingsOutOfBoundsIndexResponse.codeScanFindings(),
)
- val res = codeScanSessionSpy.mapToCodeScanIssues(recommendations)
+ val res = codeScanSessionSpy.mapToCodeScanIssues(recommendations, project)
assertThat(res).hasSize(1)
}
@@ -179,7 +194,7 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
)
exceptions.forEachIndexed { index, exception ->
- val actualMessage = codeScanSessionSpy.getTelemetryErrorMessage(exception)
+ val actualMessage = getTelemetryErrorMessage(exception)
assertThat(expectedMessages[index]).isEqualTo(actualMessage)
}
}
@@ -194,8 +209,8 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
assertThat(it.responseContext.codeScanJobId).isEqualTo("jobId")
}
+ verify(zipUploadManagerSpy, times(1)).createUploadUrlAndUpload(eq(file), eq("SourceCode"), any(), anyString())
val inOrder = inOrder(codeScanSessionSpy)
- inOrder.verify(codeScanSessionSpy, Times(1)).createUploadUrlAndUpload(eq(file), eq("SourceCode"), anyString())
inOrder.verify(codeScanSessionSpy, Times(1)).createCodeScan(eq(CodewhispererLanguage.Python.toString()), anyString())
inOrder.verify(codeScanSessionSpy, Times(1)).getCodeScan(any())
inOrder.verify(codeScanSessionSpy, Times(1)).listCodeScanFindings(eq("jobId"), eq(null))
@@ -206,9 +221,9 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
assertNotNull(sessionConfigSpy)
mockClient.stub {
- onGeneric { codeScanSessionSpy.createUploadUrlAndUpload(any(), any(), any()) }.thenThrow(
+ onGeneric { zipUploadManagerSpy.createUploadUrlAndUpload(any(), any(), any(), any()) }.thenThrow(
CodeWhispererException.builder()
- .message("Project Scan Monthly Exceeded")
+ .message("Project Review Monthly Exceeded")
.requestId("abc123")
.statusCode(400)
.cause(RuntimeException("Something went wrong"))
@@ -228,7 +243,7 @@ class CodeWhispererCodeScanTest : CodeWhispererCodeScanTestBase(PythonCodeInsigh
assertThat(codeScanResponse).isInstanceOf()
if (codeScanResponse is CodeScanResponse.Failure) {
assertThat(codeScanResponse.failureReason).isInstanceOf()
- assertThat(codeScanResponse.failureReason.toString()).contains("Project Scan Monthly Exceeded")
+ assertThat(codeScanResponse.failureReason.toString()).contains("Project Review Monthly Exceeded")
assertThat(codeScanResponse.failureReason.cause.toString()).contains("java.lang.RuntimeException: Something went wrong")
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTestBase.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTestBase.kt
index ac85aecdd4..ce295abadf 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTestBase.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanTestBase.kt
@@ -8,6 +8,7 @@ import com.github.tomakehurst.wiremock.junit.WireMockRule
import com.intellij.analysis.problemsView.toolWindow.ProblemsView
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.RegisterToolWindowTask
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.testFramework.ApplicationRule
@@ -33,7 +34,12 @@ import software.amazon.awssdk.services.codewhisperer.model.CodeScanStatus
import software.amazon.awssdk.services.codewhisperer.model.CreateCodeScanResponse
import software.amazon.awssdk.services.codewhisperer.model.GetCodeScanResponse
import software.amazon.awssdk.services.codewhisperer.model.ListCodeScanFindingsResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.CodeFixJobStatus
import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobResponse
+import software.amazon.awssdk.services.codewhispererruntime.model.Reference
+import software.amazon.awssdk.services.codewhispererruntime.model.Span
+import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeFixJobResponse
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.CodeScanSessionConfig
@@ -41,6 +47,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWh
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererZipUploadManager
import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule
import software.aws.toolkits.telemetry.CodewhispererLanguage
import java.nio.file.Path
@@ -81,12 +88,15 @@ open class CodeWhispererCodeScanTestBase(projectRule: CodeInsightTestFixtureRule
internal lateinit var fakeGetCodeScanResponse: GetCodeScanResponse
internal lateinit var fakeGetCodeScanResponsePending: GetCodeScanResponse
internal lateinit var fakeGetCodeScanResponseFailed: GetCodeScanResponse
+ internal lateinit var fakeGetCodeFixJobResponse: GetCodeFixJobResponse
+ internal lateinit var fakeStartCodeFixJobResponse: StartCodeFixJobResponse
internal val metadata: DefaultAwsResponseMetadata = DefaultAwsResponseMetadata.create(
mapOf(AwsHeader.AWS_REQUEST_ID to CodeWhispererTestUtil.testRequestId)
)
internal lateinit var scanManagerSpy: CodeWhispererCodeScanManager
+ internal lateinit var zipUploadManagerSpy: CodeWhispererZipUploadManager
internal lateinit var project: Project
@Before
@@ -95,7 +105,12 @@ open class CodeWhispererCodeScanTestBase(projectRule: CodeInsightTestFixtureRule
s3endpoint = "http://127.0.0.1:${wireMock.port()}"
scanManagerSpy = spy(CodeWhispererCodeScanManager.getInstance(project))
- doNothing().whenever(scanManagerSpy).addCodeScanUI(any())
+ doNothing().whenever(scanManagerSpy).buildCodeScanUI()
+ doNothing().whenever(scanManagerSpy).showCodeScanUI()
+
+ zipUploadManagerSpy = spy(CodeWhispererZipUploadManager.getInstance(project))
+ doNothing().whenever(zipUploadManagerSpy).uploadArtifactToS3(any(), any(), any(), any(), isNull(), any())
+ projectRule.project.replaceService(CodeWhispererZipUploadManager::class.java, zipUploadManagerSpy, disposableRule.disposable)
mockClient = mock().also {
project.replaceService(CodeWhispererClientAdaptor::class.java, it, disposableRule.disposable)
@@ -142,7 +157,7 @@ open class CodeWhispererCodeScanTestBase(projectRule: CodeInsightTestFixtureRule
"codeSnippet": [
$codeSnippetJson
],
- "severity": "severity",
+ "severity": "Low",
"remediation": {
"recommendation": {
"text": "recommendationText",
@@ -212,6 +227,36 @@ open class CodeWhispererCodeScanTestBase(projectRule: CodeInsightTestFixtureRule
.responseMetadata(metadata)
.build() as CreateUploadUrlResponse
+ fakeGetCodeFixJobResponse = GetCodeFixJobResponse.builder()
+ .jobStatus(CodeFixJobStatus.SUCCEEDED)
+ .suggestedFix(
+ software.amazon.awssdk.services.codewhispererruntime.model.SuggestedFix.builder()
+ .codeDiff("diff")
+ .description("description")
+ .references(
+ Reference.builder()
+ .url(s3endpoint)
+ .licenseName("license")
+ .repository("repo")
+ .recommendationContentSpan(
+ Span.builder()
+ .start(6)
+ .end(8)
+ .build()
+ )
+ .build()
+ )
+ .build()
+ )
+ .responseMetadata(metadata)
+ .build() as GetCodeFixJobResponse
+
+ fakeStartCodeFixJobResponse = StartCodeFixJobResponse.builder()
+ .jobId(JOB_ID)
+ .status(CodeFixJobStatus.IN_PROGRESS)
+ .responseMetadata(metadata)
+ .build() as StartCodeFixJobResponse
+
fakeCreateCodeScanResponse = CreateCodeScanResponse.builder()
.status(CodeScanStatus.COMPLETED)
.jobId(JOB_ID)
@@ -286,7 +331,7 @@ open class CodeWhispererCodeScanTestBase(projectRule: CodeInsightTestFixtureRule
"content": "codeBlock2"
}
],
- "severity": "severity",
+ "severity": "Low",
"remediation": {
"recommendation": {
"text": "recommendationText",
@@ -336,7 +381,6 @@ open class CodeWhispererCodeScanTestBase(projectRule: CodeInsightTestFixtureRule
) {
val codeScanContext = CodeScanSessionContext(project, sessionConfigSpy, CodeWhispererConstants.CodeAnalysisScope.PROJECT)
val sessionMock = spy(CodeWhispererCodeScanSession(codeScanContext))
- doNothing().`when`(sessionMock).uploadArtifactToS3(any(), any(), any(), any(), isNull(), any())
ToolWindowManager.getInstance(project).registerToolWindow(
RegisterToolWindowTask(
@@ -364,8 +408,58 @@ open class CodeWhispererCodeScanTestBase(projectRule: CodeInsightTestFixtureRule
assertThat(treeModel.getTotalIssuesCount()).isEqualTo(expectedTotalIssues)
}
+ fun createCodeScanIssue(project: Project, virtualFile: VirtualFile): CodeWhispererCodeScanIssue =
+ CodeWhispererCodeScanIssue(
+ project = project,
+ file = virtualFile,
+ startLine = 10,
+ startCol = 5,
+ endLine = 15,
+ endCol = 20,
+ title = "Potential Security Vulnerability",
+ description = Description(
+ text = "A detailed description of the security issue found in the code",
+ markdown = "# Security Issue\n\nA detailed description of the security issue found in the code"
+ ),
+ detectorId = "AWS-DETECTOR-001",
+ detectorName = "SecurityScanner",
+ findingId = "FINDING-123",
+ ruleId = "RULE-456",
+ relatedVulnerabilities = listOf(
+ "CVE-2023-12345",
+ "CVE-2023-67890"
+ ),
+ severity = "HIGH",
+ recommendation = Recommendation(
+ text = "Consider implementing secure coding practices",
+ url = "https://docs.aws.amazon.com/security-best-practices"
+ ),
+ suggestedFixes = emptyList(), // Empty list as requested
+ codeSnippet = listOf(
+ CodeLine(
+ number = 10,
+ content = "val unsecureCode = performOperation()"
+ ),
+ CodeLine(
+ number = 11,
+ content = "processData(unsecureCode)"
+ )
+ )
+ )
+
+// You might need these data classes depending on your implementation
+ data class Description(val message: String)
+ data class Recommendation(val text: String)
+ data class SuggestedFix(val description: String, val code: String)
+ data class CodeLine(val lineNumber: Int, val content: String)
+
companion object {
const val UPLOAD_ID = "uploadId"
const val JOB_ID = "jobId"
+ const val KMS_KEY_ARN = "kmsKeyArn"
+ val REQUEST_HEADERS = mapOf(
+ "Content-Type" to "application/zip",
+ "test" to "aws:test",
+ )
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt
index 45b06857e5..8188cac993 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt
@@ -45,10 +45,10 @@ class CodeWhispererProjectCodeScanTest : CodeWhispererCodeScanTestBase(PythonCod
override fun setup() {
super.setup()
setupCsharpProject()
- sessionConfigSpy = spy(CodeScanSessionConfig.create(testCs, project, CodeWhispererConstants.CodeAnalysisScope.PROJECT))
+ sessionConfigSpy = spy(CodeScanSessionConfig.create(testCs, project, CodeWhispererConstants.CodeAnalysisScope.PROJECT, true))
setupResponse(testCs.toNioPath().relativeTo(sessionConfigSpy.projectRoot.toNioPath()))
- sessionConfigSpy2 = spy(CodeScanSessionConfig.create(testCs, project, CodeWhispererConstants.CodeAnalysisScope.FILE))
+ sessionConfigSpy2 = spy(CodeScanSessionConfig.create(testCs, project, CodeWhispererConstants.CodeAnalysisScope.FILE, true))
setupResponse(testCs.toNioPath().relativeTo(sessionConfigSpy2.projectRoot.toNioPath()))
mockClient.stub {
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/amazonqCommonsConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/amazonqCommonsConnector.ts
index 01af350411..9fb0d54c9a 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/amazonqCommonsConnector.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/amazonqCommonsConnector.ts
@@ -3,15 +3,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { ChatItemAction } from '@aws/mynah-ui-chat'
+import { ChatItemAction, ChatPrompt } from '@aws/mynah-ui-chat'
import { AuthFollowUpType } from '../followUps/generator'
import { ExtensionMessage } from '../commands'
+import {getTabCommandFromTabType, isTabType, TabType } from '../storages/tabsStorage'
+import {codeScanUserGuide, codeTestUserGuide, codeTransformUserGuide, docUserGuide, featureDevUserGuide} from "../texts/constants";
+import {createClickTelemetry, createOpenAgentTelemetry, Trigger} from "../telemetry/actions";
export type WelcomeFollowupType = 'continue-to-chat'
export interface ConnectorProps {
sendMessageToExtension: (message: ExtensionMessage) => void
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => void
+ handleCommand: (chatPrompt: ChatPrompt, tabId: string) => void
}
export interface CodeReference {
licenseName?: string
@@ -26,10 +30,12 @@ export interface CodeReference {
export class Connector {
private readonly sendMessageToExtension
private readonly onWelcomeFollowUpClicked
+ private readonly handleCommand
constructor(props: ConnectorProps) {
this.sendMessageToExtension = props.sendMessageToExtension
this.onWelcomeFollowUpClicked = props.onWelcomeFollowUpClicked
+ this.handleCommand = props.handleCommand
}
followUpClicked = (tabID: string, followUp: ChatItemAction): void => {
@@ -46,4 +52,63 @@ export class Connector {
tabType,
})
}
+
+ onCustomFormAction(
+ tabId: string,
+ action: {
+ id: string
+ text?: string | undefined
+ formItemValues?: Record | undefined
+ }
+ ) {
+ const tabType = action.id.split('-')[2]
+ if (!isTabType(tabType)) {
+ return
+ }
+
+ if (action.id.startsWith('user-guide-')) {
+ this.processUserGuideLink(tabType, action.id)
+ return
+ }
+
+ if (action.id.startsWith('quick-start-')) {
+ this.handleCommand(
+ {
+ command: getTabCommandFromTabType(tabType),
+ },
+ tabId
+ )
+
+ this.sendMessageToExtension(createOpenAgentTelemetry(tabType, 'quick-start'))
+ }
+ }
+
+ private processUserGuideLink(tabType: TabType, actionId: string) {
+ let userGuideLink = ''
+ switch (tabType) {
+ case 'codescan':
+ userGuideLink = codeScanUserGuide
+ break
+ case 'codetest':
+ userGuideLink = codeTestUserGuide
+ break
+ case 'codetransform':
+ userGuideLink = codeTransformUserGuide
+ break
+ case 'doc':
+ userGuideLink = docUserGuide
+ break
+ case 'featuredev':
+ userGuideLink = featureDevUserGuide
+ break
+ }
+
+ // e.g. amazonq-explore-user-guide-featuredev
+ this.sendMessageToExtension(createClickTelemetry(`amazonq-explore-${actionId}`))
+
+ this.sendMessageToExtension({
+ command: 'open-user-guide',
+ userGuideLink,
+ })
+ }
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeScanChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeScanChatConnector.ts
new file mode 100644
index 0000000000..60ffe3222f
--- /dev/null
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeScanChatConnector.ts
@@ -0,0 +1,183 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ExtensionMessage} from "../commands";
+import {ChatItem, ChatItemType, ProgressField} from "@aws/mynah-ui-chat";
+import {FormButtonIds} from "../forms/constants";
+
+export interface ICodeScanChatConnectorProps {
+ sendMessageToExtension: (message: ExtensionMessage) => void
+ onCodeScanMessageReceived: (tabID: string, message: ChatItem, isLoading: boolean, clearPreviousItemButtons?: boolean, runReview?: boolean) => void
+ onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void,
+ onUpdatePromptProgress: (tabID: string, progressField: ProgressField | null | undefined) => void
+ onChatInputEnabled: (tabID: string, enabled: boolean) => void
+}
+
+export class CodeScanChatConnector {
+ private readonly sendMessageToExtension
+ private readonly onCodeScanMessageReceived
+ private readonly updatePlaceholder
+ private readonly updatePromptProgress
+ private readonly chatInputEnabled
+
+ constructor(props: ICodeScanChatConnectorProps) {
+ this.sendMessageToExtension = props.sendMessageToExtension
+ this.onCodeScanMessageReceived = props.onCodeScanMessageReceived
+ this.updatePlaceholder = props.onUpdatePlaceholder
+ this.updatePromptProgress = props.onUpdatePromptProgress
+ this.chatInputEnabled = props.onChatInputEnabled
+ }
+
+ private processChatMessage = (messageData: any): void => {
+ const runReview = messageData.command === "review"
+ if (this.onCodeScanMessageReceived === undefined) {
+ return
+ }
+
+ const tabID = messageData.tabID
+ const isAddingNewItem: boolean = messageData.isAddingNewItem
+ const isLoading: boolean = messageData.isLoading
+ const clearPreviousItemButtons: boolean = messageData.clearPreviousItemButtons
+ const type = messageData.messageType
+
+ if (isAddingNewItem && type === ChatItemType.ANSWER_PART) {
+ this.onCodeScanMessageReceived(tabID, {
+ type: ChatItemType.ANSWER_STREAM,
+ }, isLoading)
+ }
+
+ const chatItem: ChatItem = {
+ type: type,
+ body: messageData.message ?? undefined,
+ messageId: messageData.messageId ?? messageData.triggerID ?? '',
+ relatedContent: undefined,
+ canBeVoted: messageData.canBeVoted ?? true,
+ formItems: messageData.formItems,
+ buttons:
+ messageData.buttons !== undefined && messageData.buttons.length > 0 ? messageData.buttons : undefined,
+ followUp:
+ messageData.followUps !== undefined && messageData.followUps.length > 0
+ ? {
+ text: '',
+ options: messageData.followUps,
+ }
+ : undefined
+ }
+ this.onCodeScanMessageReceived(tabID, chatItem, isLoading, clearPreviousItemButtons, runReview)
+ }
+
+ handleMessageReceive = async (messageData: any): Promise => {
+ if (messageData.type === 'chatMessage') {
+ this.processChatMessage(messageData)
+ return
+ }
+ if (messageData.type === 'updatePlaceholderMessage') {
+ this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder)
+ return
+ }
+
+ if(messageData.type === 'updatePromptProgress') {
+ this.updatePromptProgress(messageData.tabID, messageData.progressField)
+ }
+
+ if(messageData.type === 'chatInputEnabledMessage') {
+ this.chatInputEnabled(messageData.tabID, messageData.enabled)
+ }
+ }
+
+ onFormButtonClick = (
+ tabID: string,
+ action: {
+ id: string
+ text?: string
+ formItemValues?: Record
+ }
+ ) => {
+ if (action.id === FormButtonIds.CodeScanStartProjectScan) {
+ this.sendMessageToExtension({
+ command: 'codescan_start_project_scan',
+ tabID,
+ tabType: 'codescan',
+ })
+ } else if (action.id === FormButtonIds.CodeScanStartFileScan) {
+ this.sendMessageToExtension({
+ command: 'codescan_start_file_scan',
+ tabID,
+ tabType: 'codescan'
+ })
+ } else if (action.id === FormButtonIds.CodeScanStopFileScan) {
+ this.sendMessageToExtension({
+ command: 'codescan_stop_file_scan',
+ tabID,
+ tabType: 'codescan'
+ })
+ } else if (action.id === FormButtonIds.CodeScanStopProjectScan) {
+ this.sendMessageToExtension({
+ command: 'codescan_stop_project_scan',
+ tabID,
+ tabType: 'codescan'
+ })
+ } else if (action.id === FormButtonIds.CodeScanOpenIssues) {
+ this.sendMessageToExtension({
+ command: 'codescan_open_issues',
+ tabID,
+ tabType: 'codescan'
+ })
+ }
+ }
+ onResponseBodyLinkClick(tabID: string, messageId: string, link: string) {
+ this.sendMessageToExtension({
+ command: 'response-body-link-click',
+ tabID,
+ messageId,
+ link,
+ tabType: 'codescan',
+ })
+ }
+
+ clearChat = (tabID: string): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'clear',
+ chatMessage: '',
+ tabType: 'codescan',
+ })
+ }
+
+ help = (tabID: string): void => {
+ console.log("reached here")
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'help',
+ chatMessage: '',
+ tabType: 'codescan',
+ })
+ }
+
+ onTabOpen = (tabID: string) => {
+ this.sendMessageToExtension({
+ tabID,
+ command: 'new-tab-was-created',
+ tabType: 'codescan'
+ })
+ }
+
+ onTabRemove = (tabID: string) => {
+ this.sendMessageToExtension({
+ tabID,
+ command: 'tab-was-removed',
+ tabType: 'codescan'
+ })
+ }
+
+ scan = (tabID: string): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'scan',
+ chatMessage: 'scan',
+ tabType: 'codescan'
+ })
+ }
+}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTestChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTestChatConnector.ts
new file mode 100644
index 0000000000..5a6e174497
--- /dev/null
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTestChatConnector.ts
@@ -0,0 +1,584 @@
+// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {ExtensionMessage} from "../commands";
+import {ChatPayload, ConnectorProps} from "../connector";
+import {FormButtonIds} from "../forms/constants";
+import {ChatItem, ChatItemAction, ChatItemType, MynahIcons, MynahUIDataModel} from '@aws/mynah-ui-chat'
+import {CodeReference} from "./amazonqCommonsConnector";
+import {Status} from "@aws/mynah-ui-chat/dist/static";
+import {EmptyMynahUIDataModel} from "@aws/mynah-ui-chat/dist/helper/store";
+import {doesNotMatch} from "node:assert";
+
+export interface ICodeTestChatConnectorProps {
+ sendMessageToExtension: (message: ExtensionMessage) => void
+ onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
+ onUpdateAuthentication: (
+ featureDevEnabled: boolean,
+ codeTransformEnabled: boolean,
+ docEnabled: boolean,
+ codeScanEnabled: boolean,
+ codeTestEnabled: boolean,
+ authenticatingTabIDs: string[]
+ ) => void
+ onRunTestMessageReceived?: (tabID: string, showRunTestMessage: boolean) => void
+ onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
+ onChatInputEnabled: (tabID: string, enabled: boolean) => void
+ onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
+ onError: (tabID: string, message: string, title: string) => void
+}
+
+interface IntroductionCardContentType {
+ title: string
+ description: string
+ icon: MynahIcons
+ content: {
+ body: string
+ }
+}
+
+interface MessageData {
+ message?: string
+ messageType: ChatItemType
+ messageId?: string
+ triggerID?: string
+ informationCard?: boolean
+ canBeVoted: boolean
+ filePath?: string
+ tabID: string
+}
+
+function getIntroductionCardContent(): IntroductionCardContentType {
+ const introductionBody = [
+ "I can generate unit tests for your active file. ",
+ "\n\n",
+ "After you select the functions or methods I should focus on, I will:\n",
+ "1. Generate unit tests\n",
+ "2. Place them into relevant test file\n",
+ "\n\n",
+ "To learn more, check out our [user guide](https://aws.amazon.com/q/developer/)."
+ ].join("");
+
+ return {
+ title: "/test",
+ description: "Included in your Q Developer subscription",
+ icon: MynahIcons.CHECK_LIST,
+ content: {
+ body: introductionBody
+ }
+ }
+}
+
+export class CodeTestChatConnector {
+ private readonly sendMessageToExtension
+ private readonly onChatAnswerReceived
+ private readonly onChatAnswerUpdated
+ private readonly onMessageReceived
+ private readonly onUpdateAuthentication
+ private readonly chatInputEnabled
+ private readonly updatePlaceholder
+ private readonly updatePromptProgress
+ private readonly onError
+ private readonly runTestMessageReceived
+
+ constructor(props: ConnectorProps) {
+ this.sendMessageToExtension = props.sendMessageToExtension
+ this.onChatAnswerReceived = props.onChatAnswerReceived
+ this.onChatAnswerUpdated = props.onChatAnswerUpdated
+ this.runTestMessageReceived = props.onRunTestMessageReceived
+ this.onMessageReceived = props.onMessageReceived
+ this.onUpdateAuthentication = props.onUpdateAuthentication
+ this.chatInputEnabled = props.onChatInputEnabled
+ this.updatePlaceholder = props.onUpdatePlaceholder
+ this.updatePromptProgress = props.onUpdatePromptProgress
+ this.onError = props.onError
+ }
+
+ private addAnswer = (messageData: any): void => {
+ console.log("message data in addAnswer:")
+ console.log(messageData)
+ if (this.onChatAnswerReceived === undefined) {
+ return
+ }
+ if (messageData.command === 'test' && this.runTestMessageReceived) {
+ this.runTestMessageReceived(messageData.tabID, true)
+ return
+ }
+ const answer: ChatItem = {
+ type: messageData.messageType,
+ messageId: messageData.messageId ?? messageData.triggerID,
+ body: messageData.message,
+ relatedContent: undefined,
+ snapToTop: messageData.snapToTop,
+ canBeVoted: messageData.canBeVoted ?? false,
+ followUp: messageData.followUps ? {
+ text: '',
+ options: messageData.followUps,
+ } : undefined,
+ buttons: messageData.buttons ?? undefined,
+ fileList: messageData.fileList ? {
+ rootFolderTitle: messageData.projectRootName,
+ fileTreeTitle: 'READY FOR REVIEW',
+ filePaths: messageData.fileList,
+ details: {
+ [messageData.filePaths]: {
+ icon: MynahIcons.FILE,
+ },
+ },
+ } : undefined,
+ codeBlockActions: {
+ 'insert-to-cursor': undefined
+ },
+ codeReference: messageData.codeReference?.length ? messageData.codeReference : undefined
+ }
+
+ this.onChatAnswerReceived(messageData.tabID, answer)
+ }
+
+ private updateAnswer = (messageData: any): void => {
+ console.log("message data in updateAnswer:")
+ console.log(messageData)
+ if (this.onChatAnswerUpdated == undefined) {
+ return
+ }
+ const answer: ChatItem = {
+ type: messageData.messageType,
+ messageId: messageData.messageId ?? messageData.triggerID,
+ body: messageData.message,
+ buttons: messageData.buttons ?? undefined,
+ followUp: messageData.followUps ? {
+ text: '',
+ options: messageData.followUps,
+ } : undefined,
+ canBeVoted: true,
+ fileList: messageData.fileList ? {
+ rootFolderTitle: messageData.projectRootName,
+ fileTreeTitle: 'READY FOR REVIEW',
+ filePaths: messageData.fileList,
+ details: {
+ [messageData.fileList]: {
+ icon: MynahIcons.FILE,
+ },
+ },
+ } : undefined,
+ footer: messageData.footer ? {
+ fileList: {
+ rootFolderTitle: undefined,
+ fileTreeTitle: '',
+ filePaths: messageData.footer,
+ details: {
+ [messageData.footer]: {
+ icon: MynahIcons.FILE,
+ },
+ },
+ },
+ } : undefined,
+ codeReference: messageData.codeReference?.length ? messageData.codeReference : undefined
+ }
+ this.onChatAnswerUpdated(messageData.tabID, answer)
+ }
+
+ private updateUI = (messageData: any): void => {
+ if (!this.onMessageReceived) {
+ return
+ }
+
+ const settings: MynahUIDataModel = {
+ ...(messageData.loadingChat !== undefined ? { loadingChat: messageData.loadingChat } : {}),
+ ...(messageData.cancelButtonWhenLoading !== undefined ? { cancelButtonWhenLoading: messageData.cancelButtonWhenLoading } : {}),
+ ...(messageData.promptInputPlaceholder !== undefined ? { promptInputPlaceholder: messageData.promptInputPlaceholder } : {}),
+ ...(messageData.promptInputProgress !== undefined ? { promptInputProgress: messageData.promptInputProgress } : {}),
+ // ...(messageData.promptInputDisabledState !== undefined ? { promptInputDisabledState: messageData.promptInputDisabledState } : {}),
+ }
+
+ console.log("UI settings to be updated")
+ console.log(settings)
+ this.onMessageReceived(messageData.tabID, settings, false)
+ this.chatInputEnabled(messageData.tabID, !messageData.promptInputDisabledState)
+ }
+
+ private processChatMessage = async (messageData: any): Promise => {
+ if (!this.onChatAnswerReceived) {
+ return
+ }
+ if (messageData.message === undefined && !messageData.informationCard) {
+ return
+ }
+ const answer: ChatItem = {
+ type: messageData.messageType,
+ messageId: messageData.messageId || messageData.triggerID,
+ body: messageData.informationCard ? "" : messageData.message,
+ canBeVoted: messageData.canBeVoted,
+ informationCard: messageData.informationCard ? getIntroductionCardContent() : undefined,
+ footer: messageData.filePath
+ ? {
+ fileList: {
+ rootFolderTitle: undefined,
+ fileTreeTitle: '',
+ filePaths: [messageData.filePaths],
+ },
+ }
+ : undefined,
+ }
+ this.onChatAnswerReceived(messageData.tabID, answer)
+ }
+
+ private processAuthNeededException = async (messageData: any): Promise => {
+ if (this.onChatAnswerReceived === undefined) {
+ return
+ }
+ this.onChatAnswerReceived(
+ messageData.tabID,
+ {
+ type: ChatItemType.SYSTEM_PROMPT,
+ body: messageData.message,
+ }
+ )
+ }
+
+ private processCodeResultMessage = async (messageData: any): Promise => {
+ if (this.onChatAnswerReceived !== undefined) {
+ const answer: ChatItem = {
+ type: ChatItemType.ANSWER,
+ canBeVoted: true,
+ messageId: messageData.uploadId,
+ followUp: {
+ text: '',
+ options: messageData.followUps,
+ },
+ fileList: {
+ fileTreeTitle: 'READY FOR REVIEW',
+ rootFolderTitle: messageData.projectName,
+ filePaths: messageData.filePaths,
+ },
+ body: messageData.message,
+ }
+ this.onChatAnswerReceived(messageData.tabID, answer)
+ }
+ }
+
+
+ private processChatAIPromptMessage = async (messageData: any): Promise => {
+ if (this.onChatAnswerReceived === undefined) {
+ return
+ }
+
+ if (messageData.message !== undefined) {
+ const answer: ChatItem = {
+ type: messageData.messageType,
+ messageId: messageData.messageId ?? messageData.triggerID,
+ body: messageData.message,
+ relatedContent: undefined,
+ snapToTop: messageData.snapToTop,
+ canBeVoted: false,
+ }
+
+ this.onChatAnswerReceived(messageData.tabID, answer)
+ }
+ }
+
+ private processChatSummaryMessage = async (messageData: any): Promise => {
+ if (this.onChatAnswerUpdated === undefined) {
+ return
+ }
+ if (messageData.message !== undefined) {
+ const answer: ChatItem = {
+ type: messageData.messageType,
+ messageId: messageData.messageId ?? messageData.triggerID,
+ body: messageData.message,
+ buttons: messageData.buttons ?? [],
+ canBeVoted: true,
+ footer: {
+ fileList: {
+ rootFolderTitle: undefined,
+ fileTreeTitle: '',
+ filePaths: [messageData.filePath],
+ },
+ },
+ }
+ this.onChatAnswerUpdated(messageData.tabID, answer)
+ }
+ }
+
+ handleMessageReceive = async (messageData: any): Promise => {
+ // TODO: Implement the logic to handle received messages for Unit Test generator chat
+ switch(messageData.type){
+ case 'authNeededException':
+ await this.processAuthNeededException(messageData)
+ break
+ case 'authenticationUpdateMessage':
+ this.onUpdateAuthentication(
+ messageData.featureDevEnabled,
+ messageData.codeTransformEnabled,
+ messageData.docEnabled,
+ messageData.codeScanEnabled,
+ messageData.codeTestEnabled,
+ messageData.authenticatingTabIDs
+ )
+ break
+ case 'chatInputEnabledMessage':
+ this.chatInputEnabled(messageData.tabID, messageData.enabled)
+ break
+ case 'updatePromptProgress':
+ this.updatePromptProgress(messageData.tabID, messageData.progressField)
+ break
+ case 'chatMessage':
+ await this.processChatMessage(messageData)
+ break
+ case 'addAnswer':
+ this.addAnswer(messageData)
+ break
+ case 'updateAnswer':
+ this.updateAnswer(messageData)
+ break
+ case 'chatAIPromptMessage':
+ await this.processChatAIPromptMessage(messageData)
+ break
+ case 'chatSummaryMessage':
+ await this.processChatSummaryMessage(messageData)
+ break
+ case 'updatePlaceholderMessage':
+ this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder)
+ break
+ case 'codeResultMessage':
+ await this.processCodeResultMessage(messageData)
+ break
+ case 'errorMessage':
+ this.onError(messageData.tabID, messageData.message, messageData.title)
+ break
+ case 'updateUI':
+ this.updateUI(messageData)
+ break
+ }
+ }
+
+ onFormButtonClick = (
+ tabID: string,
+ messageId: string,
+ action: {
+ id: string
+ text?: string
+ formItemValues?: Record
+ }
+ ) => {
+ if (action === undefined) {
+ return
+ }
+
+ this.sendMessageToExtension({
+ command: 'button-click',
+ actionID: action.id,
+ formSelectedValues: action.formItemValues,
+ tabType: 'codetest',
+ tabID: tabID,
+ })
+
+ if (this.onChatAnswerUpdated === undefined) {
+ return
+ }
+
+ const answer: ChatItem = {
+ type: ChatItemType.ANSWER,
+ messageId: messageId,
+ buttons: []
+ };
+
+ switch (action.id) {
+ case FormButtonIds.CodeTestViewDiff:
+ // does nothing
+ break
+ case FormButtonIds.CodeTestAccept:
+ answer.buttons = [
+ {
+ keepCardAfterClick: true,
+ text: 'Accepted',
+ id: 'utg_accepted',
+ status: 'success',
+ position: 'outside',
+ disabled: true
+ }
+ ];
+ break;
+ case FormButtonIds.CodeTestReject:
+ answer.buttons = [
+ {
+ keepCardAfterClick: true,
+ text: 'Rejected',
+ id: 'utg_rejected',
+ status: 'error',
+ position: 'outside',
+ disabled: true
+ }
+ ];
+ break;
+ case FormButtonIds.CodeTestBuildAndExecute:
+ answer.buttons = [
+ {
+ keepCardAfterClick: true,
+ text: 'Build and execute',
+ id: 'utg_build_and_execute',
+ status: 'primary',
+ position: 'outside',
+ disabled: true
+ }
+ ]
+ break;
+ case FormButtonIds.CodeTestSkipAndFinish:
+ answer.buttons = [
+ {
+ keepCardAfterClick: true,
+ text: 'Skip and finish',
+ id: 'utg_skip_and_finish',
+ status: 'primary',
+ position: 'outside',
+ disabled: true
+ }
+ ]
+ break;
+ /*
+ //TODO: generate button
+ case FormButtonIds.CodeTestRegenerate:
+ answer.buttons = [
+ {
+ keepCardAfterClick: true,
+ text: 'Regenerate',
+ id: 'utg_regenerate',
+ status: 'info',
+ position: 'outside',
+ disabled: true
+ }
+ ]
+ break;
+ */
+ case FormButtonIds.CodeTestRejectAndRevert:
+ // TODO: what behavior should this be?
+ break;
+ case FormButtonIds.CodeTestProceed:
+ answer.buttons = [
+ {
+ keepCardAfterClick: true,
+ text: 'Proceeded',
+ id: 'utg_proceeded',
+ status: 'primary',
+ position: 'outside',
+ disabled: true
+ }
+ ]
+ break;
+ case FormButtonIds.CodeTestModifyCommand:
+ answer.buttons = [
+ {
+ keepCardAfterClick: true,
+ text: 'Modify command',
+ id: 'utg_modify_command',
+ status: 'primary',
+ position: 'outside',
+ disabled: true
+ }
+ ]
+ break;
+ default:
+ console.warn(`Unhandled action ID: ${action.id}`);
+ break;
+ }
+
+ this.onChatAnswerUpdated(tabID, answer);
+ }
+
+ clearChat = (tabID: string): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'clear',
+ chatMessage: '',
+ tabType: 'codetest',
+ })
+ }
+
+ help = (tabID: string): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'help',
+ chatMessage: '',
+ tabType: 'codetest',
+ })
+ }
+
+ onTabOpen = (tabID: string) => {
+ this.sendMessageToExtension({
+ tabID,
+ command: 'new-tab-was-created',
+ tabType: 'codetest'
+ })
+ }
+
+ /**
+ * Ignore for this pr, this request Answer function is used to in the future to receive users' input
+ */
+ requestAnswer = (tabID: string, payload: ChatPayload) => {
+ this.sendMessageToExtension({
+ tabID,
+ command: 'chat-prompt',
+ chatMessage: payload.chatMessage,
+ tabType: 'codetest'
+ })
+ }
+
+ onTabRemove = (tabID: string) => {
+ this.sendMessageToExtension({
+ tabID,
+ command: 'tab-was-removed',
+ tabType: 'codetest'
+ })
+ }
+
+ startTestGen = (tabID: string, prompt: string): void => {
+ console.log("calling generate-test here")
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'start-test-gen',
+ prompt,
+ tabType: 'codetest'
+ })
+ }
+
+ onCodeInsertToCursorPosition = (tabID: string, code?: string, type?: 'selection' | 'block', codeReference?: CodeReference[]): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ code,
+ command: 'insert_code_at_cursor_position',
+ codeReference,
+ tabType: 'codetest'
+ })
+ }
+
+ onFileClick = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => {
+ this.sendMessageToExtension({
+ command: 'open-diff',
+ tabID,
+ filePath,
+ deleted,
+ messageId,
+ tabType: 'codetest',
+ })
+ }
+
+ followUpClicked = (tabID: string, followUp: ChatItemAction): void => {
+ this.sendMessageToExtension({
+ command: 'follow-up-was-clicked',
+ followUp,
+ tabID,
+ tabType: 'codetest',
+ })
+ }
+
+ onResponseBodyLinkClick = (tabID: string, messageId: string, link: string): void => {
+ this.sendMessageToExtension({
+ command: 'response-body-link-click',
+ tabID,
+ messageId,
+ link,
+ tabType: 'codetest',
+ })
+ }
+
+}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts
index b335a3d6fb..4a9dc9a141 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts
@@ -20,7 +20,14 @@ export interface ICodeTransformChatConnectorProps {
onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
onNotification: (props: {content: string; title?: string; type: NotificationType}) => void
onStartNewTransform: (tabID: string) => void
- onUpdateAuthentication: (featureDevEnabled: boolean, codeTransformEnabled: boolean, authenticatingTabIDs: string[]) => void
+ onUpdateAuthentication: (
+ featureDevEnabled: boolean,
+ codeTransformEnabled: boolean,
+ docEnabled: boolean,
+ codeScanEnabled: boolean,
+ codeTestEnabled: boolean,
+ authenticatingTabIDs: string[]
+ ) => void
tabsStorage: TabsStorage
onNewTab: (tabType: TabType) => void
}
@@ -201,7 +208,14 @@ export class CodeTransformChatConnector {
}
if (messageData.type === 'authenticationUpdateMessage') {
- this.onUpdateAuthentication(messageData.featureDevEnabled, messageData.codeTransformEnabled, messageData.authenticatingTabIDs)
+ this.onUpdateAuthentication(
+ messageData.featureDevEnabled,
+ messageData.codeTransformEnabled,
+ messageData.docEnabled,
+ messageData.codeScanEnabled,
+ messageData.codeTestEnabled,
+ messageData.authenticatingTabIDs
+ )
return
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/docChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/docChatConnector.ts
new file mode 100644
index 0000000000..07fa03b9f1
--- /dev/null
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/docChatConnector.ts
@@ -0,0 +1,397 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ChatItem, ChatItemAction, ChatItemType, FeedbackPayload, MynahIcons, ProgressField } from '@aws/mynah-ui-chat'
+import { ExtensionMessage } from '../commands'
+import { TabType, TabsStorage } from '../storages/tabsStorage'
+import { CodeReference } from './amazonqCommonsConnector'
+import { FollowUpGenerator } from '../followUps/generator'
+import { getActions } from '../diffTree/actions'
+import { DiffTreeFileInfo } from '../diffTree/types'
+
+interface ChatPayload {
+ chatMessage: string
+}
+
+export interface ConnectorProps {
+ sendMessageToExtension: (message: ExtensionMessage) => void
+ onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
+ onUpdatePromptProgress: (tabID: string, progressField: ProgressField) => void
+ onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string) => void
+ onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
+ sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined
+ onError: (tabID: string, message: string, title: string) => void
+ onWarning: (tabID: string, message: string, title: string) => void
+ onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
+ onChatInputEnabled: (tabID: string, enabled: boolean) => void
+ onUpdateAuthentication: (
+ featureDevEnabled: boolean,
+ codeTransformEnabled: boolean,
+ docEnabled: boolean,
+ codeScanEnabled: boolean,
+ codeTestEnabled: boolean,
+ authenticatingTabIDs: string[]
+ ) => void
+ onNewTab: (tabType: TabType) => void
+ tabsStorage: TabsStorage
+ onFileComponentUpdate: (
+ tabID: string,
+ filePaths: DiffTreeFileInfo[],
+ deletedFiles: DiffTreeFileInfo[],
+ messageId: string
+ ) => void
+}
+
+export class Connector {
+ private readonly sendMessageToExtension
+ private readonly onError
+ private readonly onWarning
+ private readonly onChatAnswerReceived
+ private readonly onUpdatePromptProgress
+ private readonly onAsyncEventProgress
+ private readonly updatePlaceholder
+ private readonly chatInputEnabled
+ private readonly onUpdateAuthentication
+ private readonly followUpGenerator: FollowUpGenerator
+ private readonly onNewTab
+ private readonly onFileComponentUpdate
+
+ constructor(props: ConnectorProps) {
+ this.sendMessageToExtension = props.sendMessageToExtension
+ this.onChatAnswerReceived = props.onChatAnswerReceived
+ this.onWarning = props.onWarning
+ this.onError = props.onError
+ this.onUpdatePromptProgress = props.onUpdatePromptProgress
+ this.onAsyncEventProgress = props.onAsyncEventProgress
+ this.updatePlaceholder = props.onUpdatePlaceholder
+ this.chatInputEnabled = props.onChatInputEnabled
+ this.onUpdateAuthentication = props.onUpdateAuthentication
+ this.followUpGenerator = new FollowUpGenerator()
+ this.onNewTab = props.onNewTab
+ this.onFileComponentUpdate = props.onFileComponentUpdate
+ }
+
+ onCodeInsertToCursorPosition = (
+ tabID: string,
+ code?: string,
+ type?: 'selection' | 'block',
+ codeReference?: CodeReference[]
+ ): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ code,
+ command: 'insert_code_at_cursor_position',
+ codeReference,
+ tabType: 'doc',
+ })
+ }
+
+ onCopyCodeToClipboard = (
+ tabID: string,
+ code?: string,
+ type?: 'selection' | 'block',
+ codeReference?: CodeReference[]
+ ): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ code,
+ command: 'code_was_copied_to_clipboard',
+ codeReference,
+ tabType: 'doc',
+ })
+ }
+
+ onOpenDiff = (tabID: string, filePath: string, deleted: boolean): void => {
+ this.sendMessageToExtension({
+ command: 'open-diff',
+ tabID,
+ filePath,
+ deleted,
+ tabType: 'doc',
+ })
+ }
+
+ followUpClicked = (tabID: string, followUp: ChatItemAction): void => {
+ this.sendMessageToExtension({
+ command: 'follow-up-was-clicked',
+ followUp,
+ tabID,
+ tabType: 'doc',
+ })
+ }
+
+ requestGenerativeAIAnswer = (tabID: string, payload: ChatPayload): Promise =>
+ new Promise((resolve, reject) => {
+ const message: ExtensionMessage = {
+ tabID: tabID,
+ command: 'chat-prompt',
+ chatMessage: payload.chatMessage,
+ tabType: 'doc',
+ }
+ this.sendMessageToExtension(message)
+ })
+
+ onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => {
+ this.sendMessageToExtension({
+ command: 'file-click',
+ tabID,
+ messageId,
+ filePath,
+ actionName,
+ tabType: 'doc',
+ })
+ }
+
+ private processFolderConfirmationMessage = async (messageData: any, folderPath: string): Promise => {
+ if (this.onChatAnswerReceived !== undefined) {
+ const answer: ChatItem = {
+ type: ChatItemType.ANSWER,
+ body: messageData.message ?? undefined,
+ messageId: messageData.messageID ?? messageData.triggerID ?? '',
+ fileList: {
+ rootFolderTitle: undefined,
+ fileTreeTitle: '',
+ filePaths: [folderPath],
+ details: {
+ [folderPath]: {
+ icon: MynahIcons.FOLDER,
+ clickable: false,
+ },
+ },
+ },
+ followUp: {
+ text: '',
+ options: messageData.followUps,
+ },
+ }
+ this.onChatAnswerReceived(messageData.tabID, answer)
+ }
+ }
+
+ private processChatMessage = async (messageData: any): Promise => {
+ if (this.onChatAnswerReceived !== undefined) {
+ const answer: ChatItem = {
+ type: messageData.messageType,
+ body: messageData.message ?? undefined,
+ messageId: messageData.messageID ?? messageData.triggerID ?? '',
+ relatedContent: undefined,
+ canBeVoted: messageData.canBeVoted,
+ snapToTop: messageData.snapToTop,
+ followUp:
+ messageData.followUps !== undefined && messageData.followUps.length > 0
+ ? {
+ text:
+ messageData.messageType === ChatItemType.SYSTEM_PROMPT
+ ? ''
+ : 'Please follow up with one of these',
+ options: messageData.followUps,
+ }
+ : undefined,
+ }
+ this.onChatAnswerReceived(messageData.tabID, answer)
+ }
+ }
+
+ private processCodeResultMessage = async (messageData: any): Promise => {
+ if (this.onChatAnswerReceived !== undefined) {
+ const actions = getActions([...messageData.filePaths, ...messageData.deletedFiles])
+ const answer: ChatItem = {
+ type: ChatItemType.ANSWER,
+ relatedContent: undefined,
+ followUp: undefined,
+ canBeVoted: false,
+ codeReference: messageData.references,
+ // TODO get the backend to store a message id in addition to conversationID
+ messageId: messageData.messageID ?? messageData.triggerID ?? messageData.conversationID,
+ fileList: {
+ fileTreeTitle: 'Documents ready',
+ rootFolderTitle: 'Generated documentation',
+ filePaths: (messageData.filePaths as DiffTreeFileInfo[]).map(path => path.zipFilePath),
+ deletedFiles: (messageData.deletedFiles as DiffTreeFileInfo[]).map(path => path.zipFilePath),
+ actions,
+ },
+ body: '',
+ }
+ this.onChatAnswerReceived(messageData.tabID, answer)
+ }
+ }
+
+ private processAuthNeededException = async (messageData: any): Promise => {
+ if (this.onChatAnswerReceived === undefined) {
+ return
+ }
+
+ this.onChatAnswerReceived(messageData.tabID, {
+ type: ChatItemType.ANSWER,
+ body: messageData.message,
+ followUp: undefined,
+ canBeVoted: false,
+ })
+
+ this.onChatAnswerReceived(messageData.tabID, {
+ type: ChatItemType.SYSTEM_PROMPT,
+ body: undefined,
+ followUp: this.followUpGenerator.generateAuthFollowUps('doc', messageData.authType),
+ canBeVoted: false,
+ })
+
+ return
+ }
+
+ handleMessageReceive = async (messageData: any): Promise => {
+ if (messageData.type === 'updateFileComponent') {
+ this.onFileComponentUpdate(
+ messageData.tabID,
+ messageData.filePaths,
+ messageData.deletedFiles,
+ messageData.messageId
+ )
+ return
+ }
+ if (messageData.type === 'errorMessage') {
+ this.onError(messageData.tabID, messageData.message, messageData.title)
+ return
+ }
+
+ if (messageData.type === 'showInvalidTokenNotification') {
+ this.onWarning(messageData.tabID, messageData.message, messageData.title)
+ return
+ }
+
+ if (messageData.type === 'folderConfirmationMessage') {
+ await this.processFolderConfirmationMessage(messageData, messageData.folderPath)
+ return
+ }
+
+ if (messageData.type === 'chatMessage') {
+ await this.processChatMessage(messageData)
+ return
+ }
+
+ if (messageData.type === 'codeResultMessage') {
+ await this.processCodeResultMessage(messageData)
+ return
+ }
+
+ if (messageData.type === 'asyncEventProgressMessage') {
+ this.onAsyncEventProgress(messageData.tabID, messageData.inProgress, messageData.message ?? undefined)
+ return
+ }
+
+ if (messageData.type === 'updatePlaceholderMessage') {
+ this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder)
+ return
+ }
+
+ if (messageData.type === 'chatInputEnabledMessage') {
+ this.chatInputEnabled(messageData.tabID, messageData.enabled)
+ return
+ }
+
+ if (messageData.type === 'authenticationUpdateMessage') {
+ this.onUpdateAuthentication(
+ messageData.featureDevEnabled,
+ messageData.codeTransformEnabled,
+ messageData.docEnabled,
+ messageData.codeScanEnabled,
+ messageData.codeTestEnabled,
+ messageData.authenticatingTabIDs
+ )
+ return
+ }
+
+ if (messageData.type === 'authNeededException') {
+ this.processAuthNeededException(messageData)
+ return
+ }
+
+ if (messageData.type === 'openNewTabMessage') {
+ this.onNewTab('doc')
+ return
+ }
+
+ if (messageData.type === 'updatePromptProgress') {
+ this.onUpdatePromptProgress(messageData.tabId, messageData.progressField)
+ }
+ }
+
+ onStopChatResponse = (tabID: string): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'stop-response',
+ })
+ }
+
+ onTabOpen = (tabID: string): void => {
+ this.sendMessageToExtension({
+ tabID,
+ command: 'new-tab-was-created',
+ tabType: 'doc',
+ })
+ }
+
+ onTabRemove = (tabID: string): void => {
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'tab-was-removed',
+ tabType: 'doc',
+ })
+ }
+
+ sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => {
+ this.sendMessageToExtension({
+ command: 'chat-item-feedback',
+ ...feedbackPayload,
+ tabType: 'doc',
+ tabID: tabId,
+ })
+ }
+
+ onChatItemVoted = (tabId: string, messageId: string, vote: string): void | undefined => {
+ this.sendMessageToExtension({
+ tabID: tabId,
+ messageId: messageId,
+ vote: vote,
+ command: 'chat-item-voted',
+ tabType: 'doc',
+ })
+ }
+
+ onResponseBodyLinkClick = (tabID: string, messageId: string, link: string): void => {
+ this.sendMessageToExtension({
+ command: 'response-body-link-click',
+ tabID,
+ messageId,
+ link,
+ tabType: 'doc',
+ })
+ }
+
+ sendFolderConfirmationMessage = (tabID: string, messageId: string): void => {
+ this.sendMessageToExtension({
+ command: 'folderConfirmationMessage',
+ tabID,
+ messageId,
+ tabType: 'doc',
+ })
+ }
+
+ onFormButtonClick = (
+ tabID: string,
+ action: {
+ id: string
+ text?: string
+ formItemValues?: Record
+ }
+ ) => {
+ if (action.id === "doc_stop_generate") {
+ this.sendMessageToExtension({
+ command: 'doc_stop_generate',
+ tabID,
+ tabType: 'doc',
+ })
+ }
+ }
+}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts
index bfef0844e0..14b46ce310 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts
@@ -26,7 +26,14 @@ export interface ConnectorProps {
onWarning: (tabID: string, message: string, title: string) => void
onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
onChatInputEnabled: (tabID: string, enabled: boolean) => void
- onUpdateAuthentication: (featureDevEnabled: boolean, codeTransformEnabled: boolean, authenticatingTabIDs: string[]) => void
+ onUpdateAuthentication: (
+ featureDevEnabled: boolean,
+ codeTransformEnabled: boolean,
+ docEnabled: boolean,
+ codeScanEnabled: boolean,
+ codeTestEnabled: boolean,
+ authenticatingTabIDs: string[]
+ ) => void
onNewTab: (tabType: TabType) => void
tabsStorage: TabsStorage
onFileComponentUpdate: (tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[], messageId: string, disableFileActions: boolean) => void
@@ -196,6 +203,7 @@ export class Connector {
relatedContent: undefined,
canBeVoted: messageData.canBeVoted ?? undefined,
snapToTop: messageData.snapToTop ?? undefined,
+ informationCard: messageData.informationCard ?? undefined,
followUp:
messageData.followUps !== undefined && Array.isArray(messageData.followUps)
? {
@@ -256,7 +264,14 @@ export class Connector {
}
if (messageData.type === 'authenticationUpdateMessage') {
- this.onUpdateAuthentication(messageData.featureDevEnabled, messageData.codeTransformEnabled, messageData.authenticatingTabIDs)
+ this.onUpdateAuthentication(
+ messageData.featureDevEnabled,
+ messageData.codeTransformEnabled,
+ messageData.docEnabled,
+ messageData.codeScanEnabled,
+ messageData.codeTestEnabled,
+ messageData.authenticatingTabIDs
+ )
return
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts
index f55f1b66d5..48c5107b54 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts
@@ -48,7 +48,21 @@ type MessageCommand =
| 'codetransform-pom-file-open-click'
| 'file-click'
| 'open-settings'
+ | 'button-click'
| 'store-code-result-message-id'
+ | 'folderConfirmationMessage'
+ | 'scan'
+ | 'codescan_start_project_scan'
+ | 'codescan_start_file_scan'
+ | 'codescan_stop_project_scan'
+ | 'codescan_stop_file_scan'
+ | 'codescan_open_issues'
+ | 'generate-test'
+ | 'start-test-gen'
+ | 'open-user-guide'
+ | 'send-telemetry'
+ | 'doc_stop_generate'
+ | 'updatePromptProgress'
export type ExtensionMessage = Record & { command: MessageCommand }
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts
index e51291b0c8..2f263812f0 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts
@@ -3,17 +3,28 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { ChatItem, ChatItemAction, FeedbackPayload, Engagement, NotificationType } from '@aws/mynah-ui-chat'
+import {
+ ChatItem,
+ ChatItemAction,
+ FeedbackPayload,
+ Engagement,
+ NotificationType,
+ ProgressField,
+ ChatPrompt,
+} from '@aws/mynah-ui-chat'
import { Connector as CWChatConnector } from './apps/cwChatConnector'
import { Connector as FeatureDevChatConnector } from './apps/featureDevChatConnector'
+import { Connector as DocChatConnector } from './apps/docChatConnector'
import { Connector as AmazonQCommonsConnector } from './apps/amazonqCommonsConnector'
import { ExtensionMessage } from './commands'
import { TabType, TabsStorage } from './storages/tabsStorage'
import { WelcomeFollowupType } from './apps/amazonqCommonsConnector'
import { AuthFollowUpType } from './followUps/generator'
import { CodeTransformChatConnector } from './apps/codeTransformChatConnector'
-import { isFormButtonCodeTransform } from './forms/constants'
+import { isFormButtonCodeTest, isFormButtonCodeScan, isFormButtonCodeTransform } from './forms/constants'
import { DiffTreeFileInfo } from './diffTree/types'
+import { CodeScanChatConnector } from "./apps/codeScanChatConnector";
+import { CodeTestChatConnector } from './apps/codeTestChatConnector'
export interface CodeReference {
licenseName?: string
@@ -39,6 +50,7 @@ export interface ConnectorProps {
sendMessageToExtension: (message: ExtensionMessage) => void
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
+ onChatAnswerUpdated?: (tabID: string, message:ChatItem) => void
onCodeTransformChatDisabled: (tabID: string) => void
onCodeTransformMessageReceived: (
tabID: string,
@@ -47,6 +59,7 @@ export interface ConnectorProps {
clearPreviousItemButtons?: boolean
) => void
onCodeTransformMessageUpdate: (tabID: string, messageId: string, chatItem: Partial) => void
+ onRunTestMessageReceived?: (tabID: string, showRunTestMessage: boolean) => void
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => void
onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined) => void
onCWCContextCommandMessage: (message: ChatItem, command?: string) => string | undefined
@@ -58,14 +71,17 @@ export interface ConnectorProps {
tabID: string,
filePaths: DiffTreeFileInfo[],
deletedFiles: DiffTreeFileInfo[],
- messageId: string,
- disableFileActions: boolean
+ messageId: string
) => void
onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
+ onUpdatePromptProgress: (tabID: string, progressField: ProgressField | null | undefined) => void
onChatInputEnabled: (tabID: string, enabled: boolean) => void
onUpdateAuthentication: (
featureDevEnabled: boolean,
codeTransformEnabled: boolean,
+ docEnabled: boolean,
+ codeScanEnabled: boolean,
+ codeTestEnabled: boolean,
authenticatingTabIDs: string[]
) => void
onNewTab: (tabType: TabType) => void
@@ -73,7 +89,9 @@ export interface ConnectorProps {
onCodeTransformCommandMessageReceived: (message: ChatItem, command?: string) => void
onNotification: (props: { content: string; title?: string; type: NotificationType }) => void
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void
+ handleCommand: (chatPrompt: ChatPrompt, tabId: string) => void
tabsStorage: TabsStorage
+ onCodeScanMessageReceived: (tabID: string, message: ChatItem, isLoading: boolean, clearPreviousItemButtons?: boolean) => void
}
export class Connector {
@@ -82,6 +100,9 @@ export class Connector {
private readonly cwChatConnector
private readonly featureDevChatConnector
private readonly codeTransformChatConnector: CodeTransformChatConnector
+ private readonly docChatConnector
+ private readonly codeScanChatConnector: CodeScanChatConnector
+ private readonly codeTestChatConnector: CodeTestChatConnector
private readonly tabsStorage
private readonly amazonqCommonsConnector: AmazonQCommonsConnector
@@ -93,9 +114,13 @@ export class Connector {
this.cwChatConnector = new CWChatConnector(props as ConnectorProps)
this.featureDevChatConnector = new FeatureDevChatConnector(props)
this.codeTransformChatConnector = new CodeTransformChatConnector(props)
+ this.docChatConnector = new DocChatConnector(props)
+ this.codeScanChatConnector = new CodeScanChatConnector(props)
+ this.codeTestChatConnector = new CodeTestChatConnector(props)
this.amazonqCommonsConnector = new AmazonQCommonsConnector({
sendMessageToExtension: this.sendMessageToExtension,
onWelcomeFollowUpClicked: props.onWelcomeFollowUpClicked,
+ handleCommand: props.handleCommand,
})
this.tabsStorage = props.tabsStorage
}
@@ -119,6 +144,15 @@ export class Connector {
case 'codetransform':
this.codeTransformChatConnector.onResponseBodyLinkClick(tabID, messageId, link)
break
+ case 'codescan':
+ this.codeScanChatConnector.onResponseBodyLinkClick(tabID, messageId, link)
+ break
+ case 'codetest':
+ this.codeTestChatConnector.onResponseBodyLinkClick(tabID, messageId, link)
+ break
+ case 'doc':
+ this.docChatConnector.onResponseBodyLinkClick(tabID, messageId, link)
+ break
}
}
@@ -134,6 +168,8 @@ export class Connector {
switch (this.tabsStorage.getTab(tabID)?.type) {
case 'codetransform':
return this.codeTransformChatConnector.requestAnswer(tabID, payload)
+ case 'codetest':
+ return this.codeTestChatConnector.requestAnswer(tabID, payload)
}
}
@@ -144,6 +180,9 @@ export class Connector {
case 'featuredev':
this.featureDevChatConnector.requestGenerativeAIAnswer(tabID, payload)
break
+ case 'doc':
+ this.docChatConnector.requestGenerativeAIAnswer(tabID, payload)
+ break
default:
this.cwChatConnector.requestGenerativeAIAnswer(tabID, payload)
break
@@ -156,11 +195,18 @@ export class Connector {
}
})
+ //TODO: Create a common connector to share this options across the features
clearChat = (tabID: string): void => {
switch (this.tabsStorage.getTab(tabID)?.type) {
case 'cwc':
this.cwChatConnector.clearChat(tabID)
break
+ case 'codetest':
+ this.codeTestChatConnector.clearChat(tabID)
+ break
+ case 'codescan':
+ this.codeScanChatConnector.clearChat(tabID)
+ break
}
}
@@ -169,6 +215,12 @@ export class Connector {
case 'cwc':
this.cwChatConnector.help(tabID)
break
+ case 'codetest':
+ this.codeTestChatConnector.help(tabID)
+ break
+ case 'codescan':
+ this.codeScanChatConnector.help(tabID)
+ break
}
}
@@ -180,6 +232,18 @@ export class Connector {
}
}
+ scan = (tabID: string): void => {
+ switch (this.tabsStorage.getTab(tabID)?.type) {
+ default:
+ this.codeScanChatConnector.scan(tabID)
+ break
+ }
+ }
+
+ startTestGen = (tabID: string, prompt: string): void => {
+ this.codeTestChatConnector.startTestGen(tabID, prompt)
+ }
+
handleMessageReceive = async (message: MessageEvent): Promise => {
if (message.data === undefined) {
return
@@ -192,12 +256,27 @@ export class Connector {
return
}
- if (messageData.sender === 'CWChat') {
- void this.cwChatConnector.handleMessageReceive(messageData)
- } else if (messageData.sender === 'featureDevChat') {
- void this.featureDevChatConnector.handleMessageReceive(messageData)
- } else if (messageData.sender === 'codetransform') {
- void this.codeTransformChatConnector.handleMessageReceive(messageData)
+ switch (messageData.sender) {
+ case 'CWChat':
+ void this.cwChatConnector.handleMessageReceive(messageData)
+ break
+ case 'featureDevChat':
+ void this.featureDevChatConnector.handleMessageReceive(messageData)
+ break
+ case 'codetransform':
+ void this.codeTransformChatConnector.handleMessageReceive(messageData)
+ break
+ case 'docChat':
+ void this.docChatConnector.handleMessageReceive(messageData)
+ break
+ case 'codescan':
+ void this.codeScanChatConnector.handleMessageReceive(messageData)
+ break
+ case 'codetest':
+ void this.codeTestChatConnector.handleMessageReceive(messageData)
+ break
+ default:
+ break
}
}
@@ -227,6 +306,15 @@ export class Connector {
case 'codetransform':
this.codeTransformChatConnector.onTabOpen(tabID)
break
+ case 'doc':
+ this.docChatConnector.onTabOpen(tabID)
+ break
+ case 'codescan':
+ this.codeScanChatConnector.onTabOpen(tabID)
+ break
+ case 'codetest':
+ this.codeTestChatConnector.onTabOpen(tabID)
+ break
}
}
@@ -265,6 +353,8 @@ export class Connector {
case 'featuredev':
this.featureDevChatConnector.onCodeInsertToCursorPosition(tabID, code, type, codeReference)
break
+ case 'codetest':
+ this.codeTestChatConnector.onCodeInsertToCursorPosition(tabID, code, type, codeReference)
}
}
@@ -314,6 +404,15 @@ export class Connector {
case 'codetransform':
this.codeTransformChatConnector.onTabRemove(tabID)
break
+ case 'doc':
+ this.docChatConnector.onTabRemove(tabID)
+ break
+ case 'codescan':
+ this.codeScanChatConnector.onTabRemove(tabID)
+ break
+ case 'codetest':
+ this.codeTestChatConnector.onTabRemove(tabID)
+ break
}
}
@@ -363,8 +462,11 @@ export class Connector {
switch (tabType) {
case 'codetransform':
case 'cwc':
+ case 'doc':
case 'featuredev':
+ case 'codetest':
this.amazonqCommonsConnector.authFollowUpClicked(tabID, tabType, authType)
+ break
}
}
@@ -382,6 +484,12 @@ export class Connector {
case 'codetransform':
this.codeTransformChatConnector.followUpClicked(tabID, followUp)
break
+ case 'doc':
+ this.docChatConnector.followUpClicked(tabID, followUp)
+ break
+ case 'codetest':
+ this.codeTestChatConnector.followUpClicked(tabID, followUp)
+ break
default:
this.cwChatConnector.followUpClicked(tabID, messageId, followUp)
break
@@ -393,6 +501,20 @@ export class Connector {
case 'featuredev':
this.featureDevChatConnector.onFileActionClick(tabID, messageId, filePath, actionName)
break
+ case 'doc':
+ this.docChatConnector.onFileActionClick(tabID, messageId, filePath, actionName)
+ break
+ }
+ }
+
+ onFileClick = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => {
+ switch (this.tabsStorage.getTab(tabID)?.type) {
+ case 'featuredev':
+ this.featureDevChatConnector.onOpenDiff(tabID, filePath, deleted)
+ break
+ case 'codetest':
+ this.codeTestChatConnector.onFileClick(tabID, filePath, deleted, messageId)
+ break
}
}
@@ -401,6 +523,32 @@ export class Connector {
case 'featuredev':
this.featureDevChatConnector.onOpenDiff(tabID, filePath, deleted)
break
+ case 'doc':
+ this.docChatConnector.onOpenDiff(tabID, filePath, deleted)
+ break
+ }
+ }
+
+ onCustomFormAction = (
+ tabId: string,
+ messageId: string | undefined,
+ action: any,
+ eventId: string | undefined = undefined
+ ): void | undefined => {
+ switch (this.tabsStorage.getTab(tabId)?.type) {
+ case 'codescan':
+ this.codeScanChatConnector.onFormButtonClick(tabId, action)
+ break
+ case 'codetest':
+ this.codeTestChatConnector.onFormButtonClick(tabId, messageId ?? '', action)
+ break
+ case 'doc':
+ this.docChatConnector.onFormButtonClick(tabId, action)
+ break
+ case 'agentWalkthrough': {
+ this.amazonqCommonsConnector.onCustomFormAction(tabId, action)
+ break
+ }
}
}
@@ -448,6 +596,12 @@ export class Connector {
) => {
if (isFormButtonCodeTransform(action.id)) {
this.codeTransformChatConnector.onFormButtonClick(tabId, action)
+ } else if (isFormButtonCodeScan(action.id)) {
+ this.codeScanChatConnector.onFormButtonClick(tabId, action)
+ } else if (isFormButtonCodeTest(action.id)) {
+ this.codeTestChatConnector.onFormButtonClick(tabId, messageId, action)
+ } else if (action.id === 'doc') {
+ this.docChatConnector.onFormButtonClick(tabId, action)
}
switch (this.tabsStorage.getTab(tabId)?.type) {
case 'cwc':
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/generator.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/generator.ts
index 286c6ec98f..6505e02391 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/generator.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/generator.ts
@@ -52,11 +52,37 @@ export class FollowUpGenerator {
},
],
}
+ case 'doc':
+ return {
+ text: 'Select one of the following...',
+ options: [
+ {
+ pillText: 'Create a README',
+ prompt: 'Create a README',
+ type: 'CreateDocumentation',
+ },
+ {
+ pillText: 'Update an existing README',
+ prompt: 'Update an existing README',
+ type: 'UpdateDocumentation',
+ },
+ ],
+ }
case 'codetransform':
return {
text: '',
options: [],
}
+ case 'codescan':
+ return {
+ text: '',
+ options: []
+ }
+ case 'codetest':
+ return {
+ text: '',
+ options: [],
+ }
default:
return {
text: 'Try Examples:',
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts
index 8234672976..55e28eaacc 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/followUps/handler.ts
@@ -73,6 +73,7 @@ export class FollowUpInteractionHandler {
return
}
}
+
this.connector.onFollowUpClicked(tabID, messageId, followUp)
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts
index 2ad5730375..f727647729 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts
@@ -19,6 +19,23 @@ export const enum FormButtonIds {
ConfirmHilSelection = 'confirm_hil_selection',
RejectHilSelection = 'reject_hil_selection',
OpenDependencyErrorPom = "open_dependency_error_pom",
+ CodeScanStartProjectScan = "codescan_start_project_scan",
+ CodeScanStartFileScan = "codescan_start_file_scan",
+ CodeScanStopProjectScan = "codescan_stop_project_scan",
+ CodeScanStopFileScan = "codescan_stop_file_scan",
+ CodeScanOpenIssues = "codescan_open_issues",
+ CodeTestStartGeneration = "code_test_start_generation",
+ CodeTestViewDiff = "utg_view_diff",
+ CodeTestAccept = "utg_accept",
+ CodeTestRegenerate = "utg_regenerate",
+ CodeTestReject = "utg_reject",
+ CodeTestBuildAndExecute = "utg_build_and_execute",
+ CodeTestModifyCommand = "utg_modify_command",
+ CodeTestSkipAndFinish = "utg_skip_and_finish",
+ CodeTestInstallAndContinue = "utg_install_and_continue",
+ CodeTestRejectAndRevert = "utg_reject_and_revert",
+ CodeTestProceed = "utg_proceed",
+
}
export const isFormButtonCodeTransform = (id: string): boolean => {
@@ -40,3 +57,19 @@ export const isFormButtonCodeTransform = (id: string): boolean => {
id === FormButtonIds.OpenDependencyErrorPom
)
}
+
+export const isFormButtonCodeTest = (id: string): boolean => {
+ return (
+ id === FormButtonIds.CodeTestStartGeneration || id.startsWith("utg")
+ )
+}
+
+export const isFormButtonCodeScan = (id: string): boolean => {
+ return (
+ id === FormButtonIds.CodeScanStartProjectScan ||
+ id === FormButtonIds.CodeScanStartFileScan ||
+ id === FormButtonIds.CodeScanStopProjectScan ||
+ id === FormButtonIds.CodeScanStopFileScan ||
+ id === FormButtonIds.CodeScanOpenIssues
+ )
+}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts
index f7d1cae719..dbe0dfb2b7 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts
@@ -3,7 +3,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { Connector, CWCChatItem } from './connector'
-import {ChatItem, ChatItemType, MynahIcons, MynahUI, MynahUIDataModel, NotificationType, ReferenceTrackerInformation} from '@aws/mynah-ui-chat'
+import {
+ ChatItem,
+ ChatItemType,
+ MynahIcons,
+ MynahUI,
+ MynahUIDataModel,
+ NotificationType,
+ ProgressField,
+ ReferenceTrackerInformation
+} from '@aws/mynah-ui-chat'
import './styles/dark.scss'
import { TabsStorage, TabType } from './storages/tabsStorage'
import { WelcomeFollowupType } from './apps/amazonqCommonsConnector'
@@ -17,9 +26,19 @@ import { MessageController } from './messages/controller'
import { getActions, getDetails } from './diffTree/actions'
import { DiffTreeFileInfo } from './diffTree/types'
import './styles.css'
-import {CodeSelectionType} from "@aws/mynah-ui-chat/dist/static";
+import { ChatPrompt, CodeSelectionType} from "@aws/mynah-ui-chat/dist/static";
+import {welcomeScreenTabData} from "./walkthrough/welcome";
+import { agentWalkthroughDataModel } from './walkthrough/agent'
+import {createClickTelemetry, createOpenAgentTelemetry} from "./telemetry/actions";
-export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeTransformInitEnabled: boolean) => {
+export const createMynahUI = (
+ ideApi: any,
+ featureDevInitEnabled: boolean,
+ codeTransformInitEnabled: boolean,
+ docInitEnabled: boolean,
+ codeScanEnabled: boolean,
+ codeTestEnabled: boolean
+) => {
// eslint-disable-next-line prefer-const
let mynahUI: MynahUI
// eslint-disable-next-line prefer-const
@@ -51,9 +70,18 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
let isCodeTransformEnabled = codeTransformInitEnabled
+ let isDocEnabled = docInitEnabled
+
+ let isCodeScanEnabled = codeScanEnabled
+
+ let isCodeTestEnabled = codeTestEnabled
+
const tabDataGenerator = new TabDataGenerator({
isFeatureDevEnabled,
isCodeTransformEnabled,
+ isDocEnabled,
+ isCodeScanEnabled,
+ isCodeTestEnabled,
})
// eslint-disable-next-line prefer-const
@@ -68,18 +96,36 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
// eslint-disable-next-line prefer-const
connector = new Connector({
tabsStorage,
+ /**
+ * Proxy for allowing underlying common connectors to call quick action handlers
+ */
+ handleCommand: (chatPrompt: ChatPrompt, tabId: string) => {
+ quickActionHandler.handleCommand(chatPrompt, tabId)
+ },
onUpdateAuthentication: (
featureDevEnabled: boolean,
codeTransformEnabled: boolean,
+ docEnabled: boolean,
+ codeScanEnabled: boolean,
+ codeTestEnabled: boolean,
authenticatingTabIDs: string[]
): void => {
isFeatureDevEnabled = featureDevEnabled
isCodeTransformEnabled = codeTransformEnabled
+ isDocEnabled = docEnabled
+ isCodeScanEnabled = codeScanEnabled
+ isCodeTestEnabled = codeTestEnabled
quickActionHandler.isFeatureDevEnabled = isFeatureDevEnabled
quickActionHandler.isCodeTransformEnabled = isCodeTransformEnabled
+ quickActionHandler.isDocEnabled = isDocEnabled
+ quickActionHandler.isCodeTestEnabled = isCodeTestEnabled
+ quickActionHandler.isCodeScanEnabled = isCodeScanEnabled
tabDataGenerator.quickActionsGenerator.isFeatureDevEnabled = isFeatureDevEnabled
tabDataGenerator.quickActionsGenerator.isCodeTransformEnabled = isCodeTransformEnabled
+ tabDataGenerator.quickActionsGenerator.isDocEnabled = isDocEnabled
+ tabDataGenerator.quickActionsGenerator.isCodeScanEnabled = isCodeScanEnabled
+ tabDataGenerator.quickActionsGenerator.isCodeTestEnabled = isCodeTestEnabled
// Set the new defaults for the quick action commands in all tabs now that isFeatureDevEnabled and isCodeTransformEnabled were enabled/disabled
for (const tab of tabsStorage.getTabs()) {
@@ -93,7 +139,10 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
const tabType = tabsStorage.getTab(tabID)?.type
if (
(tabType === 'featuredev' && featureDevEnabled) ||
- (tabType === 'codetransform' && codeTransformEnabled)
+ (tabType === 'codetransform' && codeTransformEnabled) ||
+ (tabType === 'doc' && docEnabled) ||
+ (tabType === 'codetransform' && codeTransformEnabled) ||
+ (tabType === 'codetest' && codeTestEnabled)
) {
mynahUI.addChatItem(tabID, {
type: ChatItemType.ANSWER,
@@ -114,7 +163,12 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
if (command === 'aws.amazonq.sendToPrompt') {
return messageController.sendSelectedCodeToTab(message)
} else {
- return messageController.sendMessageToTab(message, 'cwc')
+ const tabID = messageController.sendMessageToTab(message, 'cwc')
+ if (tabID && command) {
+ ideApi.postMessage(createOpenAgentTelemetry('cwc', 'right-click'))
+ }
+
+ return tabID
}
},
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => {
@@ -235,7 +289,7 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
mynahUI.selectTab(codeTransformTab.id, eventId)
} else {
// Click to open a new code transform tab
- quickActionHandler.handle({ command: '/transform' }, '', eventId)
+ quickActionHandler.handleCommand({ command: '/transform' }, '', eventId)
}
},
})
@@ -244,6 +298,27 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
sendMessageToExtension: message => {
ideApi.postMessage(message)
},
+ onChatAnswerUpdated: (tabID: string, item) => {
+ if (item.messageId !== undefined) {
+ mynahUI.updateChatAnswerWithMessageId(tabID, item.messageId, {
+ ...(item.body !== undefined ? { body: item.body } : {}),
+ ...(item.buttons !== undefined ? { buttons: item.buttons } : {}),
+ ...(item.fileList !== undefined ? { fileList: item.fileList } : {}),
+ ...(item.footer !== undefined ? { footer: item.footer } : {}),
+ ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}),
+ ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}),
+ })
+ } else {
+ mynahUI.updateLastChatAnswer(tabID, {
+ ...(item.body !== undefined ? { body: item.body } : {}),
+ ...(item.buttons !== undefined ? { buttons: item.buttons } : {}),
+ ...(item.fileList !== undefined ? { fileList: item.fileList } : {}),
+ ...(item.footer !== undefined ? { footer: item.footer } : {}),
+ ...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}),
+ ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}),
+ } as ChatItem)
+ }
+ },
onChatAnswerReceived: (tabID: string, item: CWCChatItem) => {
if (item.type === ChatItemType.ANSWER_PART || item.type === ChatItemType.CODE_RESULT) {
mynahUI.updateLastChatAnswer(tabID, {
@@ -255,6 +330,7 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
...(item.type === ChatItemType.CODE_RESULT
? { type: ChatItemType.CODE_RESULT, fileList: item.fileList }
: {}),
+ ...(item.codeReference !== undefined ? { codeReference: item.codeReference } : {}),
})
if (item.messageId !== undefined && item.userIntent !== undefined && item.codeBlockLanguage !== undefined) {
responseMetadata.set(item.messageId, [item.userIntent, item.codeBlockLanguage])
@@ -289,6 +365,11 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
tabsStorage.updateTabStatus(tabID, 'free')
}
},
+ onRunTestMessageReceived: (tabID: string, shouldRunTestMessage: boolean) => {
+ if (shouldRunTestMessage) {
+ quickActionHandler.handleCommand({ command: '/test' }, tabID)
+ }
+ },
onMessageReceived: (tabID: string, messageData: MynahUIDataModel) => {
mynahUI.updateStore(tabID, messageData)
},
@@ -369,6 +450,12 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
promptInputPlaceholder: newPlaceholder,
})
},
+ onUpdatePromptProgress(tabID: string, progressField: ProgressField | null | undefined) {
+ mynahUI.updateStore(tabID, {
+ // eslint-disable-next-line no-null/no-null
+ promptInputProgress: progressField ? progressField : null,
+ })
+ },
onNewTab(tabType: TabType) {
const newTabID = mynahUI.updateStore('', {})
if (!newTabID) {
@@ -406,12 +493,65 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
})
return
},
+ onCodeScanMessageReceived(tabID: string, chatItem: ChatItem, isLoading: boolean, clearPreviousItemButtons?: boolean, runReview?: boolean) {
+ if (runReview) {
+ quickActionHandler.handleCommand({ command: "/review" }, "")
+ return
+ }
+ if (chatItem.type === ChatItemType.ANSWER_PART) {
+ mynahUI.updateLastChatAnswer(tabID, {
+ ...(chatItem.messageId !== undefined ? { messageId: chatItem.messageId } : {}),
+ ...(chatItem.canBeVoted !== undefined ? { canBeVoted: chatItem.canBeVoted } : {}),
+ ...(chatItem.codeReference !== undefined ? { codeReference: chatItem.codeReference } : {}),
+ ...(chatItem.body !== undefined ? { body: chatItem.body } : {}),
+ ...(chatItem.relatedContent !== undefined ? { relatedContent: chatItem.relatedContent } : {}),
+ ...(chatItem.formItems !== undefined ? { formItems: chatItem.formItems } : {}),
+ ...(chatItem.buttons !== undefined ? { buttons: chatItem.buttons } : { buttons: [] }),
+ // For loading animation to work, do not update the chat item type
+ ...(chatItem.followUp !== undefined ? { followUp: chatItem.followUp } : {}),
+ })
+
+ if (!isLoading) {
+ mynahUI.updateStore(tabID, {
+ loadingChat: false,
+ })
+ } else {
+ mynahUI.updateStore(tabID, {
+ cancelButtonWhenLoading: false
+ })
+ }
+ }
+
+ if (
+ chatItem.type === ChatItemType.PROMPT ||
+ chatItem.type === ChatItemType.ANSWER_STREAM ||
+ chatItem.type === ChatItemType.ANSWER
+ ) {
+ if (chatItem.followUp === undefined && clearPreviousItemButtons === true) {
+ mynahUI.updateLastChatAnswer(tabID, {
+ buttons: [],
+ followUp: { options: [] },
+ })
+ }
+
+ mynahUI.addChatItem(tabID, chatItem)
+ mynahUI.updateStore(tabID, {
+ loadingChat: chatItem.type !== ChatItemType.ANSWER
+ })
+
+ if (chatItem.type === ChatItemType.PROMPT) {
+ tabsStorage.updateTabStatus(tabID, 'busy')
+ } else if (chatItem.type === ChatItemType.ANSWER) {
+ tabsStorage.updateTabStatus(tabID, 'free')
+ }
+ }
+ }
})
mynahUI = new MynahUI({
onReady: connector.uiReady,
onTabAdd: (tabID: string) => {
- // If featureDev or gumby has changed availability inbetween the default store settings and now
+ // If featureDev or gumby has changed availability in between the default store settings and now
// make sure to show/hide it accordingly
mynahUI.updateStore(tabID, {
quickActionCommands: tabDataGenerator.quickActionsGenerator.generateForTab('unknown'),
@@ -441,10 +581,30 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
chatMessage: prompt.prompt ?? ''
})
return
+ } else if (tabsStorage.getTab(tabID)?.type === 'codetest') {
+ if(prompt.command !== undefined && prompt.command.trim() !== '' && prompt.command !== '/test') {
+ quickActionHandler.handleCommand(prompt, tabID, eventId)
+ return
+ } else {
+ connector.requestAnswer(tabID, {
+ chatMessage: prompt.prompt ?? ''
+ })
+ return
+ }
+ } else if (tabsStorage.getTab(tabID)?.type === 'codescan') {
+ if(prompt.command !== undefined && prompt.command.trim() !== '') {
+ quickActionHandler.handleCommand(prompt, tabID, eventId)
+ return
+ }
}
if (prompt.command !== undefined && prompt.command.trim() !== '') {
- quickActionHandler.handle(prompt, tabID, eventId)
+ quickActionHandler.handleCommand(prompt, tabID, eventId)
+
+ const newTabType = tabsStorage.getSelectedTab()?.type
+ if (newTabType) {
+ ideApi.postMessage(createOpenAgentTelemetry(newTabType, 'quick-action'))
+ }
return
}
@@ -511,15 +671,54 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => {
connector.onFileActionClick(tabID, messageId, filePath, actionName)
},
- onOpenDiff: connector.onOpenDiff,
+ onFileClick: connector.onFileClick,
+ onChatPromptProgressActionButtonClicked: (tabID, action) => {
+ connector.onCustomFormAction(tabID, undefined, action)
+ },
tabs: {
'tab-1': {
isSelected: true,
- store: tabDataGenerator.getTabData('cwc', true),
+ store: welcomeScreenTabData(tabDataGenerator).store,
},
},
- onInBodyButtonClicked: (tabId, messageId, action) => {
- connector.onFormButtonClick(tabId, messageId, action)
+ onInBodyButtonClicked: (tabId, messageId, action, eventId) => {
+ if (action.id === 'quick-start') {
+ /**
+ * quick start is the action on the welcome page. When its
+ * clicked it collapses the view and puts it into regular
+ * "chat" which is cwc
+ */
+ tabsStorage.updateTabTypeFromUnknown(tabId, 'cwc')
+
+ // show quick start in the current tab instead of a new one
+ mynahUI.updateStore(tabId, {
+ tabHeaderDetails: undefined,
+ compactMode: false,
+ tabBackground: false,
+ promptInputText: '/',
+ promptInputLabel: undefined,
+ chatItems: [],
+ })
+
+ ideApi.postMessage(createClickTelemetry('amazonq-welcome-quick-start-button'))
+ return
+ }
+
+ if (action.id === 'explore') {
+ const newTabId = mynahUI.updateStore('', agentWalkthroughDataModel)
+ if (newTabId === undefined) {
+ mynahUI.notify({
+ content: uiComponentsTexts.noMoreTabsTooltip,
+ type: NotificationType.WARNING,
+ })
+ return
+ }
+ tabsStorage.updateTabTypeFromUnknown(newTabId, 'agentWalkthrough')
+ ideApi.postMessage(createClickTelemetry('amazonq-welcome-explore-button'))
+ return
+ }
+
+ connector.onCustomFormAction(tabId, messageId, action, eventId)
},
defaults: {
store: tabDataGenerator.getTabData('cwc', true),
@@ -542,6 +741,9 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
tabsStorage,
isFeatureDevEnabled,
isCodeTransformEnabled,
+ isDocEnabled,
+ isCodeScanEnabled,
+ isCodeTestEnabled,
})
textMessageHandler = new TextMessageHandler({
mynahUI,
@@ -554,5 +756,8 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT
tabsStorage,
isFeatureDevEnabled,
isCodeTransformEnabled,
+ isDocEnabled,
+ isCodeScanEnabled,
+ isCodeTestEnabled,
})
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts
index 142f4d4093..72635f0775 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/messages/controller.ts
@@ -15,6 +15,9 @@ export interface MessageControllerProps {
tabsStorage: TabsStorage
isFeatureDevEnabled: boolean
isCodeTransformEnabled: boolean
+ isDocEnabled: boolean
+ isCodeScanEnabled: boolean
+ isCodeTestEnabled: boolean
}
export class MessageController {
@@ -30,6 +33,9 @@ export class MessageController {
this.tabDataGenerator = new TabDataGenerator({
isFeatureDevEnabled: props.isFeatureDevEnabled,
isCodeTransformEnabled: props.isCodeTransformEnabled,
+ isDocEnabled: props.isDocEnabled,
+ isCodeScanEnabled: props.isCodeScanEnabled,
+ isCodeTestEnabled: props.isCodeTestEnabled,
})
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/generator.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/generator.ts
index 1f62a45ce0..1033f29a8e 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/generator.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/generator.ts
@@ -5,60 +5,114 @@
import { QuickActionCommand, QuickActionCommandGroup } from '@aws/mynah-ui-chat/dist/static'
import { TabType } from '../storages/tabsStorage'
+import {MynahIcons} from "@aws/mynah-ui-chat";
export interface QuickActionGeneratorProps {
isFeatureDevEnabled: boolean
isCodeTransformEnabled: boolean
+ isDocEnabled: boolean
+ isCodeScanEnabled: boolean
+ isCodeTestEnabled: boolean
}
export class QuickActionGenerator {
public isFeatureDevEnabled: boolean
public isCodeTransformEnabled: boolean
+ public isDocEnabled: boolean
+ public isCodeScanEnabled: boolean
+ public isCodeTestEnabled: boolean
constructor(props: QuickActionGeneratorProps) {
this.isFeatureDevEnabled = props.isFeatureDevEnabled
this.isCodeTransformEnabled = props.isCodeTransformEnabled
+ this.isDocEnabled = props.isDocEnabled
+ this.isCodeScanEnabled = props.isCodeScanEnabled
+ this.isCodeTestEnabled = props.isCodeTestEnabled
}
public generateForTab(tabType: TabType): QuickActionCommandGroup[] {
+ // agentWalkthrough is static and doesn't have any quick actions
+ if (tabType === 'agentWalkthrough') {
+ return []
+ }
+
const quickActionCommands = [
{
+ groupName: `Q Developer Agent for Software Development `,
commands: [
...(this.isFeatureDevEnabled
? [
{
command: '/dev',
+ icon: MynahIcons.CODE_BLOCK,
placeholder: 'Describe your task or issue in as much detail as possible',
description: 'Generate code to make a change in your project',
},
]
: []),
+ ...(this.isDocEnabled
+ ? [
+ {
+ command: '/doc',
+ icon: MynahIcons.FILE,
+ description: 'Generate documentation for your code',
+ },
+ ]
+ : []),
+ ...(this.isCodeScanEnabled
+ ? [
+ {
+ command: '/review',
+ icon: MynahIcons.BUG,
+ description: 'Identify and fix code issues before committing'
+ }
+ ]
+ : []),
+ ...(this.isCodeTestEnabled
+ ? [
+ {
+ command: '/test',
+ icon: MynahIcons.CHECK_LIST,
+ placeholder: 'Specify a function(s) in the current file(optional)',
+ description: 'Generate unit tests',
+ },
+ ]
+ : []),
+ ],
+ },
+ {
+ groupName: `Q Developer Agent for Code Transformation `,
+ commands:[
...(this.isCodeTransformEnabled
? [
- {
- command: '/transform',
- description: 'Transform your Java project',
- },
- ]
+ {
+ command: '/transform',
+ icon: MynahIcons.TRANSFORM,
+ description: 'Transform your Java project',
+ },
+ ]
: []),
],
},
{
+ groupName: 'Quick Actions',
commands: [
{
command: '/help',
+ icon: MynahIcons.HELP,
description: 'Learn more about Amazon Q',
},
{
command: '/clear',
+ icon: MynahIcons.TRASH,
description: 'Clear this session',
},
],
},
- ]
+ ].filter((section) => section.commands.length > 0)
const commandUnavailability: Record<
- TabType,
+ Exclude,
{
description: string
unavailableItems: string[]
@@ -70,11 +124,27 @@ export class QuickActionGenerator {
},
featuredev: {
description: "This command isn't available in /dev",
- unavailableItems: ['/dev', '/transform', '/help', '/clear'],
+ unavailableItems: ['/dev', '/transform', '/doc', '/help', '/clear', '/review', '/test'],
},
codetransform: {
description: "This command isn't available in /transform",
- unavailableItems: ['/dev', '/transform'],
+ unavailableItems: ['/help', '/clear'],
+ },
+ codescan: {
+ description: "This command isn't available in /review",
+ unavailableItems: ['/help', '/clear'],
+ },
+ codetest: {
+ description: "This command isn't available in /test",
+ unavailableItems: ['/help', '/clear'],
+ },
+ doc: {
+ description: "This command isn't available in /doc",
+ unavailableItems: ['/help', '/clear'],
+ },
+ welcome: {
+ description: '',
+ unavailableItems: ['/clear'],
},
unknown: {
description: '',
@@ -84,6 +154,7 @@ export class QuickActionGenerator {
return quickActionCommands.map((commandGroup: QuickActionCommandGroup) => {
return {
+ groupName: commandGroup.groupName,
commands: commandGroup.commands.map((commandItem: QuickActionCommand) => {
const commandNotAvailable = commandUnavailability[tabType].unavailableItems.includes(
commandItem.command
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts
index 426dc0a325..e9de0369f2 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/quickActions/handler.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { ChatItemType, ChatPrompt, MynahUI, NotificationType } from '@aws/mynah-ui-chat'
+import { ChatItemType, ChatPrompt, MynahIcons, MynahUI, NotificationType } from '@aws/mynah-ui-chat'
import { TabDataGenerator } from '../tabs/generator'
import { Connector } from '../connector'
import { Tab, TabsStorage } from '../storages/tabsStorage'
@@ -15,6 +15,9 @@ export interface QuickActionsHandlerProps {
tabsStorage: TabsStorage
isFeatureDevEnabled: boolean
isCodeTransformEnabled: boolean
+ isDocEnabled: boolean
+ isCodeScanEnabled: boolean
+ isCodeTestEnabled: boolean
}
export class QuickActionHandler {
@@ -24,6 +27,9 @@ export class QuickActionHandler {
private tabDataGenerator: TabDataGenerator
public isFeatureDevEnabled: boolean
public isCodeTransformEnabled: boolean
+ public isDocEnabled: boolean
+ public isCodeScanEnabled: boolean
+ public isCodeTestEnabled: boolean
constructor(props: QuickActionsHandlerProps) {
this.mynahUI = props.mynahUI
@@ -32,12 +38,19 @@ export class QuickActionHandler {
this.tabDataGenerator = new TabDataGenerator({
isFeatureDevEnabled: props.isFeatureDevEnabled,
isCodeTransformEnabled: props.isCodeTransformEnabled,
+ isDocEnabled: props.isDocEnabled,
+ isCodeScanEnabled: props.isCodeScanEnabled,
+ isCodeTestEnabled: props.isCodeTestEnabled,
})
this.isFeatureDevEnabled = props.isFeatureDevEnabled
this.isCodeTransformEnabled = props.isCodeTransformEnabled
+ this.isDocEnabled = props.isDocEnabled
+ this.isCodeScanEnabled = props.isCodeScanEnabled
+ this.isCodeTestEnabled = props.isCodeTestEnabled
}
- public handle(chatPrompt: ChatPrompt, tabID: string, eventId?: string) {
+ // Entry point for `/xxx` commands
+ public handleCommand(chatPrompt: ChatPrompt, tabID: string, eventId?: string) {
this.tabsStorage.resetTabTimer(tabID)
switch (chatPrompt.command) {
case '/dev':
@@ -49,6 +62,15 @@ export class QuickActionHandler {
case '/transform':
this.handleCodeTransformCommand(tabID, eventId)
break
+ case '/doc':
+ this.handleDocCommand(chatPrompt, tabID, 'Q - Doc')
+ break
+ case '/review':
+ this.handleCodeScanCommand(tabID, eventId)
+ break
+ case '/test':
+ this.handleCodeTestCommand(chatPrompt, tabID, eventId)
+ break
case '/clear':
this.handleClearCommand(tabID)
break
@@ -143,15 +165,38 @@ export class QuickActionHandler {
this.mynahUI.updateStore(affectedTabId, { chatItems: [] })
this.mynahUI.updateStore(
affectedTabId,
- this.tabDataGenerator.getTabData('featuredev', realPromptText === '', taskName)
+ this.tabDataGenerator.getTabData('featuredev', false, taskName)
)
+ const addInformationCard = (tabId: string) => {
+ this.mynahUI.addChatItem(tabId, {
+ type: ChatItemType.ANSWER,
+ informationCard: {
+ title: "Feature development",
+ description: "Amazon Q Developer Agent for Software Development",
+ icon: MynahIcons.BUG,
+ content: {
+ body: [
+ "I can generate code to accomplish a task or resolve an issue.",
+ "After you provide a task, I will:",
+ "1. Generate code based on your description and the code in your workspace",
+ "2. Provide a list of suggestions for you to review and add to your workspace",
+ "3. If needed, iterate based on your feedback",
+ "To learn more, visit the [user guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html).",
+ ].join("\n")
+ },
+ },
+ })
+ };
+
if (realPromptText !== '') {
this.mynahUI.addChatItem(affectedTabId, {
type: ChatItemType.PROMPT,
body: realPromptText,
})
+ addInformationCard(affectedTabId)
+
this.mynahUI.addChatItem(affectedTabId, {
type: ChatItemType.ANSWER_STREAM,
body: '',
@@ -165,7 +210,174 @@ export class QuickActionHandler {
this.connector.requestGenerativeAIAnswer(affectedTabId, {
chatMessage: realPromptText,
})
+ } else {
+ addInformationCard(affectedTabId)
+ }
+ }
+ }
+
+private handleDocCommand(chatPrompt: ChatPrompt, tabID: string, taskName: string) {
+ if (!this.isDocEnabled) {
+ return
+ }
+
+ let affectedTabId: string | undefined = tabID
+ const realPromptText = chatPrompt.escapedPrompt?.trim() ?? ''
+
+ if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') {
+ affectedTabId = this.mynahUI.updateStore('', {})
+ }
+
+ if (affectedTabId === undefined) {
+ this.mynahUI.notify({
+ content: uiComponentsTexts.noMoreTabsTooltip,
+ type: NotificationType.WARNING,
+ })
+ return
+ } else {
+ this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'doc')
+ this.connector.onKnownTabOpen(affectedTabId)
+ this.connector.onUpdateTabType(affectedTabId)
+
+ this.mynahUI.updateStore(affectedTabId, { chatItems: [] })
+
+ this.mynahUI.updateStore(
+ affectedTabId, {
+ ...this.tabDataGenerator.getTabData('doc', realPromptText === '', taskName),
+ promptInputDisabledState: true
+ }
+ )
+
+ if (realPromptText !== '') {
+ this.mynahUI.addChatItem(affectedTabId, {
+ type: ChatItemType.PROMPT,
+ body: realPromptText,
+ })
+
+ this.mynahUI.updateStore(affectedTabId, {
+ loadingChat: true,
+ promptInputDisabledState: true,
+ })
+
+ void this.connector.requestGenerativeAIAnswer(affectedTabId, {
+ chatMessage: realPromptText,
+ })
}
}
}
+
+ private showScanInTab( tabId: string) {
+ this.mynahUI.addChatItem(tabId, {
+ type: ChatItemType.PROMPT,
+ body: "Run a code review",
+ })
+ this.mynahUI.addChatItem(tabId, {
+ type: ChatItemType.ANSWER,
+ informationCard: {
+ title: "/review",
+ description: "Included in your Q Developer subscription",
+ icon: MynahIcons.BUG,
+ content: {
+ body: "Automated code review allowing developers to identify and resolve code quality issues, " +
+ "security vulnerabilities, misconfigurations, and deviations from coding best practices.\n\n" +
+ "For this workflow, Q will:\n1. Review the project or a particular file you select and identify issues before code commit\n" +
+ "2. Provide a list of findings from where you can follow up with Q to find solutions\n3. Generate on-demand code fixes inline\n\n" +
+ "To learn more, check out our [user guide](https://aws.amazon.com/q/developer/)."
+ },
+ },
+ })
+ this.connector.scan(tabId)
+ }
+
+ private handleCodeScanCommand(tabID: string, eventId?: string) {
+ if (!this.isCodeScanEnabled) {
+ return
+ }
+
+ // Check for existing opened code scan tab
+ const existingCodeScanTab = this.tabsStorage.getTabs().find(tab => tab.type === 'codescan')
+ if (existingCodeScanTab !== undefined ) {
+ this.mynahUI.selectTab(existingCodeScanTab.id, eventId || "")
+ this.connector.onTabChange(existingCodeScanTab.id)
+
+ this.mynahUI.notify({
+ title: "Q - Review",
+ content: "Switched to the opened code review tab"
+ });
+ this.showScanInTab(existingCodeScanTab.id)
+ return
+ }
+
+ // Add new tab
+ let affectedTabId: string | undefined = tabID
+ if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') {
+ affectedTabId = this.mynahUI.updateStore('', {})
+ }
+ if (affectedTabId === undefined) {
+ this.mynahUI.notify({
+ content: uiComponentsTexts.noMoreTabsTooltip,
+ type: NotificationType.WARNING
+ })
+ return
+ } else {
+ this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'codescan')
+ this.connector.onKnownTabOpen(affectedTabId)
+ // Clear unknown tab type's welcome message
+ this.mynahUI.updateStore(affectedTabId, {chatItems: []})
+ this.mynahUI.updateStore(affectedTabId, this.tabDataGenerator.getTabData('codescan', true))
+ this.mynahUI.updateStore(affectedTabId, {
+ promptInputDisabledState: true,
+ promptInputPlaceholder: 'Waiting on your inputs...',
+ loadingChat: true,
+ })
+
+ this.connector.onTabAdd(affectedTabId)
+ }
+ this.showScanInTab(affectedTabId)
+ }
+
+ private handleCodeTestCommand(chatPrompt: ChatPrompt, tabID: string, eventId: string | undefined) {
+ if (!this.isCodeTestEnabled) {
+ return
+ }
+ const testTabId = this.tabsStorage.getTabs().find((tab) => tab.type === 'codetest')?.id
+ const realPromptText = chatPrompt.escapedPrompt?.trim() ?? ''
+ if (testTabId !== undefined) {
+ this.mynahUI.selectTab(testTabId, eventId || '')
+ this.connector.onTabChange(testTabId)
+ this.connector.startTestGen(testTabId, realPromptText)
+ return
+ }
+ let affectedTabId: string | undefined = tabID
+
+ // if there is no test tab, open a new one
+ if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') {
+ affectedTabId = this.mynahUI.updateStore('', {
+ loadingChat: true,
+ })
+ }
+ if (affectedTabId === undefined) {
+ this.mynahUI.notify({
+ content: uiComponentsTexts.noMoreTabsTooltip,
+ type: NotificationType.WARNING,
+ })
+ return
+ } else {
+ this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'codetest')
+ this.connector.onKnownTabOpen(affectedTabId)
+ this.connector.onUpdateTabType(affectedTabId)
+ // reset chat history
+ this.mynahUI.updateStore(affectedTabId, {
+ chatItems: [],
+ })
+
+ // creating a new tab and printing some title
+ this.mynahUI.updateStore(
+ affectedTabId,
+ this.tabDataGenerator.getTabData('codetest', realPromptText === '', 'Q - Test')
+ )
+
+ this.connector.startTestGen(affectedTabId, realPromptText)
+ }
+ }
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/storages/tabsStorage.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/storages/tabsStorage.ts
index 85b9b1fb6c..bf40b1e530 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/storages/tabsStorage.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/storages/tabsStorage.ts
@@ -4,7 +4,38 @@
*/
export type TabStatus = 'free' | 'busy' | 'dead'
-export type TabType = 'cwc' | 'featuredev' | 'codetransform' | 'unknown'
+const TabTypes = [
+ 'cwc',
+ 'featuredev',
+ 'codetransform',
+ 'doc',
+ 'codescan',
+ 'codetest',
+ 'agentWalkthrough',
+ 'welcome',
+ 'unknown',
+] as const
+export type TabType = (typeof TabTypes)[number]
+export function isTabType(value: string): value is TabType {
+ return (TabTypes as readonly string[]).includes(value)
+}
+
+export function getTabCommandFromTabType(tabType: TabType): string {
+ switch (tabType) {
+ case 'featuredev':
+ return '/dev'
+ case 'codetransform':
+ return '/transform'
+ case 'doc':
+ return '/doc'
+ case 'codescan':
+ return '/review'
+ case 'codetest':
+ return '/test'
+ default:
+ return ''
+ }
+}
export type TabOpenType = 'click' | 'contextMenu' | 'hotkeys'
const TabTimeoutDuration = 172_800_000 // 48hrs
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts
index 42711a25f0..c5fc6f62cb 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts
@@ -12,6 +12,9 @@ import { workspaceCommand } from '../commands'
export interface TabDataGeneratorProps {
isFeatureDevEnabled: boolean
isCodeTransformEnabled: boolean
+ isDocEnabled: boolean
+ isCodeScanEnabled: boolean
+ isCodeTestEnabled: boolean
}
export class TabDataGenerator {
@@ -23,12 +26,18 @@ export class TabDataGenerator {
['cwc', 'Chat'],
['featuredev', 'Q - Dev'],
['codetransform', 'Q - Transform'],
+ ['doc', 'Q - Documentation'],
+ ['codescan', 'Q - Review'],
+ ['codetest', 'Q - Test'],
])
private tabInputPlaceholder: Map = new Map([
['unknown', 'Ask a question or enter "/" for quick commands'],
['cwc', 'Ask a question or enter "/" for quick commands'],
['featuredev', 'Describe your task or issue in detail'],
+ ['doc', 'Ask Amazon Q to generate documentation for your project'],
+ ['codescan', 'Waiting for your inputs...'],
+ ['codetest', 'Specify a function(s) in the current file(optional)'],
])
private tabWelcomeMessage: Map = new Map([
@@ -56,6 +65,14 @@ What would you like to work on?`,
'codetransform',
`Welcome to Code Transformation!`,
],
+ [
+ 'doc',
+ `Welcome to doc generation!\n\nI can help generate documentation for your code. To get started, choose what type of doc update you'd like to make.`,
+ ],
+ [
+ 'codetest',
+ `Welcome to Amazon Q Unit Test Generation. I can help you generate unit tests for your active file.`,
+ ]
])
private tabContextCommand: Map = new Map([
@@ -67,6 +84,9 @@ What would you like to work on?`,
this.quickActionsGenerator = new QuickActionGenerator({
isFeatureDevEnabled: props.isFeatureDevEnabled,
isCodeTransformEnabled: props.isCodeTransformEnabled,
+ isDocEnabled: props.isDocEnabled,
+ isCodeScanEnabled: props.isCodeScanEnabled,
+ isCodeTestEnabled: props.isCodeTestEnabled,
})
}
@@ -74,7 +94,7 @@ What would you like to work on?`,
return {
tabTitle: taskName ?? this.tabTitle.get(tabType),
promptInputInfo:
- 'Amazon Q Developer uses generative AI. You may need to verify responses. See the [AWS Responsible AI Policy](https://aws.amazon.com/machine-learning/responsible-ai/policy/).',
+ 'Amazon Q Developer uses generative AI. You may need to verify responses. See the [AWS Responsible AI Policy](https://aws.amazon.com/machine-learning/responsible-ai/policy/). Amazon Q Developer processes data across all US Regions. See [here](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/cross-region-inference.html) for more info. Amazon Q may retain chats to provide and maintain the service.',
quickActionCommands: this.quickActionsGenerator.generateForTab(tabType),
promptInputPlaceholder: this.tabInputPlaceholder.get(tabType),
contextCommands: this.tabContextCommand.get(tabType),
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/telemetry/actions.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/telemetry/actions.ts
new file mode 100644
index 0000000000..ffd65684ff
--- /dev/null
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/telemetry/actions.ts
@@ -0,0 +1,38 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ExtensionMessage } from '../commands'
+import { TabType } from '../storages/tabsStorage'
+
+export function createClickTelemetry(source: string): ExtensionMessage {
+ return {
+ command: 'send-telemetry',
+ source,
+ }
+}
+export function isClickTelemetry(message: ExtensionMessage): boolean {
+ return (
+ message.command === 'send-telemetry' && typeof message.source === 'string' && Object.keys(message).length === 2
+ )
+}
+
+export function createOpenAgentTelemetry(module: TabType, trigger: Trigger): ExtensionMessage {
+ return {
+ command: 'send-telemetry',
+ module,
+ trigger,
+ }
+}
+
+export type Trigger = 'right-click' | 'quick-action' | 'quick-start'
+
+export function isOpenAgentTelemetry(message: ExtensionMessage): boolean {
+ return (
+ message.command === 'send-telemetry' &&
+ typeof message.module === 'string' &&
+ typeof message.trigger === 'string' &&
+ Object.keys(message).length === 3
+ )
+}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/texts/constants.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/texts/constants.ts
index 577f6d3f3d..9c6c6dbc9a 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/texts/constants.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/texts/constants.ts
@@ -26,3 +26,9 @@ export const uiComponentsTexts = {
spinnerText: 'Generating your answer...',
pleaseSelect: 'Please select',
}
+
+export const docUserGuide = 'https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/doc-generation.html'
+export const featureDevUserGuide = 'https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html'
+export const codeTransformUserGuide = 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html'
+export const codeTestUserGuide = 'https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/test-generation.html'
+export const codeScanUserGuide = 'https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-reviews.html'
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/agent.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/agent.ts
new file mode 100644
index 0000000000..476991f5be
--- /dev/null
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/agent.ts
@@ -0,0 +1,194 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ChatItemContent, ChatItemType, MynahIcons, MynahUIDataModel } from '@aws/mynah-ui-chat'
+
+function createdTabbedData(examples: string[], agent: string): ChatItemContent['tabbedContent'] {
+ const exampleText = examples.map((example) => `- ${example}`).join('\n')
+ return [
+ {
+ label: 'Examples',
+ value: 'examples',
+ icon: MynahIcons.PLAY,
+ content: {
+ body: `**Example use cases:**\n${exampleText}\n\nEnter ${agent} in Q Chat to get started`,
+ },
+ },
+ ]
+}
+
+export const agentWalkthroughDataModel: MynahUIDataModel = {
+ tabBackground: false,
+ compactMode: false,
+ tabTitle: 'Explore',
+ promptInputVisible: false,
+ tabHeaderDetails: {
+ icon: MynahIcons.ASTERISK,
+ title: 'Amazon Q Developer agents capabilities',
+ description: '',
+ },
+ chatItems: [
+ {
+ type: ChatItemType.ANSWER,
+ snapToTop: true,
+ hoverEffect: true,
+ body: `### Feature development
+Implement features or make changes across your workspace, all from a single prompt.
+`,
+ icon: MynahIcons.CODE_BLOCK,
+ footer: {
+ tabbedContent: createdTabbedData(
+ [
+ '/dev update app.py to add a new api',
+ '/dev fix the error',
+ '/dev add a new button to sort by ',
+ ],
+ '/dev'
+ ),
+ },
+ buttons: [
+ {
+ status: 'clear',
+ id: 'user-guide-featuredev',
+ disabled: false,
+ text: 'Read user guide',
+ },
+ {
+ status: 'main',
+ disabled: false,
+ flash: 'once',
+ icon: MynahIcons.RIGHT_OPEN,
+ id: 'quick-start-featuredev',
+ text: `Quick start with **/dev**`,
+ },
+ ],
+ },
+ {
+ type: ChatItemType.ANSWER,
+ hoverEffect: true,
+ body: `### Unit test generation
+Automatically generate unit tests for your active file.
+`,
+ icon: MynahIcons.BUG,
+ footer: {
+ tabbedContent: createdTabbedData(
+ ['Generate tests for specific functions', 'Generate tests for null and empty inputs'],
+ '/test'
+ ),
+ },
+ buttons: [
+ {
+ status: 'clear',
+ id: 'user-guide-codetest',
+ disabled: false,
+ text: 'Read user guide',
+ },
+ {
+ status: 'primary',
+ disabled: false,
+ icon: MynahIcons.RIGHT_OPEN,
+ flash: 'once',
+ id: 'quick-start-codetest',
+ text: `Quick start with **/test**`,
+ },
+ ],
+ },
+ {
+ type: ChatItemType.ANSWER,
+ hoverEffect: true,
+ body: `### Documentation generation
+Create and update READMEs for better documented code.
+`,
+ icon: MynahIcons.CHECK_LIST,
+ footer: {
+ tabbedContent: createdTabbedData(
+ [
+ 'Generate new READMEs for your project',
+ 'Update existing READMEs with recent code changes',
+ 'Request specific changes to a README',
+ ],
+ '/doc'
+ ),
+ },
+ buttons: [
+ {
+ status: 'clear',
+ id: 'user-guide-doc',
+ disabled: false,
+ text: 'Read user guide',
+ },
+ {
+ disabled: false,
+ icon: MynahIcons.RIGHT_OPEN,
+ flash: 'infinite',
+ id: 'quick-start-doc',
+ text: `Quick start with **/doc**`,
+ },
+ ],
+ },
+ {
+ type: ChatItemType.ANSWER,
+ hoverEffect: true,
+ body: `### Code reviews
+Review code for issues, then get suggestions to fix your code instantaneously.
+`,
+ icon: MynahIcons.TRANSFORM,
+ footer: {
+ tabbedContent: createdTabbedData(
+ [
+ 'Review code for security vulnerabilities and code quality issues',
+ 'Get detailed explanations about code issues',
+ 'Apply automatic code fixes to your files',
+ ],
+ '/review'
+ ),
+ },
+ buttons: [
+ {
+ status: 'clear',
+ id: 'user-guide-codescan',
+ disabled: false,
+ text: 'Read user guide',
+ },
+ {
+ disabled: false,
+ icon: MynahIcons.RIGHT_OPEN,
+ flash: 'infinite',
+ id: 'quick-start-codescan',
+ text: `Quick start with **/review**`,
+ },
+ ],
+ },
+ {
+ type: ChatItemType.ANSWER,
+ hoverEffect: true,
+ body: `### Transformation
+Upgrade library and language versions in your codebase.
+`,
+ icon: MynahIcons.TRANSFORM,
+ footer: {
+ tabbedContent: createdTabbedData(
+ ['Upgrade Java language and dependency versions', 'Convert embedded SQL code in Java apps'],
+ '/transform'
+ ),
+ },
+ buttons: [
+ {
+ status: 'clear',
+ id: 'user-guide-codetransform',
+ disabled: false,
+ text: 'Read user guide',
+ },
+ {
+ disabled: false,
+ icon: MynahIcons.RIGHT_OPEN,
+ flash: 'infinite',
+ id: 'quick-start-codetransform',
+ text: `Quick start with **/transform**`,
+ },
+ ],
+ },
+ ],
+}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts
new file mode 100644
index 0000000000..2c28a032f2
--- /dev/null
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts
@@ -0,0 +1,46 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ChatItemType, MynahIcons, MynahUITabStoreTab } from '@aws/mynah-ui-chat'
+import { TabDataGenerator } from '../tabs/generator'
+
+export const welcomeScreenTabData = (tabs: TabDataGenerator): MynahUITabStoreTab => ({
+ isSelected: true,
+ store: {
+ quickActionCommands: tabs.quickActionsGenerator.generateForTab('welcome'),
+ tabTitle: 'Welcome to Q',
+ tabBackground: true,
+ chatItems: [
+ {
+ type: ChatItemType.ANSWER,
+ icon: MynahIcons.ASTERISK,
+ messageId: 'new-welcome-card',
+ body: `#### Work on a task using agentic capabilities
+_Generate code, scan for issues, and more._`,
+ buttons: [
+ {
+ id: 'explore',
+ disabled: false,
+ text: 'Explore',
+ },
+ {
+ id: 'quick-start',
+ text: 'Quick start',
+ disabled: false,
+ status: 'main',
+ },
+ ],
+ },
+ ],
+ promptInputLabel: 'Or, start a chat',
+ promptInputPlaceholder: 'Type your question',
+ compactMode: true,
+ tabHeaderDetails: {
+ title: "Hi, I'm Amazon Q.",
+ description: 'Where would you like to start?',
+ icon: MynahIcons.Q,
+ },
+ },
+})
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt
index 2176259dd2..c78a812bdf 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt
@@ -87,6 +87,15 @@ class CodeWhispererSettings : PersistentStateComponent()
val intValue by map()
+ val stringValue by map()
}
enum class CodeWhispererConfigurationType {
@@ -122,6 +135,10 @@ enum class CodeWhispererConfigurationType {
HasEnabledProjectContextOnce,
}
+enum class CodeWhispererStringConfigurationType {
+ IgnoredCodeReviewIssues,
+}
+
enum class CodeWhispererIntConfigurationType {
ProjectContextIndexThreadCount,
ProjectContextIndexMaxSize,
diff --git a/plugins/amazonq/src/main/resources/META-INF/plugin.xml b/plugins/amazonq/src/main/resources/META-INF/plugin.xml
index f4ce6dfac2..37db706fe7 100644
--- a/plugins/amazonq/src/main/resources/META-INF/plugin.xml
+++ b/plugins/amazonq/src/main/resources/META-INF/plugin.xml
@@ -5,38 +5,47 @@
amazon.q
Amazon Q
Amazon Q is your generative AI-powered assistant across the software development lifecycle.
- Inline code suggestions
- Code faster with inline code suggestions as you type.
-
-15+ languages supported including Java, Python, TypeScript, Rust, Terraform, AWS Cloudformation, and more
- Chat
- Generate code, refactor existing code, explain code, and get answers to questions about software development.
-
+ The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI
+
+ Agent capabilities
+ Implement new features
+ /dev
to task Amazon Q with generating new code across your entire project and implement features.
+
+ Generate documentation
+ /docs
to task Amazon Q with writing API, technical design, and onboarding documentation.
+
+ Automate code reviews
+ /review
to ask Amazon Q to perform code reviews, flagging suspicious code patterns and assessing deployment risk.
+
+ Generate unit tests
+ /test
to ask Amazon Q to generate unit tests and add them to your project, helping you improve code quality, fast.
- Security scans
- Analyze and fix security vulnerabilities in your project.
-10 languages supported including Java, Python, Javascript, Golang, and more
+ Transform workloads
+ /transform
to upgrade your Java applications in minutes, not weeks.
- Agent for software development
- Let Amazon Q plan and implement new functionality across multiple files in your workspace. Type “/” in chat to open the quick actions menu and choose the “/dev” action.
+ Core features
- Agent for code transformation
- Upgrade your Java applications in minutes, not weeks. Type “/” in chat to open the quick actions menu and choose the “/transform” action.
- Currently supports upgrading Java 8 or 11 Maven projects to Java 17
-
+ Inline chat
+ Seamlessly initial chat within the inline coding experience. Select a section of code that you need assistance with and initiate chat within the editor to request actions such as "Optimize this code", "Add comments", or "Write tests".
+
+ Chat
+ Generate code, explain code, and get answers about software development.
+
+ Inline suggestions
+
+ 15+ languages supported including Python, TypeScript, Rust, Terraform, AWS Cloudformation, and more
Code reference log
Attribute code from Amazon Q that is similar to training data. When code suggestions similar to training data are accepted, they will be added to the code reference log.
- Getting Started
- Free Tier - create or log in with an AWS Builder ID (no AWS account needed!).
- Pro Tier - if your organization is on the Amazon Q Developer Pro tier, log in with single sign-on.
-
+ Getting Started
+ Free Tier - create or log in with an AWS Builder ID (a personal profile from AWS).
+ Pro Tier - if your organization is on the Amazon Q Developer Pro tier, log in with single sign-on.
+
- Troubleshooting & feedback
- File a bug or submit a feature request on our Github repository.
+ Troubleshooting & feedback
+ File a bug or submit a feature request on our Github repository.
]]>
1.0
diff --git a/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-critical.svg b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-critical.svg
new file mode 100644
index 0000000000..7733994d24
--- /dev/null
+++ b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-critical.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-high.svg b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-high.svg
new file mode 100644
index 0000000000..ff92aebc81
--- /dev/null
+++ b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-high.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-info.svg b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-info.svg
new file mode 100644
index 0000000000..dbf7860917
--- /dev/null
+++ b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-info.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-low.svg b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-low.svg
new file mode 100644
index 0000000000..4ca6d96961
--- /dev/null
+++ b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-low.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-medium.svg b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-medium.svg
new file mode 100644
index 0000000000..a906d9b487
--- /dev/null
+++ b/plugins/core/jetbrains-community/resources/icons/resources/codewhisperer/severity-initial-medium.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/plugins/core/jetbrains-community/resources/telemetryOverride.json b/plugins/core/jetbrains-community/resources/telemetryOverride.json
index d0fb37e5ce..5637baaa16 100644
--- a/plugins/core/jetbrains-community/resources/telemetryOverride.json
+++ b/plugins/core/jetbrains-community/resources/telemetryOverride.json
@@ -1,5 +1,20 @@
{
"types": [
+ {
+ "name": "acceptedCharactersCount",
+ "type": "int",
+ "description": "The number of accepted characters"
+ },
+ {
+ "name": "acceptedCount",
+ "type": "int",
+ "description": "The number of accepted cases"
+ },
+ {
+ "name": "acceptedLinesCount",
+ "type": "int",
+ "description": "The number of accepted lines of code"
+ },
{
"name": "amazonqIndexFileSizeInMB",
"type": "int",
@@ -25,6 +40,16 @@
"type": "string",
"description": "Source triggering token refresh"
},
+ {
+ "name": "buildPayloadBytes",
+ "type": "int",
+ "description": "The uncompressed payload size in bytes of the source files in customer project context"
+ },
+ {
+ "name": "buildZipFileBytes",
+ "type": "int",
+ "description": "The compressed payload size of source files in bytes of customer project context sent"
+ },
{
"name": "component",
"allowedValues": [
@@ -160,6 +185,51 @@
],
"description": "Identifies the specific interaction that opens the chat panel"
},
+ {
+ "name": "executedCount",
+ "type": "int",
+ "description": "The number of executed operations"
+ },
+ {
+ "name": "generatedCharactersCount",
+ "type": "int",
+ "description": "Number of characters of code generated"
+ },
+ {
+ "name": "generatedCount",
+ "type": "int",
+ "description": "The number of generated cases"
+ },
+ {
+ "name": "generatedLinesCount",
+ "type": "int",
+ "description": "The number of generated lines of code"
+ },
+ {
+ "name": "hasUserPromptSupplied",
+ "type": "boolean",
+ "description": "True if user supplied prompt message as input else false"
+ },
+ {
+ "name": "isCodeBlockSelected",
+ "type": "boolean",
+ "description": "True if user selected code snippet as input else false"
+ },
+ {
+ "name": "isSupportedLanguage",
+ "type": "boolean",
+ "description": "Indicate if the language is supported"
+ },
+ {
+ "name": "jobGroup",
+ "type": "string",
+ "description": "Job group name used in the operation"
+ },
+ {
+ "name": "jobId",
+ "type": "string",
+ "description": "Job id used in the operation"
+ },
{
"name": "reAuth",
"type": "boolean",
@@ -449,6 +519,92 @@
}
]
},
+ {
+ "name": "amazonq_utgGenerateTests",
+ "description": "Client side invocation of the AmazonQ Unit Test Generation",
+ "metadata": [
+ {
+ "type": "acceptedCharactersCount",
+ "required": false
+ },
+ {
+ "type": "acceptedCount",
+ "required": false
+ },
+ {
+ "type": "acceptedLinesCount",
+ "required": false
+ },
+ {
+ "type": "artifactsUploadDuration",
+ "required": false
+ },
+ {
+ "type": "buildPayloadBytes",
+ "required": false
+ },
+ {
+ "type": "buildZipFileBytes",
+ "required": false
+ },
+ {
+ "type": "credentialStartUrl",
+ "required": false
+ },
+ {
+ "type": "cwsprChatProgrammingLanguage"
+ },
+ {
+ "type": "generatedCharactersCount",
+ "required": false
+ },
+ {
+ "type": "generatedCount",
+ "required": false
+ },
+ {
+ "type": "generatedLinesCount",
+ "required": false
+ },
+ {
+ "type": "hasUserPromptSupplied"
+ },
+ {
+ "type": "isCodeBlockSelected",
+ "required": false
+ },
+ {
+ "type": "isSupportedLanguage"
+ },
+ {
+ "type": "jobGroup",
+ "required": false
+ },
+ {
+ "type": "jobId",
+ "required": false
+ },
+ {
+ "type": "perfClientLatency",
+ "required": false
+ },
+ {
+ "type": "result"
+ },
+ {
+ "type": "reason",
+ "required": false
+ },
+ {
+ "type": "reasonDesc",
+ "required": false
+ },
+ {
+ "type": "source",
+ "required": false
+ }
+ ]
+ },
{
"name": "auth_modifyConnection",
"description": "An auth connection was modified in some way, e.g. deleted, updated",
diff --git a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt
index 024c57f80c..3ed96be9f5 100644
--- a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt
+++ b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt
@@ -115,6 +115,8 @@ object AwsIcons {
object CodeWhisperer {
@JvmField val CUSTOM = load("icons/resources/CodewhispererCustom.svg") // 16 * 16
+ // Icons with full severity string
+
@JvmField val SEVERITY_INFO = load("/icons/resources/codewhisperer/severity-info.svg")
@JvmField val SEVERITY_LOW = load("/icons/resources/codewhisperer/severity-low.svg")
@@ -124,6 +126,18 @@ object AwsIcons {
@JvmField val SEVERITY_HIGH = load("/icons/resources/codewhisperer/severity-high.svg")
@JvmField val SEVERITY_CRITICAL = load("/icons/resources/codewhisperer/severity-critical.svg")
+
+ // Icons with severity initials
+
+ @JvmField val SEVERITY_INITIAL_INFO = load("/icons/resources/codewhisperer/severity-initial-info.svg")
+
+ @JvmField val SEVERITY_INITIAL_LOW = load("/icons/resources/codewhisperer/severity-initial-low.svg")
+
+ @JvmField val SEVERITY_INITIAL_MEDIUM = load("/icons/resources/codewhisperer/severity-initial-medium.svg")
+
+ @JvmField val SEVERITY_INITIAL_HIGH = load("/icons/resources/codewhisperer/severity-initial-high.svg")
+
+ @JvmField val SEVERITY_INITIAL_CRITICAL = load("/icons/resources/codewhisperer/severity-initial-critical.svg")
}
}
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 f64ba2c269..c0750e73ad 100644
--- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties
+++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties
@@ -17,29 +17,60 @@ action.aws.toolkit.dynamoViewer.changeMaxResults.text=Max Results
action.aws.toolkit.dynamodb.delete_table.text=Delete Table...
action.aws.toolkit.ecr.repository.pull.text=Pull from Repository...
action.aws.toolkit.ecr.repository.push.text=Push to Repository...
-action.aws.toolkit.jetbrains.core.services.cwc.commands.ExplainCodeAction.description = Explains the selected code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.ExplainCodeAction.text = Explain Code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.description = Fixes the selected code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.text = Fix Code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.description = Generates unit tests for the selected code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.text = Generate Tests (Beta)
-action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.description = Optimizes the selected code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.text = Optimize Code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.RefactorCodeAction.description = Refactors the selected code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.RefactorCodeAction.text = Refactor Code
-action.aws.toolkit.jetbrains.core.services.cwc.commands.SendToPromptAction.description = Sends selected code to chat
-action.aws.toolkit.jetbrains.core.services.cwc.commands.SendToPromptAction.text = Send to Prompt
-action.aws.toolkit.jetbrains.core.services.cwc.inline.openChat.text = Inline Chat
+action.aws.toolkit.jetbrains.core.services.cwc.commands.ExplainCodeAction.description=Explains the selected code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.ExplainCodeAction.text=Explain Code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.description=Fixes the selected code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.text=Fix Code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.description=Generates unit tests for the selected code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.text=Generate Tests
+action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.description=Optimizes the selected code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.text=Optimize Code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.RefactorCodeAction.description=Refactors the selected code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.RefactorCodeAction.text=Refactor Code
+action.aws.toolkit.jetbrains.core.services.cwc.commands.SendToPromptAction.description=Sends selected code to chat
+action.aws.toolkit.jetbrains.core.services.cwc.commands.SendToPromptAction.text=Send to Prompt
+action.aws.toolkit.jetbrains.core.services.cwc.inline.openChat.text=Inline Chat
action.aws.toolkit.open.arn.browser.text=Open ARN in AWS Console
action.aws.toolkit.open.telemetry.viewer.text=View AWS Telemetry
action.aws.toolkit.s3.open.bucket.viewer.prefixed.text=View Bucket with Prefix...
action.aws.toolkit.s3.open.bucket.viewer.text=View Bucket
action.aws.toolkit.toolwindow.explorer.newConnection.text=Setup authentication to begin
action.aws.toolkit.toolwindow.newConnection.text=Add Another Connection...
-action.aws.toolkit.toolwindow.sso.signout.text=Sign out of SSO
action.dynamic.open.text=Open Resource...
action.q.openchat.text=Open Chat Panel
amazonqChat.project_context.index_in_progress=By the way, I'm still indexing this project for full context from your workspace. I may have a better response in a few minutes when it's complete if you'd like to try again then.
+amazonqDoc.edit.message=Please describe the changes you would like to make to your documentation. For example, you can ask me to add a section, remove a section, make a correction, expand upon something, etc.
+amazonqDoc.edit.placeholder=Describe your documentation request
+amazonqDoc.error.generating=Unable to generate changes.
+amazonqDoc.error_text=I'm sorry, I ran into an issue while trying to generate your documentation. Please try again.
+amazonqDoc.exception.content_length_error=Your workspace is too large for me to review. Your workspace must be within the quota, even if you choose a smaller folder. For more information on quotas, see the Amazon Q Developer documentation.
+amazonqDoc.exception.no_change_required=I couldn't find any code changes to update in the README. Try another documentation task.
+amazonqDoc.exception.prompt_too_vague=I need more information to make changes to your README. Try providing some of the following details:\n- Which sections you want to modify\n- The content you want to add or remove\n- Specific issues that need correcting\n\nFor more information on prompt best practices, see the Amazon Q Developer documentation.
+amazonqDoc.exception.prompt_unrelated=These changes don't seem related to documentation. Try describing your changes again, using the following best practices:\n- Changes should relate to how project functionality is reflected in the README\n- Content you refer to should be available in your codebase\n\nFor more information on prompt best practices, see the Amazon Q Developer documentation.
+amazonqDoc.exception.readme_too_large=The README in your folder is too large for me to review. Try reducing the size of your README, or choose a folder with a smaller README. For more information on quotas, see the Amazon Q Developer documentation.
+amazonqDoc.exception.workspace_empty=The folder you chose did not contain any source files in a supported language. Choose another folder and try again. For more information on supported languages, see the Amazon Q Developer documentation.
+amazonqDoc.inprogress_message.generating=Generating documentation...
+amazonqDoc.progress_message.baseline=This may take a few minutes.
+amazonqDoc.progress_message.creating=Okay, I'm creating a README for your project.
+amazonqDoc.progress_message.generating=Generating documentation
+amazonqDoc.progress_message.scanning=Scanning source files
+amazonqDoc.progress_message.summarizing=Summarizing source files
+amazonqDoc.progress_message.updating=Okay, I'm updating the README to reflect your code changes.
+amazonqDoc.prompt.create=Create a README
+amazonqDoc.prompt.folder.change=Change folder
+amazonqDoc.prompt.folder.proceed=Yes
+amazonqDoc.prompt.placeholder=Choose an option to continue
+amazonqDoc.prompt.reject.close_session=End session
+amazonqDoc.prompt.reject.message=Your changes have been discarded.
+amazonqDoc.prompt.reject.new_task=Start a new documentation task
+amazonqDoc.prompt.review.accept=Accept
+amazonqDoc.prompt.review.changes=Make changes
+amazonqDoc.prompt.review.message=Please review and accept the changes.
+amazonqDoc.prompt.update=Update an existing README
+amazonqDoc.prompt.update.follow_up.edit=Make a specific edit or change
+amazonqDoc.prompt.update.follow_up.sync=Synchronize with recent code changes
+amazonqDoc.session.create=Create documentation for a specific folder
+amazonqDoc.session.sync=Sync documentation
amazonqFeatureDev.chat_message.ask_for_new_task=What new task would you like to work on?
amazonqFeatureDev.chat_message.closed_session=Okay, I've ended this chat session. You can open a new tab to chat or start another workflow.
amazonqFeatureDev.chat_message.requesting_changes=Requesting changes ...
@@ -223,6 +254,9 @@ aws.settings.auto_update.text=Automatically install plugin updates when availabl
aws.settings.aws_cli_settings=AWS CLI Settings
aws.settings.codewhisperer.automatic_import_adder=Imports recommendation
aws.settings.codewhisperer.automatic_import_adder.tooltip=Amazon Q will add import statements with code suggestions when necessary
+aws.settings.codewhisperer.code_review=Code Review
+aws.settings.codewhisperer.code_review.description=Specifies a list of code issue identifiers(separated by ";") that Amazon Q should ignore when reviewing your workspace. Each item in the array should be a unique string identifier for a specific code issue. This allows you to suppress notifications for known issues that you've assessed and determined to be false positives or not applicable to your project. Use this setting with caution, as it may cause you to miss important security alerts.
+aws.settings.codewhisperer.code_review.title=Ignored Security Issues
aws.settings.codewhisperer.configurable.controlled_by_admin=\ Controlled by your admin
aws.settings.codewhisperer.configurable.opt_out.title=Share Amazon Q content with AWS
aws.settings.codewhisperer.configurable.opt_out.tooltip=When checked, your content processed by Amazon Q may be used for service improvement (except for content processed by the Amazon Q Developer Pro tier). Unchecking this box will cause AWS to delete any of your content used for that purpose. The information used to provide the Amazon Q service to you will not be affected. See the Service Terms for more detail.
@@ -791,54 +825,86 @@ codemodernizer.toolwindow.table.header.run_length=Job running time
codemodernizer.toolwindow.table.header.status=Status
codemodernizer.toolwindow.transformation.progress.header=Transformation progress
codemodernizer.toolwindow.transformation.progress.running_time=Running time: {0}
+codescan.chat.message.button.fileScan=Review active file
+codescan.chat.message.button.openIssues=View in Code Issues Panel
+codescan.chat.message.button.projectScan=Review project
+codescan.chat.message.error_request=Request failed
+codescan.chat.message.not_git_repo=Your workspace is not in a git repository. I'll review your project files for security issues, and your in-flight changes for code quality issues.
+codescan.chat.message.project_scan_failed=Sorry, I ran into an issue during the review. Please try again.
+codescan.chat.message.scan_begin_file=Okay, I'm reviewing your file for code issues.
+codescan.chat.message.scan_begin_project=Okay, I'm reviewing your project for code issues.
+codescan.chat.message.scan_begin_wait_time=This may take a few minutes. I'll share updates here as I work on this.
+codescan.chat.message.scan_file_in_progress=File review is in progress...
+codescan.chat.message.scan_project_in_progress=Project review is in progress...
+codescan.chat.message.scan_step_1=Initiating code review.
+codescan.chat.message.scan_step_2=Waiting for review to finish.
+codescan.chat.message.scan_step_3=Processing review results.
+codescan.chat.new_scan.input.message=Which type of review would you like to run?
+codescan.chat.placeholder.scan_in_progress=Reviewing code issues...
+codescan.chat.placeholder.waiting_for_inputs=Waiting on your inputs...
codewhisperer.actions.connect_github.title=Connect with Us on GitHub
codewhisperer.actions.open_settings.title=Open Settings
codewhisperer.actions.send_feedback.title=Send Feedback
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.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 scan job cancelled by user.
+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.
+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=Amazon Q: Explain
-codewhisperer.codescan.file_ext_not_supported=File extension {0} is not supported for Amazon Q Security Scan feature. Please try again with a valid file format - java, python, javascript, typescript, csharp, yaml, json, tf, hcl, ruby, go.
+codewhisperer.codescan.explain_button_label=Explain
+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}
-codewhisperer.codescan.file_too_large=Amazon Q: The selected file exceeds the input artifact limit. Try again with a smaller file. For more information about scan limits, see the Amazon Q documentation.
+codewhisperer.codescan.file_path_label=File Path
+codewhisperer.codescan.file_too_large=Amazon Q: The selected file exceeds the input artifact limit. Try again with a smaller file. For more information about review limits, see the Amazon Q documentation.
codewhisperer.codescan.file_too_large_telemetry=Payload size limit reached
codewhisperer.codescan.fix_applied_fail=Apply fix command failed. {0}
codewhisperer.codescan.fix_available_label=Code fix available
codewhisperer.codescan.fix_button_label=Fix with Q
+codewhisperer.codescan.generate_fix_button_label=Generate Fix
+codewhisperer.codescan.ignore_all_button=Ignore All
+codewhisperer.codescan.ignore_button=Ignore
codewhisperer.codescan.invalid_source_zip_telemetry=Failed to create valid source zip.
-codewhisperer.codescan.java_module_not_found=Java plugin is required for scanning Java files, install Java plugin or perform the code scan in Intellij Idea instead.
-codewhisperer.codescan.no_file_open=Amazon Q: No file is open in an active editor. Open a file to start a Security Scan.
-codewhisperer.codescan.no_file_open_telemetry=Open a valid file to scan.
-codewhisperer.codescan.problems_window_not_found=Unable to display Security Scan results as the Problems View tool window cannot be fetched.
-codewhisperer.codescan.run_scan=Run Project Scan
-codewhisperer.codescan.run_scan_complete= Security Scan completed for {0, choice, 1#1 file|2#{0,number} files}. {1, choice, 0#No issues|1#1 issue|2#{1,number} issues} found in {2}. Last Run {4}
-codewhisperer.codescan.run_scan_error=Amazon Q encountered an error while scanning for security issues. Please try again later.
-codewhisperer.codescan.run_scan_error_telemetry=Security scan failed.
-codewhisperer.codescan.run_scan_info=Select 'Run' in toolbar to scan this package for security issues.
-codewhisperer.codescan.scan_display=Amazon Q Security Issues
-codewhisperer.codescan.scan_display_with_issues=Amazon Q Security Issues {0}
-codewhisperer.codescan.scan_in_progress=Scanning active project and its dependencies...
+codewhisperer.codescan.java_module_not_found=Java plugin is required for reviewing Java files, install Java plugin or perform the code review in Intellij Idea instead.
+codewhisperer.codescan.no_file_open=Amazon Q: No file is open in an active editor. Open a file to start a Code Review.
+codewhisperer.codescan.no_file_open_telemetry=Open a valid file to review.
+codewhisperer.codescan.problems_window_not_found=Unable to display Code Review results as the Problems View tool window cannot be fetched.
+codewhisperer.codescan.quota_exceeded=You've reached the monthly quota for Amazon Q Developer's agent capabilities. You can try again next month. For more information on usage limits, see the Amazon Q Developer pricing page.
+codewhisperer.codescan.regenerate_fix_button_label=Regenerate Fix
+codewhisperer.codescan.run_scan=Full Project Scan is now /review! Open in Chat Panel
+codewhisperer.codescan.run_scan_complete= Code Review completed for {0, choice, 1#1 file|2#{0,number} files}. {1, choice, 0#No issues|1#1 issue|2#{1,number} issues} found in {2}. Last Run {4}
+codewhisperer.codescan.run_scan_error=Amazon Q encountered an error while reviewing for code issues. Please try again later.
+codewhisperer.codescan.run_scan_error_telemetry=Code Review failed.
+codewhisperer.codescan.run_scan_info=Enter /review in Amazon Q Chat Panel to run code reviews.
+codewhisperer.codescan.scan_complete_count=- {0}: `{1, choice, 1#{1,number} issue|2#{1,number} issues}`
+codewhisperer.codescan.scan_complete_file=Reviewing your File is complete. Here's what I found:
+codewhisperer.codescan.scan_complete_project=Reviewing your Project is complete. Here's what I found:
+codewhisperer.codescan.scan_display=Amazon Q Code Issues
+codewhisperer.codescan.scan_display_with_issues=Amazon Q Code Issues {0}
+codewhisperer.codescan.scan_in_progress=Code review in progress...
codewhisperer.codescan.scan_recommendation= {0} {1}
-codewhisperer.codescan.scan_recommendation_invalid= {0} {1} [No longer valid: Re-scan to validate the fix]
-codewhisperer.codescan.scan_recommendation_invalid.tooltip_text=No longer valid. Re-scan to validate the fix.
-codewhisperer.codescan.scan_timed_out=Security Scan failed. Amazon Q timed out.
-codewhisperer.codescan.scanned_files_heading= {0} files were scanned during the last security scan.
-codewhisperer.codescan.stop_scan=Stop Security Scan
-codewhisperer.codescan.stop_scan_confirm_button=Stop scan
-codewhisperer.codescan.stop_scan_confirm_message=Are you sure you want to stop ongoing security scan? This scan will be counted as one complete scan towards your monthly security scan limits.
-codewhisperer.codescan.stopping_scan=Stopping Security Scan...
+codewhisperer.codescan.scan_recommendation_invalid= {0} {1} [No longer valid: Re-run the review to validate the fix]
+codewhisperer.codescan.scan_recommendation_invalid.tooltip_text=No longer valid. Re-run the review to validate the fix.
+codewhisperer.codescan.scan_results_hidden_by_filters=All code review results are hidden by current filters.
+codewhisperer.codescan.scan_timed_out=Code Review failed. Amazon Q timed out.
+codewhisperer.codescan.scanned_files_heading= {0} files were reviewed during the last code review.
+codewhisperer.codescan.severity_issues_count= {0} {1, choice, 1#{1,number} issue|2#{1,number} issues}
+codewhisperer.codescan.stop_scan=Stop Code Review
+codewhisperer.codescan.stop_scan_confirm_button=Stop review
+codewhisperer.codescan.stop_scan_confirm_message=Are you sure you want to stop ongoing code review? This review will be counted as one complete review towards your monthly code review limits.
+codewhisperer.codescan.stopping_scan=Stopping Code Review...
codewhisperer.codescan.suggested_fix_description=Why are we recommending this?
codewhisperer.codescan.suggested_fix_label=Suggested code fix preview
-codewhisperer.codescan.unsupported_language_error=Amazon Q: Project does not contain valid files to scan
-codewhisperer.codescan.unsupported_language_error_telemetry=Project does not contain valid files to scan
-codewhisperer.codescan.upload_to_s3_failed=Amazon Q is unable to upload your workspace artifacts to Amazon S3 for security scans. For more information, see the Amazon Q documentation.
-codewhisperer.codescan.view_scanned_files=View {0} scanned files
+codewhisperer.codescan.unsupported_language_error=Amazon Q: Project does not contain valid files to review
+codewhisperer.codescan.unsupported_language_error_telemetry=Project does not contain valid files to review
+codewhisperer.codescan.upload_to_s3_failed=Amazon Q is unable to upload your project artifacts to Amazon S3 for code reviews. For more information, see the Amazon Q documentation.
+codewhisperer.codescan.view_scanned_files=View {0} reviewed files
codewhisperer.credential.login.dialog.exception.cancel_login=Login cancelled
codewhisperer.credential.login.dialog.ok_button=Connect
codewhisperer.credential.login.dialog.prompt=Select a connection option to start using Amazon Q
@@ -866,11 +932,11 @@ codewhisperer.explorer.learn=Learn
codewhisperer.explorer.node.dismiss=Dismiss
codewhisperer.explorer.node.install_q=Install the Amazon Q Plugin
codewhisperer.explorer.pause_auto=Pause Auto-Suggestions
-codewhisperer.explorer.pause_auto_scans =Pause Auto-Scans
+codewhisperer.explorer.pause_auto_scans=Pause Auto-Reviews
codewhisperer.explorer.paused=\ Paused
codewhisperer.explorer.reconnect=Reconnect
codewhisperer.explorer.resume_auto=Resume Auto-Suggestions
-codewhisperer.explorer.resume_auto_scans =Resume Auto-Scans
+codewhisperer.explorer.resume_auto_scans=Resume Auto-Reviews
codewhisperer.explorer.tooltip.comment=Start with auto-suggestions and find more features here!
codewhisperer.explorer.tooltip.title=Get started with Amazon Q
codewhisperer.explorer.usage_limit_hit=\ Free tier limit met, paused until {0}
@@ -908,7 +974,7 @@ codewhisperer.notification.custom.simple.button.select_another_customization=Sel
codewhisperer.notification.custom.simple.button.select_customization=Select customization
codewhisperer.notification.remote.ide_unsupported.message=Please update your IDE backend to a 2023.3 or later version to continue using Amazon Q inline suggestions.
codewhisperer.notification.remote.ide_unsupported.title=Amazon Q inline suggestion not supported in this IDE version
-codewhisperer.notification.usage_limit.codescan.warn.content=Amazon Q: You have reached the monthly limit for project scans.
+codewhisperer.notification.usage_limit.codescan.warn.content=Amazon Q: You have reached the monthly limit for project reviews.
codewhisperer.notification.usage_limit.codesuggestion.warn.content=You have reached the monthly fair use limit of code recommendations.
codewhisperer.popup.button.accept= Insert Code \u21E5
codewhisperer.popup.button.next=Next→
@@ -924,10 +990,10 @@ codewhisperer.statusbar.popup.title=Reconnect to Amazon Q?
codewhisperer.statusbar.sub_menu.connect_help.title=Connect / Help
codewhisperer.statusbar.sub_menu.inline.title=Inline Suggestions
codewhisperer.statusbar.sub_menu.other_features.title=Other Features
-codewhisperer.statusbar.sub_menu.security_scans.title=Security Scans
+codewhisperer.statusbar.sub_menu.security_scans.title=Code Reviews
codewhisperer.statusbar.tooltip=Amazon Q status
codewhisperer.toolwindow.entry.prefix=[{0}] ACCEPTED recommendation with the following code provided with reference under
-codewhisperer.toolwindow.entry.suffix= {1, choice, 0#|1#. Added to {0}} at line {2}
+codewhisperer.toolwindow.entry.suffix={1, choice, 0#|1#. Added to {0}} at line {2}
codewhisperer.toolwindow.popup.text=Reference code under the {0} license from repository {1}
codewhisperer.toolwindow.settings=Amazon Q Settings
codewhisperer.toolwindow.settings.prefix=Don't want suggestions that include code with references? Uncheck this option in
@@ -1261,6 +1327,7 @@ general.acknowledge=Acknowledge
general.add.another=Add another
general.auth.reauthenticate=Reauthenticate
general.cancel=Cancel
+general.canceling=Canceling
general.close_button=Close
general.configure_button=Configure
general.confirm=Confirm
@@ -1304,6 +1371,7 @@ general.save=Save
general.select_button=Select
general.step.canceled={0} has been canceled
general.step.failed={0} has failed: {1}
+general.success=Complete...
general.time=Time
general.time.five_minutes=Five Minutes
general.time.one_minute=One Minute
@@ -1975,6 +2043,18 @@ sqs.subscribe.sns.validation.empty_topic=Topic must be specified.
sqs.toolwindow=SQS
sqs.url.parse_error=Error parsing SQS queue URL
tags.title=Tags
+testgen.error.generic_error_message=I am experiencing technical difficulties at the moment. Please try again in a few minutes.
+testgen.error.maximum_generations_reach=You've reached the monthly quota for Amazon Q Developer's agent capabilities. You can try again next month. For more information on usage limits, see the Amazon Q Developer pricing page .
+testgen.message.cancelled=Unit test generation cancelled.
+testgen.message.failed=Sorry, Test generation failed. Please try again in few minutes.
+testgen.message.regenerate_input=Sure thing. Please provide new instructions for me to generate the tests, and select the function(s) you would like to test.
+testgen.message.success=Unit test generation completed.
+testgen.no_file_found=Sorry, I couldn't find a file to generate tests.
+testgen.placeholder.newtab=Ask any coding question or type \u0022/\u0022 for actions
+testgen.placeholder.select_an_option = Please select an action to proceed (Accept or Reject)
+testgen.placeholder.view_diff="Select View Diff to see the generated unit tests"
+testgen.placeholder.waiting_on_your_inputs=Waiting on your inputs...
+testgen.progressbar.generate_unit_tests=Generating unit tests...
toolkit.login.aws_builder_id.already_connected.cancel=Use existing AWS Builder ID
toolkit.login.aws_builder_id.already_connected.message=You already signed in with an AWS Builder ID.\nSign out to add another?
toolkit.login.aws_builder_id.already_connected.reconnect=Sign out
diff --git a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json
index 00b4bb2334..00e2305eb2 100644
--- a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json
+++ b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json
@@ -1267,7 +1267,8 @@
"required": ["conversationId", "codeGenerationId"],
"members": {
"conversationId": { "shape": "ConversationId" },
- "codeGenerationId": { "shape": "CodeGenerationId" }
+ "codeGenerationId": { "shape": "CodeGenerationId" },
+ "intent": { "shape": "String" }
}
},
"GetTaskAssistCodeGenerationResponse": {
diff --git a/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json b/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json
index 3934499739..1e7898a48d 100644
--- a/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json
+++ b/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json
@@ -288,7 +288,8 @@
"ExportContext":{
"type":"structure",
"members":{
- "transformationExportContext":{"shape":"TransformationExportContext"}
+ "transformationExportContext":{"shape":"TransformationExportContext"},
+ "unitTestGenerationExportContext":{"shape":"UnitTestGenerationExportContext"}
},
"union":true
},
@@ -296,7 +297,8 @@
"type":"string",
"enum":[
"TRANSFORMATION",
- "TASK_ASSIST"
+ "TASK_ASSIST",
+ "UNIT_TESTS"
]
},
"ExportResultArchiveRequest":{
@@ -625,6 +627,12 @@
"USAGE"
]
},
+ "TestGenerationJobGroupName":{
+ "type":"string",
+ "max":128,
+ "min":1,
+ "pattern":"[a-zA-Z0-9-_]+"
+ },
"TextDocument":{
"type":"structure",
"required":["relativeFilePath"],
@@ -706,6 +714,19 @@
"downloadArtifactType":{"shape":"TransformationDownloadArtifactType"}
}
},
+ "UUID":{
+ "type":"string",
+ "max":36,
+ "min":36
+ },
+ "UnitTestGenerationExportContext":{
+ "type":"structure",
+ "required":["testGenerationJobGroupName"],
+ "members":{
+ "testGenerationJobGroupName":{"shape":"TestGenerationJobGroupName"},
+ "testGenerationJobId":{"shape":"UUID"}
+ }
+ },
"UploadId":{
"type":"string",
"max":128,