diff --git a/package-lock.json b/package-lock.json
index ac6d71b0088..070ac99e486 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
"vscode-nls-dev": "^4.0.4"
},
"devDependencies": {
- "@aws-toolkits/telemetry": "^1.0.282",
+ "@aws-toolkits/telemetry": "^1.0.284",
"@playwright/browser-chromium": "^1.43.1",
"@types/he": "^1.2.3",
"@types/vscode": "^1.68.0",
@@ -5135,13 +5135,14 @@
}
},
"node_modules/@aws-toolkits/telemetry": {
- "version": "1.0.282",
- "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.282.tgz",
- "integrity": "sha512-MHktYmucYHvEm4Sscr93UmKr83D9pKJIvETo1bZiNtCsE0jxcNglxZwqZruy13Fks5uk523ZhaIALW22TF0Zpg==",
+ "version": "1.0.285",
+ "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.285.tgz",
+ "integrity": "sha512-O5/kbCE9cXF8scL5XmeDjMX9ojmCLvXg6cwcBayTS4URypI6XFat6drmaIF/QoDqxAfnHLHs0zypOdqSWCDr8w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"ajv": "^6.12.6",
+ "cross-spawn": "^7.0.6",
"fs-extra": "^11.1.0",
"lodash": "^4.17.20",
"prettier": "^3.3.2",
diff --git a/package.json b/package.json
index 70f9d0f4c35..bc03c2b8395 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"generateNonCodeFiles": "npm run generateNonCodeFiles -w packages/ --if-present"
},
"devDependencies": {
- "@aws-toolkits/telemetry": "^1.0.282",
+ "@aws-toolkits/telemetry": "^1.0.284",
"@playwright/browser-chromium": "^1.43.1",
"@types/he": "^1.2.3",
"@types/vscode": "^1.68.0",
diff --git a/packages/amazonq/.changes/next-release/Feature-389df2e8-de2c-4505-b631-97aa8d5025bb.json b/packages/amazonq/.changes/next-release/Feature-389df2e8-de2c-4505-b631-97aa8d5025bb.json
new file mode 100644
index 00000000000..a92a111c70e
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-389df2e8-de2c-4505-b631-97aa8d5025bb.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Added a getting started page for exploring amazon q agents"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-5c2fae3e-c794-438b-8af5-2c31c00ab000.json b/packages/amazonq/.changes/next-release/Feature-5c2fae3e-c794-438b-8af5-2c31c00ab000.json
new file mode 100644
index 00000000000..1181f2b0172
--- /dev/null
+++ b/packages/amazonq/.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/packages/amazonq/.changes/next-release/Feature-5cac73d3-dfc5-4065-a6d9-f093a3c0b258.json b/packages/amazonq/.changes/next-release/Feature-5cac73d3-dfc5-4065-a6d9-f093a3c0b258.json
new file mode 100644
index 00000000000..5be0ead47dd
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-5cac73d3-dfc5-4065-a6d9-f093a3c0b258.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "`/doc` in Q chat to generate and update documentation for your project"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-6967c79c-041f-4201-b10b-6ccd47568bad.json b/packages/amazonq/.changes/next-release/Feature-6967c79c-041f-4201-b10b-6ccd47568bad.json
new file mode 100644
index 00000000000..88d57f1376e
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-6967c79c-041f-4201-b10b-6ccd47568bad.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Amazon Q Code Scan is now Amazon Q Code Review"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-d81e958e-081b-4832-a183-1c863f99d18f.json b/packages/amazonq/.changes/next-release/Feature-d81e958e-081b-4832-a183-1c863f99d18f.json
new file mode 100644
index 00000000000..96c6254f001
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-d81e958e-081b-4832-a183-1c863f99d18f.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/packages/amazonq/.changes/next-release/Feature-ee6b1c26-ea04-4214-8dc4-df09d58c0bdf.json b/packages/amazonq/.changes/next-release/Feature-ee6b1c26-ea04-4214-8dc4-df09d58c0bdf.json
new file mode 100644
index 00000000000..eaf6b86a986
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-ee6b1c26-ea04-4214-8dc4-df09d58c0bdf.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Security Scan: New TreeView to display security scan issues and vulnerabilities detected in your project. The TreeView provides an organized and hierarchical view of the scan results, making it easier to navigate and prioritize the issues that need to be addressed."
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-f5369c80-8c95-4637-82d5-ae6c680aa0e2.json b/packages/amazonq/.changes/next-release/Feature-f5369c80-8c95-4637-82d5-ae6c680aa0e2.json
new file mode 100644
index 00000000000..b293d43007c
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-f5369c80-8c95-4637-82d5-ae6c680aa0e2.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Security Scan: Added ability to suppress or ignore security issues"
+}
diff --git a/packages/amazonq/README.md b/packages/amazonq/README.md
index beee7557b6a..6fe90e3fec2 100644
--- a/packages/amazonq/README.md
+++ b/packages/amazonq/README.md
@@ -3,62 +3,52 @@
[](https://www.youtube.com/@amazonwebservices)

-# Getting Started
-
-**Free Tier** - create or log in with an AWS Builder ID (a personal profile from AWS).
+# Agent capabilities
-**Pro Tier** - if your organization is on the Amazon Q Developer Pro tier, log in with single sign-on.
-
-
+### Implement new features
+`/dev` to task Amazon Q with generating new code across your entire project and implement features.
-# Features
-
-## Inline code suggestions
-
-Code faster with inline code suggestions as you type.
-
-
-
-[_15+ languages supported including Python, TypeScript, Rust, Terraform, AWS Cloudformation, and more_](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html)
+### Generate documentation
+`/docs` to task Amazon Q with writing API, technical design, and onboarding documentation.
-## Chat
+### Automate code reviews
+`/review` to ask Amazon Q to perform code reviews, flagging suspicious code patterns and assessing deployment risk.
-Generate code, explain code, and get answers to questions about software development.
+### Generate unit tests
+`/test` to ask Amazon Q to generate unit tests and add them to your project, helping you improve code quality, fast.
-
+### Transform workloads
+`/transform` to upgrade your Java applications in minutes, not weeks.
-## Security scans
+
-Analyze and fix security vulnerabilities in your project.
+# Core features
-
+### Inline chat
+Seamlessly initiate 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".
-[_10 languages supported including Python, TypeScript, C#, and more_](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/security-scans.html)
+### Chat
+Generate code, explain code, and get answers about software development.
-## Agent for software development
+### Inline suggestions
+Receive real-time code suggestions ranging from snippets to full functions based on your comments and existing code.
-Amazon Q can implement new functionality across multiple files in your workspace.
-
-Type “/” in chat to open the quick actions menu and choose the “/dev” action.
-
-
-
-_Note - this demo has been trimmed, Amazon Q can take several minutes to generate code_
+[_15+ languages supported including Python, TypeScript, Rust, Terraform, AWS Cloudformation, and more_](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html)
-## Agent for code transformation
+### Code reference log
-Upgrade your Java applications in minutes, not weeks.
+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.
-Type “/” in chat to open the quick actions menu and choose the “/transform” action.
+
-
+# Getting Started
-[_Currently supports upgrading Java 8 or 11 Maven projects to Java 17_](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#prerequisites)
+**Free Tier** - create or log in with an AWS Builder ID (a personal profile from AWS).
-## Code reference log
+**Pro Tier** - if your organization is on the Amazon Q Developer Pro tier, log in with single sign-on.
-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.
+
-## Troubleshooting & feedback
+# Troubleshooting & feedback
-[File a bug](https://github.com/aws/aws-toolkit-vscode/issues/new?assignees=&labels=bug&projects=&template=bug_report.md) or [submit a feature request](https://github.com/aws/aws-toolkit-vscode/issues/new?assignees=&labels=feature-request&projects=&template=feature_request.md) on our Github repository.
+[File a bug](https://github.com/aws/aws-toolkit-vscode/issues/new?assignees=&labels=bug&projects=&template=bug_report.md) or [submit a feature request](https://github.com/aws/aws-toolkit-vscode/issues/new?assignees=&labels=feature-request&projects=&template=feature_request.md) on our Github repository.
\ No newline at end of file
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 4fbace6f050..6a324892815 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -1,7 +1,7 @@
{
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
- "description": "Amazon Q is your generative AI-powered assistant across the software development lifecycle.",
+ "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI",
"version": "1.39.0-SNAPSHOT",
"extensionKind": [
"workspace"
@@ -161,6 +161,14 @@
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndexMaxSize%",
"default": 250,
"scope": "application"
+ },
+ "amazonQ.ignoredSecurityIssues": {
+ "type": "array",
+ "markdownDescription": "%AWS.configuration.description.amazonq.ignoredSecurityIssues%",
+ "scope": "window",
+ "items": {
+ "type": "string"
+ }
}
}
},
@@ -198,6 +206,12 @@
"name": "%AWS.amazonq.login%",
"when": "!aws.isWebExtHost && aws.amazonq.showLoginView"
},
+ {
+ "type": "tree",
+ "id": "aws.amazonq.SecurityIssuesTree",
+ "name": "%AWS.amazonq.security%",
+ "when": "!aws.isSageMaker && !aws.isWebExtHost && !aws.amazonq.showLoginView"
+ },
{
"type": "webview",
"id": "aws.AmazonQChatView",
@@ -241,6 +255,16 @@
"view": "aws.amazonq.transformationProposedChangesTree",
"contents": "Project transformation is complete.\n Downloading the proposed changes...",
"when": "gumby.reviewState == PreparingReview"
+ },
+ {
+ "view": "aws.amazonq.SecurityIssuesTree",
+ "contents": "No code issues have been detected in the workspace.",
+ "when": "!aws.amazonq.security.noMatches"
+ },
+ {
+ "view": "aws.amazonq.SecurityIssuesTree",
+ "contents": "No matches.\n[Clear Filters](command:aws.amazonq.securityIssuesTreeFilter.clearFilters)",
+ "when": "aws.amazonq.security.noMatches"
}
],
"submenus": [
@@ -255,6 +279,11 @@
{
"label": "%AWS.generic.help%",
"id": "aws.amazonq.submenu.help"
+ },
+ {
+ "label": "%AWS.generic.moreActions%",
+ "id": "aws.amazonq.submenu.securityIssueMoreActions",
+ "icon": "$(ellipsis)"
}
],
"menus": {
@@ -339,6 +368,38 @@
"submenu": "aws.amazonq.submenu.help",
"when": "view == aws.AmazonQChatView || view == aws.amazonq.AmazonCommonAuth",
"group": "y_toolkitMeta@2"
+ },
+ {
+ "command": "aws.amazonq.security.showFilters",
+ "when": "view == aws.amazonq.SecurityIssuesTree",
+ "group": "navigation"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "_aws.amazonq.notifications.dismiss",
+ "when": "viewItem == amazonqNotificationStartUp",
+ "group": "inline@1"
+ },
+ {
+ "command": "aws.amazonq.openSecurityIssuePanel",
+ "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix)",
+ "group": "inline@4"
+ },
+ {
+ "command": "aws.amazonq.security.ignore",
+ "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix)",
+ "group": "inline@5"
+ },
+ {
+ "command": "aws.amazonq.security.generateFix",
+ "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithoutFix",
+ "group": "inline@6"
+ },
+ {
+ "submenu": "aws.amazonq.submenu.securityIssueMoreActions",
+ "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix)",
+ "group": "inline@7"
}
],
"amazonqEditorContextSubmenu": [
@@ -360,8 +421,7 @@
},
{
"command": "aws.amazonq.generateUnitTests",
- "group": "cw_chat@5",
- "when": "aws.codewhisperer.connected && aws.isInternalUser"
+ "group": "cw_chat@5"
},
{
"command": "aws.amazonq.sendToPrompt",
@@ -378,13 +438,6 @@
"group": "cw_chat"
}
],
- "view/item/context": [
- {
- "command": "_aws.amazonq.notifications.dismiss",
- "when": "viewItem == amazonqNotificationStartUp",
- "group": "inline@1"
- }
- ],
"aws.amazonq.submenu.feedback": [
{
"command": "aws.amazonq.submitFeedback",
@@ -397,6 +450,14 @@
}
],
"aws.amazonq.submenu.help": [
+ {
+ "command": "aws.amazonq.walkthrough.show",
+ "group": "1_help@1"
+ },
+ {
+ "command": "aws.amazonq.exploreAgents",
+ "group": "1_help@2"
+ },
{
"command": "aws.amazonq.github",
"group": "1_help@3"
@@ -409,6 +470,26 @@
"command": "aws.amazonq.viewLogs",
"group": "1_help@5"
}
+ ],
+ "aws.amazonq.submenu.securityIssueMoreActions": [
+ {
+ "command": "aws.amazonq.security.explain",
+ "group": "1_more@1"
+ },
+ {
+ "command": "aws.amazonq.applySecurityFix",
+ "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix",
+ "group": "1_more@3"
+ },
+ {
+ "command": "aws.amazonq.security.regenerateFix",
+ "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix",
+ "group": "1_more@4"
+ },
+ {
+ "command": "aws.amazonq.security.ignoreAll",
+ "group": "1_more@5"
+ }
]
},
"commands": [
@@ -426,7 +507,7 @@
"enablement": "aws.codewhisperer.connected"
},
{
- "command": "aws.amazonq.security.scan",
+ "command": "aws.amazonq.security.scan-statusbar",
"title": "%AWS.command.amazonq.security.scan%",
"category": "%AWS.amazonq.title%",
"enablement": "aws.codewhisperer.connected"
@@ -459,7 +540,7 @@
"command": "aws.amazonq.generateUnitTests",
"title": "%AWS.command.amazonq.generateUnitTests%",
"category": "%AWS.amazonq.title%",
- "enablement": "aws.codewhisperer.connected && aws.isInternalUser"
+ "enablement": "aws.codewhisperer.connected"
},
{
"command": "aws.amazonq.reconnect",
@@ -599,11 +680,72 @@
"category": "%AWS.amazonq.title%",
"enablement": "aws.codewhisperer.connected"
},
+ {
+ "command": "aws.amazonq.securityIssuesTreeFilter.clearFilters",
+ "title": "Clear Filters",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.security.generateFix",
+ "title": "%AWS.command.amazonq.generateFix%",
+ "icon": "$(wrench)",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.applySecurityFix",
+ "title": "%AWS.command.amazonq.acceptFix%",
+ "icon": "$(check)",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.security.regenerateFix",
+ "title": "%AWS.command.amazonq.regenerateFix%",
+ "icon": "$(lightbulb-autofix)",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.openSecurityIssuePanel",
+ "title": "%AWS.command.amazonq.viewDetails%",
+ "icon": "$(search)",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.security.explain",
+ "title": "%AWS.command.amazonq.explainIssue%",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.security.ignore",
+ "title": "%AWS.command.amazonq.ignoreIssue%",
+ "icon": "$(circle-slash)",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.security.ignoreAll",
+ "title": "%AWS.command.amazonq.ignoreAllIssues%",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
+ {
+ "command": "aws.amazonq.security.showFilters",
+ "title": "%AWS.command.amazonq.filterIssues%",
+ "icon": "$(list-filter)",
+ "enablement": "view == aws.amazonq.SecurityIssuesTree"
+ },
{
"command": "aws.amazonq.inline.invokeChat",
"title": "%AWS.amazonq.inline.invokeChat%",
"category": "%AWS.amazonq.title%",
"enablement": "aws.codewhisperer.connected"
+ },
+ {
+ "command": "aws.amazonq.exploreAgents",
+ "title": "%AWS.amazonq.exploreAgents%",
+ "category": "%AWS.amazonq.title%",
+ "enablement": "aws.codewhisperer.connected"
+ },
+ {
+ "command": "aws.amazonq.walkthrough.show",
+ "title": "%AWS.amazonq.welcomeWalkthrough%"
}
],
"keybindings": [
@@ -647,8 +789,7 @@
"command": "aws.amazonq.generateUnitTests",
"key": "win+alt+t",
"mac": "cmd+alt+t",
- "linux": "meta+alt+t",
- "when": "aws.codewhisperer.connected && aws.isInternalUser"
+ "linux": "meta+alt+t"
},
{
"command": "aws.amazonq.invokeInlineCompletion",
@@ -718,327 +859,362 @@
"fontCharacter": "\\f1ac"
}
},
- "aws-amazonq-transform-arrow-dark": {
+ "aws-amazonq-severity-critical": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ad"
}
},
- "aws-amazonq-transform-arrow-light": {
+ "aws-amazonq-severity-high": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ae"
}
},
- "aws-amazonq-transform-default-dark": {
+ "aws-amazonq-severity-info": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1af"
}
},
- "aws-amazonq-transform-default-light": {
+ "aws-amazonq-severity-low": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b0"
}
},
- "aws-amazonq-transform-dependencies-dark": {
+ "aws-amazonq-severity-medium": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b1"
}
},
- "aws-amazonq-transform-dependencies-light": {
+ "aws-amazonq-transform-arrow-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b2"
}
},
- "aws-amazonq-transform-file-dark": {
+ "aws-amazonq-transform-arrow-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b3"
}
},
- "aws-amazonq-transform-file-light": {
+ "aws-amazonq-transform-default-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b4"
}
},
- "aws-amazonq-transform-logo": {
+ "aws-amazonq-transform-default-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b5"
}
},
- "aws-amazonq-transform-step-into-dark": {
+ "aws-amazonq-transform-dependencies-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b6"
}
},
- "aws-amazonq-transform-step-into-light": {
+ "aws-amazonq-transform-dependencies-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b7"
}
},
- "aws-amazonq-transform-variables-dark": {
+ "aws-amazonq-transform-file-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b8"
}
},
- "aws-amazonq-transform-variables-light": {
+ "aws-amazonq-transform-file-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b9"
}
},
- "aws-applicationcomposer-icon": {
+ "aws-amazonq-transform-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ba"
}
},
- "aws-applicationcomposer-icon-dark": {
+ "aws-amazonq-transform-step-into-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bb"
}
},
- "aws-apprunner-service": {
+ "aws-amazonq-transform-step-into-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bc"
}
},
- "aws-cdk-logo": {
+ "aws-amazonq-transform-variables-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bd"
}
},
- "aws-cloudformation-stack": {
+ "aws-amazonq-transform-variables-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1be"
}
},
- "aws-cloudwatch-log-group": {
+ "aws-applicationcomposer-icon": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bf"
}
},
- "aws-codecatalyst-logo": {
+ "aws-applicationcomposer-icon-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c0"
}
},
- "aws-codewhisperer-icon-black": {
+ "aws-apprunner-service": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c1"
}
},
- "aws-codewhisperer-icon-white": {
+ "aws-cdk-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c2"
}
},
- "aws-codewhisperer-learn": {
+ "aws-cloudformation-stack": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c3"
}
},
- "aws-ecr-registry": {
+ "aws-cloudwatch-log-group": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c4"
}
},
- "aws-ecs-cluster": {
+ "aws-codecatalyst-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c5"
}
},
- "aws-ecs-container": {
+ "aws-codewhisperer-icon-black": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c6"
}
},
- "aws-ecs-service": {
+ "aws-codewhisperer-icon-white": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c7"
}
},
- "aws-generic-attach-file": {
+ "aws-codewhisperer-learn": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c8"
}
},
- "aws-iot-certificate": {
+ "aws-ecr-registry": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c9"
}
},
- "aws-iot-policy": {
+ "aws-ecs-cluster": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ca"
}
},
- "aws-iot-thing": {
+ "aws-ecs-container": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cb"
}
},
- "aws-lambda-function": {
+ "aws-ecs-service": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cc"
}
},
- "aws-mynah-MynahIconBlack": {
+ "aws-generic-attach-file": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cd"
}
},
- "aws-mynah-MynahIconWhite": {
+ "aws-iot-certificate": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ce"
}
},
- "aws-mynah-logo": {
+ "aws-iot-policy": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cf"
}
},
- "aws-redshift-cluster": {
+ "aws-iot-thing": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d0"
}
},
- "aws-redshift-cluster-connected": {
+ "aws-lambda-function": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d1"
}
},
- "aws-redshift-database": {
+ "aws-mynah-MynahIconBlack": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d2"
}
},
- "aws-redshift-redshift-cluster-connected": {
+ "aws-mynah-MynahIconWhite": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d3"
}
},
- "aws-redshift-schema": {
+ "aws-mynah-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d4"
}
},
- "aws-redshift-table": {
+ "aws-redshift-cluster": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d5"
}
},
- "aws-s3-bucket": {
+ "aws-redshift-cluster-connected": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d6"
}
},
- "aws-s3-create-bucket": {
+ "aws-redshift-database": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d7"
}
},
- "aws-schemas-registry": {
+ "aws-redshift-redshift-cluster-connected": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d8"
}
},
- "aws-schemas-schema": {
+ "aws-redshift-schema": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d9"
}
},
- "aws-stepfunctions-preview": {
+ "aws-redshift-table": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1da"
}
+ },
+ "aws-s3-bucket": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1db"
+ }
+ },
+ "aws-s3-create-bucket": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1dc"
+ }
+ },
+ "aws-schemas-registry": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1dd"
+ }
+ },
+ "aws-schemas-schema": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1de"
+ }
+ },
+ "aws-stepfunctions-preview": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1df"
+ }
}
},
"walkthroughs": [
diff --git a/packages/amazonq/src/app/amazonqScan/app.ts b/packages/amazonq/src/app/amazonqScan/app.ts
new file mode 100644
index 00000000000..d639ab6bf2a
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/app.ts
@@ -0,0 +1,87 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import {
+ AmazonQAppInitContext,
+ MessagePublisher,
+ MessageListener,
+ focusAmazonQPanel,
+ DefaultAmazonQAppInitContext,
+} from 'aws-core-vscode/amazonq'
+import { AuthUtil, codeScanState, onDemandFileScanState } from 'aws-core-vscode/codewhisperer'
+import { ScanChatControllerEventEmitters, ChatSessionManager } from 'aws-core-vscode/amazonqScan'
+import { ScanController } from './chat/controller/controller'
+import { AppToWebViewMessageDispatcher } from './chat/views/connector/connector'
+import { Messenger } from './chat/controller/messenger/messenger'
+import { UIMessageListener } from './chat/views/actions/uiMessageListener'
+import { debounce } from 'lodash'
+import { Commands, placeholder } from 'aws-core-vscode/shared'
+
+export function init(appContext: AmazonQAppInitContext) {
+ const scanChatControllerEventEmitters: ScanChatControllerEventEmitters = {
+ authClicked: new vscode.EventEmitter(),
+ tabOpened: new vscode.EventEmitter(),
+ tabClosed: new vscode.EventEmitter(),
+ runScan: new vscode.EventEmitter(),
+ formActionClicked: new vscode.EventEmitter(),
+ errorThrown: new vscode.EventEmitter(),
+ showSecurityScan: new vscode.EventEmitter(),
+ scanStopped: new vscode.EventEmitter(),
+ followUpClicked: new vscode.EventEmitter(),
+ scanProgress: new vscode.EventEmitter(),
+ processResponseBodyLinkClick: new vscode.EventEmitter(),
+ fileClicked: new vscode.EventEmitter(),
+ scanCancelled: new vscode.EventEmitter(),
+ }
+ const dispatcher = new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher())
+ const messenger = new Messenger(dispatcher)
+
+ new ScanController(scanChatControllerEventEmitters, messenger, appContext.onDidChangeAmazonQVisibility.event)
+
+ const scanChatUIInputEventEmitter = new vscode.EventEmitter()
+
+ new UIMessageListener({
+ chatControllerEventEmitters: scanChatControllerEventEmitters,
+ webViewMessageListener: new MessageListener(scanChatUIInputEventEmitter),
+ })
+
+ appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(scanChatUIInputEventEmitter), 'review')
+
+ const debouncedEvent = debounce(async () => {
+ const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected'
+ let authenticatingSessionID = ''
+
+ if (authenticated) {
+ const session = ChatSessionManager.Instance.getSession()
+
+ if (session.isTabOpen() && session.isAuthenticating) {
+ authenticatingSessionID = session.tabID!
+ session.isAuthenticating = false
+ }
+ }
+
+ messenger.sendAuthenticationUpdate(authenticated, [authenticatingSessionID])
+ }, 500)
+
+ AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => {
+ return debouncedEvent()
+ })
+
+ Commands.register('aws.amazonq.security.scan-statusbar', async () => {
+ if (AuthUtil.instance.isConnectionExpired()) {
+ await AuthUtil.instance.notifyReauthenticate()
+ }
+ return focusAmazonQPanel.execute(placeholder, 'amazonq.security.scan').then(() => {
+ DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessagePublisher().publish({
+ sender: 'amazonqCore',
+ command: 'review',
+ })
+ })
+ })
+
+ codeScanState.setChatControllers(scanChatControllerEventEmitters)
+ onDemandFileScanState.setChatControllers(scanChatControllerEventEmitters)
+}
diff --git a/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts b/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts
new file mode 100644
index 00000000000..599271f0f3b
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts
@@ -0,0 +1,358 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * This class is responsible for responding to UI events by calling
+ * the Scan extension.
+ */
+import * as vscode from 'vscode'
+import { AuthController } from 'aws-core-vscode/amazonq'
+import { getLogger, placeholder, i18n, openUrl, fs, TabTypeDataMap, randomUUID } from 'aws-core-vscode/shared'
+import { ScanChatControllerEventEmitters, Session, ChatSessionManager } from 'aws-core-vscode/amazonqScan'
+import {
+ AggregatedCodeScanIssue,
+ AuthUtil,
+ CodeAnalysisScope,
+ codeScanState,
+ isGitRepo,
+ onDemandFileScanState,
+ SecurityScanError,
+ SecurityScanStep,
+ showFileScan,
+ showSecurityScan,
+} from 'aws-core-vscode/codewhisperer'
+import { Messenger, ScanNamedMessages } from './messenger/messenger'
+import MessengerUtils from './messenger/messengerUtils'
+import {
+ cancellingProgressField,
+ fileScanProgressField,
+ projectScanProgressField,
+ ScanAction,
+ scanProgressMessage,
+ scanSummaryMessage,
+} from '../../models/constants'
+import path from 'path'
+
+export class ScanController {
+ private readonly messenger: Messenger
+ private readonly sessionStorage: ChatSessionManager
+ private authController: AuthController
+
+ public constructor(
+ private readonly chatControllerMessageListeners: ScanChatControllerEventEmitters,
+ messenger: Messenger,
+ onDidChangeAmazonQVisibility: vscode.Event
+ ) {
+ this.messenger = messenger
+ this.sessionStorage = ChatSessionManager.Instance
+ this.authController = new AuthController()
+
+ this.chatControllerMessageListeners.tabOpened.event((data) => {
+ return this.tabOpened(data)
+ })
+
+ this.chatControllerMessageListeners.tabClosed.event((data) => {
+ return this.tabClosed(data)
+ })
+
+ this.chatControllerMessageListeners.authClicked.event((data) => {
+ this.authClicked(data)
+ })
+
+ this.chatControllerMessageListeners.runScan.event((data) => {
+ return this.scanInitiated(data)
+ })
+
+ this.chatControllerMessageListeners.formActionClicked.event((data) => {
+ return this.formActionClicked(data)
+ })
+
+ this.chatControllerMessageListeners.errorThrown.event((data) => {
+ return this.handleError(data)
+ })
+
+ this.chatControllerMessageListeners.showSecurityScan.event((data) => {
+ return this.handleScanResults(data)
+ })
+
+ this.chatControllerMessageListeners.scanStopped.event((data) => {
+ return this.handleScanStopped(data)
+ })
+
+ this.chatControllerMessageListeners.followUpClicked.event((data) => {
+ return this.handleFollowUpClicked(data)
+ })
+
+ this.chatControllerMessageListeners.scanProgress.event((data) => {
+ return this.handleScanProgress(data)
+ })
+
+ this.chatControllerMessageListeners.processResponseBodyLinkClick.event((data) => {
+ return this.processLink(data)
+ })
+
+ this.chatControllerMessageListeners.fileClicked.event((data) => {
+ return this.processFileClick(data)
+ })
+
+ this.chatControllerMessageListeners.scanCancelled.event((data) => {
+ return this.handleScanCancelled(data)
+ })
+ }
+
+ private async tabOpened(message: any) {
+ const session: Session = this.sessionStorage.getSession()
+ const tabID = this.sessionStorage.setActiveTab(message.tabID)
+
+ // check if authentication has expired
+ try {
+ getLogger().debug(`Q - Review: Session created with id: ${session.tabID}`)
+
+ const authState = await AuthUtil.instance.getChatAuthState()
+ if (authState.amazonQ !== 'connected') {
+ void this.messenger.sendAuthNeededExceptionMessage(authState, tabID)
+ session.isAuthenticating = true
+ return
+ }
+ } catch (err: any) {
+ this.messenger.sendErrorMessage(err.message, message.tabID)
+ }
+ }
+
+ private async tabClosed(data: any) {
+ this.sessionStorage.removeActiveTab()
+ }
+
+ private authClicked(message: any) {
+ this.authController.handleAuth(message.authType)
+
+ this.messenger.sendAnswer({
+ type: 'answer',
+ tabID: message.tabID,
+ message: 'Follow instructions to re-authenticate ...',
+ })
+
+ // Explicitly ensure the user goes through the re-authenticate flow
+ this.messenger.sendChatInputEnabled(message.tabID, false)
+ }
+
+ private async scanInitiated(message: any) {
+ const session: Session = this.sessionStorage.getSession()
+ try {
+ // check that a project is open
+ const workspaceFolders = vscode.workspace.workspaceFolders
+ if (workspaceFolders === undefined || workspaceFolders.length === 0) {
+ this.messenger.sendChatInputEnabled(message.tabID, false)
+ this.messenger.sendErrorResponse('no-project-found', message.tabID)
+ return
+ }
+ // check that the session is authenticated
+ const authState = await AuthUtil.instance.getChatAuthState()
+ if (authState.amazonQ !== 'connected') {
+ void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID)
+ session.isAuthenticating = true
+ return
+ }
+ this.messenger.sendPromptMessage({
+ tabID: message.tabID,
+ message: i18n('AWS.amazonq.scans.runCodeScan'),
+ })
+ this.messenger.sendCapabilityCard({ tabID: message.tabID })
+ // Displaying types of scans and wait for user input
+ this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.scans.waitingForInput'))
+
+ this.messenger.sendScans(message.tabID, i18n('AWS.amazonq.scans.chooseScan.description'))
+ } catch (e: any) {
+ this.messenger.sendErrorMessage(e.message, message.tabID)
+ }
+ }
+
+ private async formActionClicked(message: any) {
+ const typedAction = MessengerUtils.stringToEnumValue(ScanAction, message.action as any)
+ switch (typedAction) {
+ case ScanAction.STOP_PROJECT_SCAN:
+ codeScanState.setToCancelling()
+ this.messenger.sendUpdatePromptProgress(message.tabID, cancellingProgressField)
+ break
+ case ScanAction.STOP_FILE_SCAN:
+ onDemandFileScanState.setToCancelling()
+ this.messenger.sendUpdatePromptProgress(message.tabID, cancellingProgressField)
+ break
+ }
+ }
+
+ private async handleError(message: {
+ error: SecurityScanError
+ tabID: string
+ scope: CodeAnalysisScope
+ fileName: string | undefined
+ scanUuid?: string
+ }) {
+ if (this.isNotMatchingId(message)) {
+ return
+ }
+ if (message.error.code === 'NoSourceFilesError') {
+ this.messenger.sendScanResults(message.tabID, message.scope, message.fileName, true)
+ this.messenger.sendAnswer({
+ tabID: message.tabID,
+ type: 'answer',
+ canBeVoted: true,
+ message: scanSummaryMessage(message.scope, []),
+ })
+ } else {
+ this.messenger.sendErrorResponse(message.error, message.tabID)
+ }
+ }
+
+ private async handleScanResults(message: {
+ error: Error
+ totalIssues: number
+ tabID: string
+ securityRecommendationCollection: AggregatedCodeScanIssue[]
+ scope: CodeAnalysisScope
+ fileName: string
+ scanUuid?: string
+ }) {
+ if (this.isNotMatchingId(message)) {
+ return
+ }
+ this.messenger.sendScanResults(message.tabID, message.scope, message.fileName, true)
+ this.messenger.sendAnswer({
+ tabID: message.tabID,
+ type: 'answer',
+ canBeVoted: true,
+ message: scanSummaryMessage(message.scope, message.securityRecommendationCollection),
+ })
+ }
+
+ private async handleScanStopped(message: { tabID: string }) {
+ this.messenger.sendUpdatePlaceholder(message.tabID, TabTypeDataMap.review.placeholder)
+ // eslint-disable-next-line unicorn/no-null
+ this.messenger.sendUpdatePromptProgress(message.tabID, null)
+ this.messenger.sendChatInputEnabled(message.tabID, true)
+ }
+
+ private async handleFollowUpClicked(message: any) {
+ switch (message.followUp.type) {
+ case ScanAction.RUN_PROJECT_SCAN: {
+ this.messenger.sendPromptMessage({
+ tabID: message.tabID,
+ message: i18n('AWS.amazonq.scans.projectScan'),
+ })
+
+ const workspaceFolders = vscode.workspace.workspaceFolders ?? []
+ for (const folder of workspaceFolders) {
+ if (!(await isGitRepo(folder.uri))) {
+ this.messenger.sendAnswer({
+ tabID: message.tabID,
+ type: 'answer',
+ message: i18n('AWS.amazonq.scans.noGitRepo'),
+ })
+ break
+ }
+ }
+
+ this.messenger.sendScanInProgress({
+ type: 'answer-stream',
+ tabID: message.tabID,
+ canBeVoted: true,
+ message: scanProgressMessage(0, CodeAnalysisScope.PROJECT),
+ })
+ this.messenger.sendUpdatePromptProgress(message.tabID, projectScanProgressField)
+ const scanUuid = randomUUID()
+ this.sessionStorage.getSession().scanUuid = scanUuid
+ void showSecurityScan.execute(placeholder, 'amazonQChat', true, scanUuid)
+ break
+ }
+ case ScanAction.RUN_FILE_SCAN: {
+ // check if IDE has active file open.
+ const activeEditor = vscode.window.activeTextEditor
+ // also check all open editors and allow this to proceed if only one is open (even if not main focus)
+ const allVisibleEditors = vscode.window.visibleTextEditors
+ const openFileEditors = allVisibleEditors.filter((editor) => editor.document.uri.scheme === 'file')
+ const hasOnlyOneOpenFileSplitView = openFileEditors.length === 1
+ getLogger().debug(`hasOnlyOneOpenSplitView: ${hasOnlyOneOpenFileSplitView}`)
+ // is not a file if the currently highlighted window is not a file, and there is either more than one or no file windows open
+ const isNotFile = activeEditor?.document.uri.scheme !== 'file' && !hasOnlyOneOpenFileSplitView
+ getLogger().debug(`activeEditor: ${activeEditor}, isNotFile: ${isNotFile}`)
+ if (!activeEditor || isNotFile) {
+ this.messenger.sendErrorResponse(
+ isNotFile ? 'invalid-file-type' : 'no-open-file-found',
+ message.tabID
+ )
+ this.messenger.sendUpdatePlaceholder(
+ message.tabID,
+ 'Please open and highlight a source code file in order run a code scan.'
+ )
+ this.messenger.sendChatInputEnabled(message.tabID, true)
+ return
+ }
+ const fileEditorToTest = hasOnlyOneOpenFileSplitView ? openFileEditors[0] : activeEditor
+ const fileName = fileEditorToTest.document.uri.fsPath
+
+ this.messenger.sendPromptMessage({
+ tabID: message.tabID,
+ message: i18n('AWS.amazonq.scans.fileScan'),
+ })
+ this.messenger.sendScanInProgress({
+ type: 'answer-stream',
+ tabID: message.tabID,
+ canBeVoted: true,
+ message: scanProgressMessage(
+ SecurityScanStep.GENERATE_ZIP - 1,
+ CodeAnalysisScope.FILE_ON_DEMAND,
+ fileName ? path.basename(fileName) : undefined
+ ),
+ })
+ this.messenger.sendUpdatePromptProgress(message.tabID, fileScanProgressField)
+ const scanUuid = randomUUID()
+ this.sessionStorage.getSession().scanUuid = scanUuid
+ void showFileScan.execute(placeholder, 'amazonQChat', scanUuid)
+ break
+ }
+ }
+ }
+
+ private async handleScanProgress(message: any) {
+ if (this.isNotMatchingId(message)) {
+ return
+ }
+ this.messenger.sendAnswer({
+ type: 'answer-part',
+ tabID: message.tabID,
+ messageID: ScanNamedMessages.SCAN_SUBMISSION_STATUS_MESSAGE,
+ message: scanProgressMessage(
+ message.step,
+ message.scope,
+ message.fileName ? path.basename(message.fileName) : undefined
+ ),
+ })
+ }
+
+ private processLink(message: any) {
+ void openUrl(vscode.Uri.parse(message.link))
+ }
+
+ private async processFileClick(message: any) {
+ const workspaceFolders = vscode.workspace.workspaceFolders ?? []
+ for (const workspaceFolder of workspaceFolders) {
+ const projectPath = workspaceFolder.uri.fsPath
+ const filePathWithoutProjectName = message.filePath.split('/').slice(1).join('/')
+ const absolutePath = path.join(projectPath, filePathWithoutProjectName)
+ if (await fs.existsFile(absolutePath)) {
+ const document = await vscode.workspace.openTextDocument(absolutePath)
+ await vscode.window.showTextDocument(document)
+ }
+ }
+ }
+
+ private async handleScanCancelled(message: any) {
+ this.messenger.sendAnswer({ type: 'answer', tabID: message.tabID, message: 'Cancelled' })
+ }
+
+ private isNotMatchingId(data: { scanUuid?: string }): boolean {
+ const messagescanUuid = data.scanUuid
+ const currentscanUuid = this.sessionStorage.getSession().scanUuid
+ return Boolean(messagescanUuid) && Boolean(currentscanUuid) && messagescanUuid !== currentscanUuid
+ }
+}
diff --git a/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messenger.ts b/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messenger.ts
new file mode 100644
index 00000000000..18b05e8bb84
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messenger.ts
@@ -0,0 +1,230 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * This class controls the presentation of the various chat bubbles presented by the
+ * Q Security Scans.
+ *
+ * As much as possible, all strings used in the experience should originate here.
+ */
+
+import { AuthFollowUpType, AuthMessageDataMap } from 'aws-core-vscode/amazonq'
+import {
+ FeatureAuthState,
+ SecurityScanError,
+ CodeWhispererConstants,
+ SecurityScanStep,
+ DefaultCodeScanErrorMessage,
+} from 'aws-core-vscode/codewhisperer'
+import { ChatItemButton, ProgressField } from '@aws/mynah-ui/dist/static'
+import { MynahIcons, ChatItemAction } from '@aws/mynah-ui'
+import { ChatItemType } from 'aws-core-vscode/amazonq'
+import {
+ AppToWebViewMessageDispatcher,
+ AuthNeededException,
+ AuthenticationUpdateMessage,
+ CapabilityCardMessage,
+ ChatInputEnabledMessage,
+ ChatMessage,
+ ChatPrompt,
+ ErrorMessage,
+ UpdatePlaceholderMessage,
+ UpdatePromptProgressMessage,
+} from '../../views/connector/connector'
+import { i18n } from 'aws-core-vscode/shared'
+import { ScanAction, scanProgressMessage } from '../../../models/constants'
+import path from 'path'
+
+export type UnrecoverableErrorType = 'no-project-found' | 'no-open-file-found' | 'invalid-file-type'
+
+export enum ScanNamedMessages {
+ SCAN_SUBMISSION_STATUS_MESSAGE = 'scanSubmissionMessage',
+}
+
+export class Messenger {
+ public constructor(private readonly dispatcher: AppToWebViewMessageDispatcher) {}
+
+ public sendAnswer(params: {
+ message?: string
+ type: ChatItemType
+ tabID: string
+ messageID?: string
+ followUps?: ChatItemAction[]
+ canBeVoted?: boolean
+ }) {
+ this.dispatcher.sendChatMessage(
+ new ChatMessage(
+ {
+ message: params.message,
+ messageType: params.type,
+ messageId: params.messageID,
+ followUps: params.followUps,
+ canBeVoted: true,
+ },
+ params.tabID
+ )
+ )
+ }
+
+ public sendChatInputEnabled(tabID: string, enabled: boolean) {
+ this.dispatcher.sendChatInputEnabled(new ChatInputEnabledMessage(tabID, enabled))
+ }
+
+ public sendUpdatePlaceholder(tabID: string, newPlaceholder: string) {
+ this.dispatcher.sendUpdatePlaceholder(new UpdatePlaceholderMessage(tabID, newPlaceholder))
+ }
+
+ public sendUpdatePromptProgress(tabID: string, progressField: ProgressField | null) {
+ this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, progressField))
+ }
+
+ public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) {
+ let authType: AuthFollowUpType = 'full-auth'
+ let message = AuthMessageDataMap[authType].message
+
+ switch (credentialState.amazonQ) {
+ case 'disconnected':
+ authType = 'full-auth'
+ message = AuthMessageDataMap[authType].message
+ break
+ case 'unsupported':
+ authType = 'use-supported-auth'
+ message = AuthMessageDataMap[authType].message
+ break
+ case 'expired':
+ authType = 're-auth'
+ message = AuthMessageDataMap[authType].message
+ break
+ }
+
+ this.dispatcher.sendAuthNeededExceptionMessage(new AuthNeededException(message, authType, tabID))
+ }
+
+ public sendAuthenticationUpdate(scanEnabled: boolean, authenticatingTabIDs: string[]) {
+ this.dispatcher.sendAuthenticationUpdate(new AuthenticationUpdateMessage(scanEnabled, authenticatingTabIDs))
+ }
+
+ public sendScanInProgress(params: {
+ message?: string
+ type: ChatItemType
+ tabID: string
+ messageID?: string
+ canBeVoted?: boolean
+ }) {
+ this.dispatcher.sendChatMessage(
+ new ChatMessage(
+ {
+ message: params.message,
+ messageType: params.type,
+ messageId: ScanNamedMessages.SCAN_SUBMISSION_STATUS_MESSAGE,
+ canBeVoted: params.canBeVoted,
+ },
+ params.tabID
+ )
+ )
+ }
+
+ public sendErrorMessage(errorMessage: string, tabID: string) {
+ this.dispatcher.sendErrorMessage(
+ new ErrorMessage(CodeWhispererConstants.genericErrorMessage, errorMessage, tabID)
+ )
+ }
+
+ public sendScanResults(
+ tabID: string,
+ scope: CodeWhispererConstants.CodeAnalysisScope,
+ fileName?: string,
+ canBeVoted?: boolean
+ ) {
+ this.dispatcher.sendChatMessage(
+ new ChatMessage(
+ {
+ message: scanProgressMessage(
+ SecurityScanStep.PROCESS_SCAN_RESULTS + 1,
+ scope,
+ fileName ? path.basename(fileName) : undefined
+ ),
+ messageType: 'answer-part',
+ messageId: ScanNamedMessages.SCAN_SUBMISSION_STATUS_MESSAGE,
+ canBeVoted: canBeVoted,
+ },
+ tabID
+ )
+ )
+ }
+
+ public sendErrorResponse(error: UnrecoverableErrorType | SecurityScanError, tabID: string) {
+ let message = DefaultCodeScanErrorMessage
+ const buttons: ChatItemButton[] = []
+ if (typeof error === 'string') {
+ switch (error) {
+ case 'no-project-found': {
+ // TODO: If required we can add "Open the Projects" button in the chat panel.
+ message = CodeWhispererConstants.noOpenProjectsFound
+ break
+ }
+ case 'no-open-file-found': {
+ message = CodeWhispererConstants.noOpenFileFound
+ break
+ }
+ case 'invalid-file-type': {
+ message = CodeWhispererConstants.invalidFileTypeChatMessage
+ break
+ }
+ }
+ } else if (error.code === 'NoActiveFileError') {
+ message = CodeWhispererConstants.noOpenFileFound
+ } else if (error.code === 'ContentLengthError') {
+ message = CodeWhispererConstants.ProjectSizeExceededErrorMessage
+ } else if (error.code === 'NoSourceFilesError') {
+ message = CodeWhispererConstants.noSourceFilesErrorMessage
+ } else {
+ message = error.customerFacingMessage
+ }
+ this.dispatcher.sendChatMessage(
+ new ChatMessage(
+ {
+ message,
+ messageType: 'answer',
+ buttons,
+ },
+ tabID
+ )
+ )
+ }
+
+ public sendScans(tabID: string, message: string) {
+ const followUps: ChatItemAction[] = []
+ followUps.push({
+ pillText: i18n('AWS.amazonq.scans.projectScan'),
+ status: 'info',
+ icon: 'folder' as MynahIcons,
+ type: ScanAction.RUN_PROJECT_SCAN,
+ })
+ followUps.push({
+ pillText: i18n('AWS.amazonq.scans.fileScan'),
+ status: 'info',
+ icon: 'file' as MynahIcons,
+ type: ScanAction.RUN_FILE_SCAN,
+ })
+ this.dispatcher.sendChatMessage(
+ new ChatMessage(
+ {
+ message,
+ messageType: 'ai-prompt',
+ followUps,
+ },
+ tabID
+ )
+ )
+ }
+
+ // This function shows selected scan type in the chat panel as a user input
+ public sendPromptMessage(params: { tabID: string; message: string }) {
+ this.dispatcher.sendPromptMessage(new ChatPrompt(params.message, params.tabID))
+ }
+
+ public sendCapabilityCard(params: { tabID: string }) {
+ this.dispatcher.sendChatMessage(new CapabilityCardMessage(params.tabID))
+ }
+}
diff --git a/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messengerUtils.ts b/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messengerUtils.ts
new file mode 100644
index 00000000000..67351c3eb6e
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messengerUtils.ts
@@ -0,0 +1,19 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ */
+//TODO: Refactor the common functionality between Transform, FeatureDev, CWSPRChat, Scan and UTG to a new Folder.
+
+export default class MessengerUtils {
+ static stringToEnumValue = (
+ enumObject: T,
+ value: `${T[K]}`
+ ): T[K] => {
+ if (Object.values(enumObject).includes(value)) {
+ return value as unknown as T[K]
+ } else {
+ throw new Error('Value provided was not found in Enum')
+ }
+ }
+}
diff --git a/packages/amazonq/src/app/amazonqScan/chat/session/session.ts b/packages/amazonq/src/app/amazonqScan/chat/session/session.ts
new file mode 100644
index 00000000000..1ca7e8d7362
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/chat/session/session.ts
@@ -0,0 +1,25 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export enum ConversationState {
+ IDLE,
+ JOB_SUBMITTED,
+}
+
+export class Session {
+ // Used to keep track of whether or not the current session is currently authenticating/needs authenticating
+ public isAuthenticating: boolean = false
+
+ // A tab may or may not be currently open
+ public tabID: string | undefined
+
+ public conversationState: ConversationState = ConversationState.IDLE
+
+ constructor() {}
+
+ public isTabOpen(): boolean {
+ return this.tabID !== undefined
+ }
+}
diff --git a/packages/amazonq/src/app/amazonqScan/chat/storages/chatSession.ts b/packages/amazonq/src/app/amazonqScan/chat/storages/chatSession.ts
new file mode 100644
index 00000000000..b7df6eb0cc6
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/chat/storages/chatSession.ts
@@ -0,0 +1,54 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ */
+
+import { Session } from '../session/session'
+
+export class SessionNotFoundError extends Error {}
+
+export class ChatSessionManager {
+ private static _instance: ChatSessionManager
+ private activeSession: Session | undefined
+
+ constructor() {}
+
+ public static get Instance() {
+ return this._instance || (this._instance = new this())
+ }
+
+ private createSession(): Session {
+ this.activeSession = new Session()
+ return this.activeSession
+ }
+
+ public getSession(): Session {
+ if (this.activeSession === undefined) {
+ return this.createSession()
+ }
+
+ return this.activeSession
+ }
+
+ public setActiveTab(tabID: string): string {
+ if (this.activeSession !== undefined) {
+ if (!this.activeSession.isTabOpen()) {
+ this.activeSession.tabID = tabID
+ return tabID
+ }
+ return this.activeSession.tabID!
+ }
+
+ throw new SessionNotFoundError()
+ }
+
+ public removeActiveTab(): void {
+ if (this.activeSession !== undefined) {
+ if (this.activeSession.isTabOpen()) {
+ this.activeSession.tabID = undefined
+ return
+ }
+ }
+ }
+}
diff --git a/packages/amazonq/src/app/amazonqScan/chat/views/actions/uiMessageListener.ts b/packages/amazonq/src/app/amazonqScan/chat/views/actions/uiMessageListener.ts
new file mode 100644
index 00000000000..ede78d1a0bf
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/chat/views/actions/uiMessageListener.ts
@@ -0,0 +1,115 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { MessageListener, ExtensionMessage } from 'aws-core-vscode/amazonq'
+import { ScanChatControllerEventEmitters } from 'aws-core-vscode/amazonqScan'
+
+type UIMessage = ExtensionMessage & {
+ tabID?: string
+}
+
+export interface UIMessageListenerProps {
+ readonly chatControllerEventEmitters: ScanChatControllerEventEmitters
+ readonly webViewMessageListener: MessageListener
+}
+
+export class UIMessageListener {
+ private scanControllerEventsEmitters: ScanChatControllerEventEmitters | undefined
+ private webViewMessageListener: MessageListener
+
+ constructor(props: UIMessageListenerProps) {
+ this.scanControllerEventsEmitters = props.chatControllerEventEmitters
+ this.webViewMessageListener = props.webViewMessageListener
+
+ // Now we are listening to events that get sent from amazonq/webview/actions/actionListener (e.g. the tab)
+ this.webViewMessageListener.onMessage((msg) => {
+ this.handleMessage(msg)
+ })
+ }
+
+ private handleMessage(msg: ExtensionMessage) {
+ switch (msg.command) {
+ case 'new-tab-was-created':
+ this.tabOpened(msg)
+ break
+ case 'tab-was-removed':
+ this.tabClosed(msg)
+ break
+ case 'auth-follow-up-was-clicked':
+ this.authClicked(msg)
+ break
+ case 'review':
+ this.scan(msg)
+ break
+ case 'form-action-click':
+ this.formActionClicked(msg)
+ break
+ case 'follow-up-was-clicked':
+ this.followUpClicked(msg)
+ break
+ case 'response-body-link-click':
+ this.processResponseBodyLinkClick(msg)
+ break
+ case 'file-click':
+ this.processFileClick(msg)
+ break
+ }
+ }
+
+ private scan(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.runScan.fire({
+ tabID: msg.tabID,
+ })
+ }
+
+ private formActionClicked(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.formActionClicked.fire({
+ ...msg,
+ })
+ }
+
+ private tabOpened(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.tabOpened.fire({
+ tabID: msg.tabID,
+ })
+ }
+
+ private tabClosed(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.tabClosed.fire({
+ tabID: msg.tabID,
+ })
+ }
+
+ private authClicked(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.authClicked.fire({
+ tabID: msg.tabID,
+ authType: msg.authType,
+ })
+ }
+
+ private followUpClicked(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.followUpClicked.fire({
+ followUp: msg.followUp,
+ tabID: msg.tabID,
+ })
+ }
+
+ private processResponseBodyLinkClick(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.processResponseBodyLinkClick.fire({
+ command: msg.command,
+ messageId: msg.messageId,
+ tabID: msg.tabID,
+ link: msg.link,
+ })
+ }
+
+ private processFileClick(msg: UIMessage) {
+ this.scanControllerEventsEmitters?.fileClicked.fire({
+ tabID: msg.tabID,
+ messageId: msg.messageId,
+ filePath: msg.filePath,
+ })
+ }
+}
diff --git a/packages/amazonq/src/app/amazonqScan/chat/views/connector/connector.ts b/packages/amazonq/src/app/amazonqScan/chat/views/connector/connector.ts
new file mode 100644
index 00000000000..c906a401f91
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/chat/views/connector/connector.ts
@@ -0,0 +1,192 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { AuthFollowUpType, MessagePublisher, ChatItemType } from 'aws-core-vscode/amazonq'
+import { ScanMessageType } from 'aws-core-vscode/amazonqScan'
+import { ChatItemButton, ProgressField, ChatItemAction, ChatItemContent } from '@aws/mynah-ui/dist/static'
+import { scanChat } from '../../../models/constants'
+import { MynahIcons } from '@aws/mynah-ui'
+
+class UiMessage {
+ readonly time: number = Date.now()
+ readonly sender: string = scanChat
+ readonly type: ScanMessageType = 'chatMessage'
+ readonly status: string = 'info'
+
+ public constructor(protected tabID: string) {}
+}
+
+export class AuthenticationUpdateMessage {
+ readonly time: number = Date.now()
+ readonly sender: string = scanChat
+ readonly type: ScanMessageType = 'authenticationUpdateMessage'
+
+ constructor(
+ readonly scanEnabled: boolean,
+ readonly authenticatingTabIDs: string[]
+ ) {}
+}
+
+export class AuthNeededException extends UiMessage {
+ override type: ScanMessageType = 'authNeededException'
+
+ constructor(
+ readonly message: string,
+ readonly authType: AuthFollowUpType,
+ tabID: string
+ ) {
+ super(tabID)
+ }
+}
+
+export interface ChatMessageProps {
+ readonly message: string | undefined
+ readonly messageId?: string | undefined
+ readonly messageType: ChatItemType
+ readonly canBeVoted?: boolean
+ readonly buttons?: ChatItemButton[]
+ readonly followUps?: ChatItemAction[] | undefined
+ readonly informationCard?: ChatItemContent['informationCard']
+ readonly fileList?: ChatItemContent['fileList']
+}
+
+export class ChatMessage extends UiMessage {
+ readonly message: string | undefined
+ readonly messageId?: string | undefined
+ readonly messageType: ChatItemType
+ readonly canBeVoted?: boolean
+ readonly buttons: ChatItemButton[]
+ readonly followUps: ChatItemAction[] | undefined
+ readonly informationCard: ChatItemContent['informationCard']
+ readonly fileList: ChatItemContent['fileList']
+ override type: ScanMessageType = 'chatMessage'
+
+ constructor(props: ChatMessageProps, tabID: string) {
+ super(tabID)
+ this.message = props.message
+ this.messageType = props.messageType
+ this.buttons = props.buttons || []
+ this.messageId = props.messageId || undefined
+ this.followUps = props.followUps
+ this.informationCard = props.informationCard || undefined
+ this.fileList = props.fileList
+ this.canBeVoted = props.canBeVoted || undefined
+ }
+}
+
+export class CapabilityCardMessage extends ChatMessage {
+ constructor(tabID: string) {
+ super(
+ {
+ message: '',
+ messageType: 'answer',
+ informationCard: {
+ title: '/review',
+ description: 'Included in your Q Developer subscription',
+ content: {
+ body: `I can review your workspace for vulnerabilities and issues.
+
+After you begin a review, I will:
+1. Review all relevant code in your workspace or your current file
+2. Provide a list of issues for your review
+
+You can then investigate, fix, or ignore issues.
+
+To learn more, check out our [User Guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/security-scans.html).`,
+ },
+ icon: 'bug' as MynahIcons,
+ },
+ },
+ tabID
+ )
+ }
+}
+
+export class ChatInputEnabledMessage extends UiMessage {
+ override type: ScanMessageType = 'chatInputEnabledMessage'
+
+ constructor(
+ tabID: string,
+ readonly enabled: boolean
+ ) {
+ super(tabID)
+ }
+}
+
+export class UpdatePlaceholderMessage extends UiMessage {
+ readonly newPlaceholder: string
+ override type: ScanMessageType = 'updatePlaceholderMessage'
+
+ constructor(tabID: string, newPlaceholder: string) {
+ super(tabID)
+ this.newPlaceholder = newPlaceholder
+ }
+}
+
+export class UpdatePromptProgressMessage extends UiMessage {
+ readonly progressField: ProgressField | null
+ override type: ScanMessageType = 'updatePromptProgress'
+ constructor(tabID: string, progressField: ProgressField | null) {
+ super(tabID)
+ this.progressField = progressField
+ }
+}
+
+export class ErrorMessage extends UiMessage {
+ override type: ScanMessageType = 'errorMessage'
+ constructor(
+ readonly title: string,
+ readonly message: string,
+ tabID: string
+ ) {
+ super(tabID)
+ }
+}
+
+export class ChatPrompt extends UiMessage {
+ readonly message: string | undefined
+ readonly messageType = 'system-prompt'
+ override type: ScanMessageType = 'chatPrompt'
+ constructor(message: string | undefined, tabID: string) {
+ super(tabID)
+ this.message = message
+ }
+}
+
+export class AppToWebViewMessageDispatcher {
+ constructor(private readonly appsToWebViewMessagePublisher: MessagePublisher) {}
+
+ public sendChatMessage(message: ChatMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendUpdatePlaceholder(message: UpdatePlaceholderMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendUpdatePromptProgress(message: UpdatePromptProgressMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendAuthenticationUpdate(message: AuthenticationUpdateMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendAuthNeededExceptionMessage(message: AuthNeededException) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendChatInputEnabled(message: ChatInputEnabledMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendErrorMessage(message: ErrorMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendPromptMessage(message: ChatPrompt) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+}
diff --git a/packages/amazonq/src/app/amazonqScan/index.ts b/packages/amazonq/src/app/amazonqScan/index.ts
new file mode 100644
index 00000000000..c195193740b
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/index.ts
@@ -0,0 +1,7 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { default as MessengerUtils } from './chat/controller/messenger/messengerUtils'
+export { init as scanChatAppInit } from './app'
diff --git a/packages/amazonq/src/app/amazonqScan/models/constants.ts b/packages/amazonq/src/app/amazonqScan/models/constants.ts
new file mode 100644
index 00000000000..93e815884e1
--- /dev/null
+++ b/packages/amazonq/src/app/amazonqScan/models/constants.ts
@@ -0,0 +1,99 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { ProgressField, MynahIcons, ChatItemButton } from '@aws/mynah-ui'
+import { AggregatedCodeScanIssue, CodeAnalysisScope, SecurityScanStep, severities } from 'aws-core-vscode/codewhisperer'
+import { i18n } from 'aws-core-vscode/shared'
+
+// For uniquely identifiying which chat messages should be routed to Scan
+export const scanChat = 'scanChat'
+
+export enum ScanAction {
+ RUN_PROJECT_SCAN = 'runProjectScan',
+ RUN_FILE_SCAN = 'runFileScan',
+ STOP_PROJECT_SCAN = 'stopProjectScan',
+ STOP_FILE_SCAN = 'stopFileScan',
+}
+
+export const cancelFileScanButton: ChatItemButton = {
+ id: ScanAction.STOP_FILE_SCAN,
+ text: i18n('AWS.generic.cancel'),
+ icon: 'cancel' as MynahIcons,
+}
+
+export const cancelProjectScanButton: ChatItemButton = {
+ ...cancelFileScanButton,
+ id: ScanAction.STOP_PROJECT_SCAN,
+}
+
+export const fileScanProgressField: ProgressField = {
+ status: 'default',
+ text: i18n('AWS.amazonq.scans.fileScanInProgress'),
+ value: -1,
+ actions: [cancelFileScanButton],
+}
+
+export const projectScanProgressField: ProgressField = {
+ ...fileScanProgressField,
+ text: i18n('AWS.amazonq.scans.projectScanInProgress'),
+ actions: [cancelProjectScanButton],
+}
+
+export const cancellingProgressField: ProgressField = {
+ status: 'warning',
+ text: i18n('AWS.generic.cancelling'),
+ value: -1,
+ actions: [],
+}
+
+const checkIcons = {
+ wait: '☐',
+ current: '☐',
+ done: '☑',
+}
+export const scanProgressMessage = (
+ currentStep: SecurityScanStep,
+ scope: CodeAnalysisScope,
+ fileName?: string
+) => `Okay, I'm reviewing ${scope === CodeAnalysisScope.PROJECT ? 'your project' : fileName ? `\`${fileName}\`` : 'your file'} for code issues.
+
+This may take a few minutes. I'll share my progress here.
+
+${getIconForStep(SecurityScanStep.CREATE_SCAN_JOB, currentStep)} Initiating code review
+
+${getIconForStep(SecurityScanStep.POLL_SCAN_STATUS, currentStep)} Reviewing your code
+
+${getIconForStep(SecurityScanStep.PROCESS_SCAN_RESULTS, currentStep)} Processing review results
+`
+
+export const scanSummaryMessage = (
+ scope: CodeAnalysisScope,
+ securityRecommendationCollection: AggregatedCodeScanIssue[]
+) => {
+ const severityCounts = securityRecommendationCollection.reduce(
+ (accumulator, current) => ({
+ ...Object.fromEntries(
+ severities.map((severity) => [
+ severity,
+ accumulator[severity] +
+ current.issues.filter((issue) => issue.severity === severity && issue.visible).length,
+ ])
+ ),
+ }),
+ Object.fromEntries(severities.map((severity) => [severity, 0]))
+ )
+ return `I completed the code review. I found the following issues in your ${scope === CodeAnalysisScope.PROJECT ? 'workspace' : 'file'}:
+${Object.entries(severityCounts)
+ .map(([severity, count]) => `- ${severity}: \`${count} ${count === 1 ? 'issue' : 'issues'}\``)
+ .join('\n')}
+`
+}
+
+const getIconForStep = (targetStep: number, currentStep: number) => {
+ return currentStep === targetStep
+ ? checkIcons.current
+ : currentStep > targetStep
+ ? checkIcons.done
+ : checkIcons.wait
+}
diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts
index 5edd9affdcd..f7b3f9a0fa5 100644
--- a/packages/amazonq/src/app/chat/activation.ts
+++ b/packages/amazonq/src/app/chat/activation.ts
@@ -9,6 +9,7 @@ import { telemetry } from 'aws-core-vscode/telemetry'
import { AuthUtil, CodeWhispererSettings } from 'aws-core-vscode/codewhisperer'
import { Commands, placeholder, funcUtil } from 'aws-core-vscode/shared'
import * as amazonq from 'aws-core-vscode/amazonq'
+import { scanChatAppInit } from '../amazonqScan'
import { init as inlineChatInit } from '../../inlineChat/app'
export async function activate(context: ExtensionContext) {
@@ -69,6 +70,9 @@ function registerApps(appInitContext: amazonq.AmazonQAppInitContext, context: Ex
amazonq.cwChatAppInit(appInitContext)
amazonq.featureDevChatAppInit(appInitContext)
amazonq.gumbyChatAppInit(appInitContext)
+ amazonq.testChatAppInit(appInitContext)
+ scanChatAppInit(appInitContext)
+ amazonq.docChatAppInit(appInitContext)
inlineChatInit(context)
}
diff --git a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts b/packages/amazonq/test/e2e/amazonq/featureDev.test.ts
index a96da96c199..5b830834743 100644
--- a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts
+++ b/packages/amazonq/test/e2e/amazonq/featureDev.test.ts
@@ -9,7 +9,8 @@ import sinon from 'sinon'
import { registerAuthHook, using } from 'aws-core-vscode/test'
import { loginToIdC } from './utils/setup'
import { Messenger } from './framework/messenger'
-import { FollowUpTypes, examples } from 'aws-core-vscode/amazonqFeatureDev'
+import { examples } from 'aws-core-vscode/amazonqFeatureDev'
+import { FollowUpTypes } from 'aws-core-vscode/amazonq'
import { sleep } from 'aws-core-vscode/shared'
describe('Amazon Q Feature Dev', function () {
diff --git a/packages/amazonq/test/e2e/amazonq/framework/messenger.ts b/packages/amazonq/test/e2e/amazonq/framework/messenger.ts
index 28b34aa3bdb..353bd3b4a9c 100644
--- a/packages/amazonq/test/e2e/amazonq/framework/messenger.ts
+++ b/packages/amazonq/test/e2e/amazonq/framework/messenger.ts
@@ -6,7 +6,7 @@
import assert from 'assert'
import { MynahUI, MynahUIProps, MynahUIDataModel } from '@aws/mynah-ui'
import { waitUntil } from 'aws-core-vscode/shared'
-import { FollowUpTypes } from 'aws-core-vscode/amazonqFeatureDev'
+import { FollowUpTypes } from 'aws-core-vscode/amazonq'
export interface MessengerOptions {
waitIntervalInMs?: number
diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts
index 29a78c552ce..4c6073114f8 100644
--- a/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts
+++ b/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts
@@ -3,18 +3,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as assert from 'assert'
-
-import { Messenger, ChatSessionStorage } from 'aws-core-vscode/amazonqFeatureDev'
+import { FeatureDevChatSessionStorage } from 'aws-core-vscode/amazonqFeatureDev'
+import { Messenger } from 'aws-core-vscode/amazonq'
import { createMessenger } from 'aws-core-vscode/test'
describe('chatSession', () => {
const tabID = '1234'
- let chatStorage: ChatSessionStorage
+ let chatStorage: FeatureDevChatSessionStorage
let messenger: Messenger
beforeEach(() => {
messenger = createMessenger()
- chatStorage = new ChatSessionStorage(messenger)
+ chatStorage = new FeatureDevChatSessionStorage(messenger)
})
it('locks getSession', async () => {
diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts
index b79f0c4bf4f..a7a5d831f67 100644
--- a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts
+++ b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts
@@ -18,7 +18,8 @@ import {
sessionWriteFile,
assertTelemetry,
} from 'aws-core-vscode/test'
-import { CurrentWsFolders, CodeGenState, FeatureDevClient, Messenger } from 'aws-core-vscode/amazonqFeatureDev'
+import { CurrentWsFolders, CodeGenState, FeatureDevClient, featureDevScheme } from 'aws-core-vscode/amazonqFeatureDev'
+import { Messenger } from 'aws-core-vscode/amazonq'
import path from 'path'
import { fs } from 'aws-core-vscode/shared'
@@ -36,7 +37,7 @@ describe('session', () => {
describe('preloader', () => {
it('emits start chat telemetry', async () => {
- const session = await createSession({ messenger, conversationID })
+ const session = await createSession({ messenger, conversationID, scheme: featureDevScheme })
await session.preloader('implement twosum in typescript')
@@ -63,7 +64,7 @@ describe('session', () => {
const tabID = '123'
const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders
workspaceFolderUriFsPath = controllerSetup.workspaceFolder.uri.fsPath
- uri = generateVirtualMemoryUri(uploadID, notRejectedFileName)
+ uri = generateVirtualMemoryUri(uploadID, notRejectedFileName, featureDevScheme)
const testConfig = {
conversationId: conversationID,
@@ -90,7 +91,7 @@ describe('session', () => {
relativePath: 'rejectedFile.js',
fileContent: 'rejectedFileContent',
rejected: true,
- virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'rejectedFile.js'),
+ virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'rejectedFile.js', featureDevScheme),
workspaceFolder: controllerSetup.workspaceFolder,
changeApplied: false,
},
@@ -101,7 +102,12 @@ describe('session', () => {
0,
{}
)
- const session = await createSession({ messenger, sessionState: codeGenState, conversationID })
+ const session = await createSession({
+ messenger,
+ sessionState: codeGenState,
+ conversationID,
+ scheme: featureDevScheme,
+ })
encodedContent = new TextEncoder().encode(notRejectedFileContent)
await sessionRegisterProvider(session, uri, encodedContent)
return session
diff --git a/packages/amazonq/test/unit/codewhisperer/models/model.test.ts b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts
new file mode 100644
index 00000000000..ae7114a22c8
--- /dev/null
+++ b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts
@@ -0,0 +1,73 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert'
+import sinon from 'sinon'
+import { SecurityIssueFilters, SecurityTreeViewFilterState } from 'aws-core-vscode/codewhisperer'
+import { globals } from 'aws-core-vscode/shared'
+
+describe('model', function () {
+ describe('SecurityTreeViewFilterState', function () {
+ let securityTreeViewFilterState: SecurityTreeViewFilterState
+
+ beforeEach(function () {
+ securityTreeViewFilterState = SecurityTreeViewFilterState.instance
+ })
+
+ afterEach(function () {
+ sinon.restore()
+ })
+
+ it('should get the state', async function () {
+ const state: SecurityIssueFilters = {
+ severity: {
+ Critical: false,
+ High: true,
+ Medium: true,
+ Low: true,
+ Info: true,
+ },
+ }
+ await globals.globalState.update('aws.amazonq.securityIssueFilters', state)
+ assert.deepStrictEqual(securityTreeViewFilterState.getState(), state)
+ })
+
+ it('should set the state', async function () {
+ await globals.globalState.update('aws.amazonq.securityIssueFilters', {
+ severity: {
+ Critical: true,
+ High: true,
+ Medium: true,
+ Low: true,
+ Info: true,
+ },
+ } satisfies SecurityIssueFilters)
+ const state = {
+ severity: {
+ Critical: false,
+ High: true,
+ Medium: true,
+ Low: true,
+ Info: true,
+ },
+ } satisfies SecurityIssueFilters
+ await securityTreeViewFilterState.setState(state)
+ assert.deepStrictEqual(globals.globalState.get('aws.amazonq.securityIssueFilters'), state)
+ })
+
+ it('should get hidden severities', async function () {
+ await globals.globalState.update('aws.amazonq.securityIssueFilters', {
+ severity: {
+ Critical: true,
+ High: false,
+ Medium: true,
+ Low: false,
+ Info: true,
+ },
+ } satisfies SecurityIssueFilters)
+ const hiddenSeverities = securityTreeViewFilterState.getHiddenSeverities()
+ assert.deepStrictEqual(hiddenSeverities, ['High', 'Low'])
+ })
+ })
+})
diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueCodeActionProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueCodeActionProvider.test.ts
index 3015fef6ff1..3ac473cbcca 100644
--- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueCodeActionProvider.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueCodeActionProvider.test.ts
@@ -6,15 +6,17 @@
import * as vscode from 'vscode'
import { createCodeActionContext, createCodeScanIssue, createMockDocument } from 'aws-core-vscode/test'
import assert from 'assert'
-import { SecurityIssueCodeActionProvider } from 'aws-core-vscode/codewhisperer'
+import { SecurityIssueCodeActionProvider, SecurityIssueProvider } from 'aws-core-vscode/codewhisperer'
describe('securityIssueCodeActionProvider', () => {
+ let securityIssueProvider: SecurityIssueProvider
let securityIssueCodeActionProvider: SecurityIssueCodeActionProvider
let mockDocument: vscode.TextDocument
let context: vscode.CodeActionContext
let token: vscode.CancellationTokenSource
beforeEach(() => {
+ securityIssueProvider = SecurityIssueProvider.instance
securityIssueCodeActionProvider = new SecurityIssueCodeActionProvider()
mockDocument = createMockDocument('def two_sum(nums, target):\nfor', 'test.py', 'python')
context = createCodeActionContext()
@@ -22,7 +24,7 @@ describe('securityIssueCodeActionProvider', () => {
})
it('should provide quick fix for each issue that has a suggested fix', () => {
- securityIssueCodeActionProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: mockDocument.fileName,
issues: [createCodeScanIssue({ title: 'issue 1' }), createCodeScanIssue({ title: 'issue 2' })],
@@ -31,23 +33,27 @@ describe('securityIssueCodeActionProvider', () => {
const range = new vscode.Range(0, 0, 0, 0)
const actual = securityIssueCodeActionProvider.provideCodeActions(mockDocument, range, context, token.token)
- assert.strictEqual(actual.length, 6)
+ assert.strictEqual(actual.length, 10)
assert.strictEqual(actual[0].title, 'Amazon Q: Fix "issue 1"')
assert.strictEqual(actual[0].kind, vscode.CodeActionKind.QuickFix)
assert.strictEqual(actual[1].title, 'Amazon Q: View details for "issue 1"')
assert.strictEqual(actual[1].kind, vscode.CodeActionKind.QuickFix)
assert.strictEqual(actual[2].title, 'Amazon Q: Explain "issue 1"')
assert.strictEqual(actual[2].kind, vscode.CodeActionKind.QuickFix)
- assert.strictEqual(actual[3].title, 'Amazon Q: Fix "issue 2"')
+ assert.strictEqual(actual[3].title, 'Amazon Q: Ignore this "issue 1" issue')
assert.strictEqual(actual[3].kind, vscode.CodeActionKind.QuickFix)
- assert.strictEqual(actual[4].title, 'Amazon Q: View details for "issue 2"')
+ assert.strictEqual(actual[4].title, 'Amazon Q: Ignore all "issue 1" issues')
assert.strictEqual(actual[4].kind, vscode.CodeActionKind.QuickFix)
- assert.strictEqual(actual[5].title, 'Amazon Q: Explain "issue 2"')
+ assert.strictEqual(actual[5].title, 'Amazon Q: Fix "issue 2"')
assert.strictEqual(actual[5].kind, vscode.CodeActionKind.QuickFix)
+ assert.strictEqual(actual[6].title, 'Amazon Q: View details for "issue 2"')
+ assert.strictEqual(actual[6].kind, vscode.CodeActionKind.QuickFix)
+ assert.strictEqual(actual[7].title, 'Amazon Q: Explain "issue 2"')
+ assert.strictEqual(actual[7].kind, vscode.CodeActionKind.QuickFix)
})
it('should not provide quick fix if the issue does not have a suggested fix', () => {
- securityIssueCodeActionProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: mockDocument.fileName,
issues: [createCodeScanIssue({ title: 'issue 1', suggestedFixes: [] })],
@@ -56,15 +62,19 @@ describe('securityIssueCodeActionProvider', () => {
const range = new vscode.Range(0, 0, 0, 0)
const actual = securityIssueCodeActionProvider.provideCodeActions(mockDocument, range, context, token.token)
- assert.strictEqual(actual.length, 2)
+ assert.strictEqual(actual.length, 4)
assert.strictEqual(actual[0].title, 'Amazon Q: View details for "issue 1"')
assert.strictEqual(actual[0].kind, vscode.CodeActionKind.QuickFix)
assert.strictEqual(actual[1].title, 'Amazon Q: Explain "issue 1"')
assert.strictEqual(actual[1].kind, vscode.CodeActionKind.QuickFix)
+ assert.strictEqual(actual[2].title, 'Amazon Q: Ignore this "issue 1" issue')
+ assert.strictEqual(actual[2].kind, vscode.CodeActionKind.QuickFix)
+ assert.strictEqual(actual[3].title, 'Amazon Q: Ignore all "issue 1" issues')
+ assert.strictEqual(actual[3].kind, vscode.CodeActionKind.QuickFix)
})
it('should skip issues not in the current file', () => {
- securityIssueCodeActionProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: 'some/path',
issues: [createCodeScanIssue({ title: 'issue 1' })],
@@ -77,9 +87,24 @@ describe('securityIssueCodeActionProvider', () => {
const range = new vscode.Range(0, 0, 0, 0)
const actual = securityIssueCodeActionProvider.provideCodeActions(mockDocument, range, context, token.token)
- assert.strictEqual(actual.length, 3)
+ assert.strictEqual(actual.length, 5)
assert.strictEqual(actual[0].title, 'Amazon Q: Fix "issue 2"')
assert.strictEqual(actual[1].title, 'Amazon Q: View details for "issue 2"')
assert.strictEqual(actual[2].title, 'Amazon Q: Explain "issue 2"')
+ assert.strictEqual(actual[3].title, 'Amazon Q: Ignore this "issue 2" issue')
+ assert.strictEqual(actual[4].title, 'Amazon Q: Ignore all "issue 2" issues')
+ })
+
+ it('should not show issues that are not visible', () => {
+ securityIssueProvider.issues = [
+ {
+ filePath: mockDocument.fileName,
+ issues: [createCodeScanIssue({ visible: false })],
+ },
+ ]
+ const range = new vscode.Range(0, 0, 0, 0)
+ const actual = securityIssueCodeActionProvider.provideCodeActions(mockDocument, range, context, token.token)
+
+ assert.strictEqual(actual.length, 0)
})
})
diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts
index 162f7534218..956c3b43d73 100644
--- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts
@@ -4,16 +4,18 @@
*/
import * as vscode from 'vscode'
-import { SecurityIssueHoverProvider } from 'aws-core-vscode/codewhisperer'
+import { SecurityIssueHoverProvider, SecurityIssueProvider } from 'aws-core-vscode/codewhisperer'
import { createCodeScanIssue, createMockDocument, assertTelemetry } from 'aws-core-vscode/test'
import assert from 'assert'
describe('securityIssueHoverProvider', () => {
+ let securityIssueProvider: SecurityIssueProvider
let securityIssueHoverProvider: SecurityIssueHoverProvider
let mockDocument: vscode.TextDocument
let token: vscode.CancellationTokenSource
beforeEach(() => {
+ securityIssueProvider = SecurityIssueProvider.instance
securityIssueHoverProvider = new SecurityIssueHoverProvider()
mockDocument = createMockDocument('def two_sum(nums, target):\nfor', 'test.py', 'python')
token = new vscode.CancellationTokenSource()
@@ -30,7 +32,7 @@ describe('securityIssueHoverProvider', () => {
}),
]
- securityIssueHoverProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: mockDocument.fileName,
issues,
@@ -46,10 +48,16 @@ describe('securityIssueHoverProvider', () => {
'fix\n\n' +
`[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent(
JSON.stringify([issues[0], mockDocument.fileName])
- )} 'Open "Amazon Q Security Issue"')\n` +
+ )} 'Open "Code Issue Details"')\n` +
` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent(
JSON.stringify([issues[0]])
)} 'Explain with Amazon Q')\n` +
+ ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent(
+ JSON.stringify([issues[0], mockDocument.fileName, 'hover'])
+ )} 'Ignore Issue')\n` +
+ ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent(
+ JSON.stringify([issues[0], 'hover'])
+ )} 'Ignore Similar Issues')\n` +
` | [$(wrench) Fix](command:aws.amazonq.applySecurityFix?${encodeURIComponent(
JSON.stringify([issues[0], mockDocument.fileName, 'hover'])
)} 'Fix with Amazon Q')\n` +
@@ -90,10 +98,16 @@ describe('securityIssueHoverProvider', () => {
'recommendationText\n\n' +
`[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent(
JSON.stringify([issues[1], mockDocument.fileName])
- )} 'Open "Amazon Q Security Issue"')\n` +
+ )} 'Open "Code Issue Details"')\n` +
` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent(
JSON.stringify([issues[1]])
- )} 'Explain with Amazon Q')\n`
+ )} 'Explain with Amazon Q')\n` +
+ ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent(
+ JSON.stringify([issues[1], mockDocument.fileName, 'hover'])
+ )} 'Ignore Issue')\n` +
+ ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent(
+ JSON.stringify([issues[1], 'hover'])
+ )} 'Ignore Similar Issues')\n`
)
assertTelemetry('codewhisperer_codeScanIssueHover', [
{ findingId: 'finding-1', detectorId: 'language/detector-1', ruleId: 'Rule-123', includesFix: true },
@@ -102,7 +116,7 @@ describe('securityIssueHoverProvider', () => {
})
it('should return empty contents if there is no issue on the current position', () => {
- securityIssueHoverProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: mockDocument.fileName,
issues: [createCodeScanIssue()],
@@ -114,7 +128,7 @@ describe('securityIssueHoverProvider', () => {
})
it('should skip issues not in the current file', () => {
- securityIssueHoverProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: 'some/path',
issues: [createCodeScanIssue()],
@@ -130,7 +144,7 @@ describe('securityIssueHoverProvider', () => {
it('should not show severity badge if undefined', () => {
const issues = [createCodeScanIssue({ severity: undefined, suggestedFixes: [] })]
- securityIssueHoverProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: mockDocument.fileName,
issues,
@@ -144,10 +158,16 @@ describe('securityIssueHoverProvider', () => {
'recommendationText\n\n' +
`[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent(
JSON.stringify([issues[0], mockDocument.fileName])
- )} 'Open "Amazon Q Security Issue"')\n` +
+ )} 'Open "Code Issue Details"')\n` +
` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent(
JSON.stringify([issues[0]])
- )} 'Explain with Amazon Q')\n`
+ )} 'Explain with Amazon Q')\n` +
+ ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent(
+ JSON.stringify([issues[0], mockDocument.fileName, 'hover'])
+ )} 'Ignore Issue')\n` +
+ ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent(
+ JSON.stringify([issues[0], 'hover'])
+ )} 'Ignore Similar Issues')\n`
)
})
@@ -162,7 +182,7 @@ describe('securityIssueHoverProvider', () => {
],
}),
]
- securityIssueHoverProvider.issues = [
+ securityIssueProvider.issues = [
{
filePath: mockDocument.fileName,
issues,
@@ -176,10 +196,16 @@ describe('securityIssueHoverProvider', () => {
'fix\n\n' +
`[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent(
JSON.stringify([issues[0], mockDocument.fileName])
- )} 'Open "Amazon Q Security Issue"')\n` +
+ )} 'Open "Code Issue Details"')\n` +
` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent(
JSON.stringify([issues[0]])
)} 'Explain with Amazon Q')\n` +
+ ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent(
+ JSON.stringify([issues[0], mockDocument.fileName, 'hover'])
+ )} 'Ignore Issue')\n` +
+ ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent(
+ JSON.stringify([issues[0], 'hover'])
+ )} 'Ignore Similar Issues')\n` +
` | [$(wrench) Fix](command:aws.amazonq.applySecurityFix?${encodeURIComponent(
JSON.stringify([issues[0], mockDocument.fileName, 'hover'])
)} 'Fix with Amazon Q')\n` +
@@ -216,4 +242,16 @@ describe('securityIssueHoverProvider', () => {
'\n\n'
)
})
+
+ it('should not show issues that are not visible', () => {
+ const issues = [createCodeScanIssue({ visible: false })]
+ securityIssueProvider.issues = [
+ {
+ filePath: mockDocument.fileName,
+ issues,
+ },
+ ]
+ const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token)
+ assert.strictEqual(actual.contents.length, 0)
+ })
})
diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueProvider.test.ts
index 35af4440db1..cbe4daed9fb 100644
--- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueProvider.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueProvider.test.ts
@@ -37,14 +37,14 @@ describe('securityIssueProvider', () => {
]
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 1)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 2)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -1,1 +1,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -1,1 +1,1 @@'))
const changeEvent = createTextDocumentChangeEvent(mockDocument, new vscode.Range(0, 0, 0, 0), '\n')
mockProvider.handleDocumentChange(changeEvent)
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 2)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 3)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -2,1 +2,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -2,1 +2,1 @@'))
})
it('does not move the issue if the document changed below the line', () => {
@@ -52,14 +52,14 @@ describe('securityIssueProvider', () => {
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 0)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 1)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -1,1 +1,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -1,1 +1,1 @@'))
const changeEvent = createTextDocumentChangeEvent(mockDocument, new vscode.Range(2, 0, 2, 0), '\n')
mockProvider.handleDocumentChange(changeEvent)
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 0)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 1)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -1,1 +1,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -1,1 +1,1 @@'))
})
it('should do nothing if no content changes', () => {
@@ -69,7 +69,7 @@ describe('securityIssueProvider', () => {
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 1)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 2)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -1,1 +1,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -1,1 +1,1 @@'))
const changeEvent = createTextDocumentChangeEvent(mockDocument, new vscode.Range(0, 0, 0, 0), '')
changeEvent.contentChanges = []
@@ -77,21 +77,21 @@ describe('securityIssueProvider', () => {
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 1)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 2)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -1,1 +1,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -1,1 +1,1 @@'))
})
it('should do nothing if file path does not match', () => {
mockProvider.issues = [{ filePath: 'some/path', issues: [createCodeScanIssue({ startLine: 1, endLine: 2 })] }]
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 1)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 2)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -1,1 +1,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -1,1 +1,1 @@'))
const changeEvent = createTextDocumentChangeEvent(mockDocument, new vscode.Range(0, 0, 0, 0), '\n')
mockProvider.handleDocumentChange(changeEvent)
assert.strictEqual(mockProvider.issues[0].issues[0].startLine, 1)
assert.strictEqual(mockProvider.issues[0].issues[0].endLine, 2)
- assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code.startsWith('@@ -1,1 +1,1 @@'))
+ assert.ok(mockProvider.issues[0].issues[0].suggestedFixes[0].code?.startsWith('@@ -1,1 +1,1 @@'))
})
describe('removeIssue', () => {
diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts
new file mode 100644
index 00000000000..bd7c3aab8de
--- /dev/null
+++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts
@@ -0,0 +1,106 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ FileItem,
+ IssueItem,
+ SecurityIssueTreeViewProvider,
+ SecurityTreeViewFilterState,
+ SecurityIssueProvider,
+ SeverityItem,
+} from 'aws-core-vscode/codewhisperer'
+import { createCodeScanIssue } from 'aws-core-vscode/test'
+import assert from 'assert'
+import sinon from 'sinon'
+
+describe('SecurityIssueTreeViewProvider', function () {
+ let securityIssueProvider: SecurityIssueProvider
+ let securityIssueTreeViewProvider: SecurityIssueTreeViewProvider
+
+ beforeEach(function () {
+ securityIssueProvider = SecurityIssueProvider.instance
+ securityIssueTreeViewProvider = new SecurityIssueTreeViewProvider()
+ })
+
+ afterEach(function () {
+ sinon.restore()
+ })
+
+ describe('getTreeItem', function () {
+ it('should return the element as a FileItem', function () {
+ const element = new FileItem('dummy-path', [])
+ const result = securityIssueTreeViewProvider.getTreeItem(element)
+ assert.strictEqual(result, element)
+ })
+
+ it('should return the element as a IssueItem', function () {
+ const element = new IssueItem('dummy-path', createCodeScanIssue())
+ const result = securityIssueTreeViewProvider.getTreeItem(element)
+ assert.strictEqual(result, element)
+ })
+ })
+
+ describe('getChildren', function () {
+ it('should return sorted list of severities if element is undefined', function () {
+ securityIssueProvider.issues = [
+ { filePath: 'file/path/c', issues: [createCodeScanIssue(), createCodeScanIssue()] },
+ { filePath: 'file/path/d', issues: [createCodeScanIssue(), createCodeScanIssue()] },
+ { filePath: 'file/path/a', issues: [createCodeScanIssue(), createCodeScanIssue()] },
+ { filePath: 'file/path/b', issues: [createCodeScanIssue(), createCodeScanIssue()] },
+ ]
+
+ const element = undefined
+ const result = securityIssueTreeViewProvider.getChildren(element) as SeverityItem[]
+ assert.strictEqual(result.length, 5)
+ assert.strictEqual(result[0].label, 'Critical')
+ assert.strictEqual(result[0].description, '0 issues')
+ assert.strictEqual(result[1].label, 'High')
+ assert.strictEqual(result[1].description, '8 issues')
+ assert.strictEqual(result[2].label, 'Medium')
+ assert.strictEqual(result[2].description, '0 issues')
+ assert.strictEqual(result[3].label, 'Low')
+ assert.strictEqual(result[3].description, '0 issues')
+ assert.strictEqual(result[4].label, 'Info')
+ assert.strictEqual(result[4].description, '0 issues')
+ })
+
+ it('should return sorted list of issues if element is SeverityItem', function () {
+ const element = new SeverityItem('Critical', [
+ {
+ ...createCodeScanIssue({ title: 'Finding A', startLine: 10, severity: 'Critical' }),
+ filePath: 'file/path/a',
+ },
+ {
+ ...createCodeScanIssue({ title: 'Finding B', startLine: 2, severity: 'Critical' }),
+ filePath: 'file/path/b',
+ },
+ ])
+ const result = securityIssueTreeViewProvider.getChildren(element) as IssueItem[]
+ assert.strictEqual(result.length, 2)
+ assert.strictEqual(result[0].label, 'Finding A')
+ assert.strictEqual(result[1].label, 'Finding B')
+ })
+
+ it('should filter out severities', function () {
+ const element = undefined
+ let result = securityIssueTreeViewProvider.getChildren(element) as SeverityItem[]
+ assert.strictEqual(result.length, 5)
+
+ sinon.stub(SecurityTreeViewFilterState.instance, 'getHiddenSeverities').returns(['Medium'])
+
+ result = securityIssueTreeViewProvider.getChildren(element) as SeverityItem[]
+ assert.strictEqual(result.length, 4)
+ assert.ok(result.every((item) => item.severity !== 'Medium'))
+ })
+
+ it('should not show issues that are not visible', function () {
+ const element = new SeverityItem('Critical', [
+ { ...createCodeScanIssue({ visible: false }), filePath: 'file/path/a' },
+ ])
+ const result = securityIssueTreeViewProvider.getChildren(element) as IssueItem[]
+ assert.strictEqual(result.length, 0)
+ })
+ })
+})
diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts
index d9f492e6128..b0086b2a205 100644
--- a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts
@@ -19,33 +19,35 @@ import sinon from 'sinon'
import * as vscode from 'vscode'
import fs from 'fs' // eslint-disable-line no-restricted-imports
-const mockCodeScanFindings = JSON.stringify([
- {
- filePath: 'workspaceFolder/python3.7-plain-sam-app/hello_world/app.py',
- startLine: 1,
- endLine: 1,
- title: 'title',
- description: {
+const buildRawCodeScanIssue = (params?: Partial): RawCodeScanIssue => ({
+ filePath: 'workspaceFolder/python3.7-plain-sam-app/hello_world/app.py',
+ startLine: 1,
+ endLine: 1,
+ title: 'title',
+ description: {
+ text: 'text',
+ markdown: 'markdown',
+ },
+ detectorId: 'detectorId',
+ detectorName: 'detectorName',
+ findingId: 'findingId',
+ relatedVulnerabilities: [],
+ severity: 'High',
+ remediation: {
+ recommendation: {
text: 'text',
- markdown: 'markdown',
- },
- detectorId: 'detectorId',
- detectorName: 'detectorName',
- findingId: 'findingId',
- relatedVulnerabilities: [],
- severity: 'High',
- remediation: {
- recommendation: {
- text: 'text',
- url: 'url',
- },
- suggestedFixes: [],
+ url: 'url',
},
- codeSnippet: [],
- } satisfies RawCodeScanIssue,
-])
+ suggestedFixes: [],
+ },
+ codeSnippet: [],
+ ...params,
+})
-const mockListCodeScanFindingsResponse: Awaited>> = {
+const buildMockListCodeScanFindingsResponse = (
+ codeScanFindings: string = JSON.stringify([buildRawCodeScanIssue()]),
+ nextToken?: boolean
+): Awaited>> => ({
$response: {
hasNextPage: () => false,
nextPage: () => undefined,
@@ -56,16 +58,9 @@ const mockListCodeScanFindingsResponse: Awaited>
-> = {
- ...mockListCodeScanFindingsResponse,
- nextToken: 'nextToken',
-}
+ codeScanFindings,
+ nextToken: nextToken ? 'nextToken' : undefined,
+})
describe('securityScanHandler', function () {
describe('listScanResults', function () {
@@ -81,7 +76,7 @@ describe('securityScanHandler', function () {
})
it('should make ListCodeScanFindings request and aggregate findings by file path', async function () {
- mockClient.listCodeScanFindings.resolves(mockListCodeScanFindingsResponse)
+ mockClient.listCodeScanFindings.resolves(buildMockListCodeScanFindingsResponse())
const aggregatedCodeScanIssueList = await listScanResults(
mockClient,
@@ -100,11 +95,26 @@ describe('securityScanHandler', function () {
it('should handle ListCodeScanFindings request with paginated response', async function () {
mockClient.listCodeScanFindings
.onFirstCall()
- .resolves(mockListCodeScanFindingsPaginatedResponse)
+ .resolves(
+ buildMockListCodeScanFindingsResponse(
+ JSON.stringify([buildRawCodeScanIssue({ title: 'title1' })]),
+ true
+ )
+ )
.onSecondCall()
- .resolves(mockListCodeScanFindingsPaginatedResponse)
+ .resolves(
+ buildMockListCodeScanFindingsResponse(
+ JSON.stringify([buildRawCodeScanIssue({ title: 'title2' })]),
+ true
+ )
+ )
.onThirdCall()
- .resolves(mockListCodeScanFindingsResponse)
+ .resolves(
+ buildMockListCodeScanFindingsResponse(
+ JSON.stringify([buildRawCodeScanIssue({ title: 'title3' })]),
+ false
+ )
+ )
const aggregatedCodeScanIssueList = await listScanResults(
mockClient,
@@ -154,7 +164,7 @@ describe('securityScanHandler', function () {
{ filePath: 'file2.ts', startLine: 1, endLine: 1, codeSnippet: [{ number: 1, content: 'line 1' }] },
])
- mapToAggregatedList(codeScanIssueMap, json, editor, CodeAnalysisScope.FILE)
+ mapToAggregatedList(codeScanIssueMap, json, editor, CodeAnalysisScope.FILE_AUTO)
assert.equal(codeScanIssueMap.size, 2)
assert.equal(codeScanIssueMap.get('file1.ts')?.length, 1)
@@ -175,7 +185,7 @@ describe('securityScanHandler', function () {
{ filePath: 'file1.ts', startLine: 3, endLine: 3, codeSnippet: [{ number: 3, content: 'line 3' }] },
])
- mapToAggregatedList(codeScanIssueMap, json, editor, CodeAnalysisScope.FILE)
+ mapToAggregatedList(codeScanIssueMap, json, editor, CodeAnalysisScope.FILE_AUTO)
assert.equal(codeScanIssueMap.size, 1)
assert.equal(codeScanIssueMap.get('file1.ts')?.length, 2)
@@ -195,7 +205,36 @@ describe('securityScanHandler', function () {
{ filePath: 'file1.ts', startLine: 3, endLine: 3, codeSnippet: [{ number: 3, content: '**** **' }] },
])
- mapToAggregatedList(codeScanIssueMap, json, editor, CodeAnalysisScope.FILE)
+ mapToAggregatedList(codeScanIssueMap, json, editor, CodeAnalysisScope.FILE_AUTO)
+ assert.strictEqual(codeScanIssueMap.size, 1)
+ assert.strictEqual(codeScanIssueMap.get('file1.ts')?.length, 1)
+ })
+
+ it('should handle duplicate issues', function () {
+ const json = JSON.stringify([
+ {
+ filePath: 'file1.ts',
+ startLine: 1,
+ endLine: 2,
+ title: 'duplicate issue',
+ codeSnippet: [
+ { number: 1, content: 'line 1' },
+ { number: 2, content: 'line 2' },
+ ],
+ },
+ {
+ filePath: 'file1.ts',
+ startLine: 1,
+ endLine: 2,
+ title: 'duplicate issue',
+ codeSnippet: [
+ { number: 1, content: 'line 1' },
+ { number: 2, content: 'line 2' },
+ ],
+ },
+ ])
+
+ mapToAggregatedList(codeScanIssueMap, json, editor, CodeAnalysisScope.FILE_AUTO)
assert.strictEqual(codeScanIssueMap.size, 1)
assert.strictEqual(codeScanIssueMap.get('file1.ts')?.length, 1)
})
diff --git a/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts
index a5d79bb2fe3..a4a03a5236a 100644
--- a/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts
@@ -11,6 +11,9 @@ import { getTestWorkspaceFolder } from 'aws-core-vscode/test'
import { CodeAnalysisScope, ZipUtil } from 'aws-core-vscode/codewhisperer'
import { codeScanTruncDirPrefix } from 'aws-core-vscode/codewhisperer'
import { ToolkitError } from 'aws-core-vscode/shared'
+import { LspClient } from 'aws-core-vscode/amazonq'
+import { fs } from 'aws-core-vscode/shared'
+import path from 'path'
describe('zipUtil', function () {
const workspaceFolder = getTestWorkspaceFolder()
@@ -22,6 +25,11 @@ describe('zipUtil', function () {
const zipUtil = new ZipUtil()
assert.deepStrictEqual(zipUtil.getProjectPaths(), [workspaceFolder])
})
+
+ it('Should return the correct project path for unit test generation', function () {
+ const zipUtil = new ZipUtil()
+ assert.deepStrictEqual(zipUtil.getProjectPath(appCodePath), workspaceFolder)
+ })
})
describe('generateZip', function () {
@@ -34,7 +42,7 @@ describe('zipUtil', function () {
})
it('Should generate zip for file scan and return expected metadata', async function () {
- const zipMetadata = await zipUtil.generateZip(vscode.Uri.file(appCodePath), CodeAnalysisScope.FILE)
+ const zipMetadata = await zipUtil.generateZip(vscode.Uri.file(appCodePath), CodeAnalysisScope.FILE_AUTO)
assert.strictEqual(zipMetadata.lines, 49)
assert.ok(zipMetadata.rootDir.includes(codeScanTruncDirPrefix))
assert.ok(zipMetadata.srcPayloadSizeInBytes > 0)
@@ -48,7 +56,7 @@ describe('zipUtil', function () {
sinon.stub(zipUtil, 'reachSizeLimit').returns(true)
await assert.rejects(
- () => zipUtil.generateZip(vscode.Uri.file(appCodePath), CodeAnalysisScope.FILE),
+ () => zipUtil.generateZip(vscode.Uri.file(appCodePath), CodeAnalysisScope.FILE_AUTO),
new ToolkitError(`Payload size limit reached`, { code: 'FileSizeExceeded' })
)
})
@@ -105,4 +113,133 @@ describe('zipUtil', function () {
assert.equal(zipMetadata2.lines, zipMetadata.lines + 1)
})
})
+
+ describe('generateZipTestGen', function () {
+ let zipUtil: ZipUtil
+ let mockFs: sinon.SinonStubbedInstance
+ const projectPath = '/test/project'
+ const zipDirPath = '/test/zip'
+ const zipFilePath = '/test/zip/test.zip'
+
+ beforeEach(function () {
+ zipUtil = new ZipUtil()
+ mockFs = sinon.stub(fs)
+
+ const mockRepoMapPath = '/path/to/repoMapData.json'
+ mockFs.exists.withArgs(mockRepoMapPath).resolves(true)
+ sinon.stub(LspClient, 'instance').get(() => ({
+ getRepoMapJSON: sinon.stub().resolves(mockRepoMapPath),
+ }))
+
+ sinon.stub(zipUtil, 'getZipDirPath').returns(zipDirPath)
+ sinon.stub(zipUtil as any, 'zipProject').resolves(zipFilePath)
+ })
+
+ afterEach(function () {
+ sinon.restore()
+ })
+
+ it('Should generate zip for test generation successfully', async function () {
+ mockFs.stat.resolves({
+ type: vscode.FileType.File,
+ size: 1000,
+ ctime: Date.now(),
+ mtime: Date.now(),
+ } as vscode.FileStat)
+
+ mockFs.readFileBytes.resolves(Buffer.from('test content'))
+
+ // Fix: Create a Set from the array
+ zipUtil['_totalSize'] = 500
+ zipUtil['_totalBuildSize'] = 200
+ zipUtil['_totalLines'] = 100
+ zipUtil['_language'] = 'typescript'
+ zipUtil['_pickedSourceFiles'] = new Set(['file1.ts', 'file2.ts'])
+
+ const result = await zipUtil.generateZipTestGen(projectPath, false)
+
+ assert.ok(mockFs.mkdir.calledWith(path.join(zipDirPath, 'utgRequiredArtifactsDir')))
+ assert.ok(
+ mockFs.mkdir.calledWith(path.join(zipDirPath, 'utgRequiredArtifactsDir', 'buildAndExecuteLogDir'))
+ )
+ assert.ok(mockFs.mkdir.calledWith(path.join(zipDirPath, 'utgRequiredArtifactsDir', 'repoMapData')))
+ assert.ok(mockFs.mkdir.calledWith(path.join(zipDirPath, 'utgRequiredArtifactsDir', 'testCoverageDir')))
+
+ // assert.ok(
+ // mockFs.copy.calledWith(
+ // '/path/to/repoMapData.json',
+ // path.join(zipDirPath, 'utgRequiredArtifactsDir', 'repoMapData', 'repoMapData.json')
+ // )
+ // )
+
+ assert.strictEqual(result.rootDir, zipDirPath)
+ assert.strictEqual(result.zipFilePath, zipFilePath)
+ assert.strictEqual(result.srcPayloadSizeInBytes, 500)
+ assert.strictEqual(result.buildPayloadSizeInBytes, 200)
+ assert.strictEqual(result.zipFileSizeInBytes, 1000)
+ assert.strictEqual(result.lines, 100)
+ assert.strictEqual(result.language, 'typescript')
+ assert.deepStrictEqual(Array.from(result.scannedFiles), ['file1.ts', 'file2.ts'])
+ })
+
+ // it('Should handle LSP client error', async function () {
+ // // Override the default stub with one that rejects
+ // sinon.stub(LspClient, 'instance').get(() => ({
+ // getRepoMapJSON: sinon.stub().rejects(new Error('LSP error')),
+ // }))
+
+ // await assert.rejects(() => zipUtil.generateZipTestGen(projectPath), /LSP error/)
+ // })
+
+ it('Should handle file system errors during directory creation', async function () {
+ sinon.stub(LspClient, 'instance').get(() => ({
+ getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'),
+ }))
+ mockFs.mkdir.rejects(new Error('Directory creation failed'))
+
+ await assert.rejects(() => zipUtil.generateZipTestGen(projectPath, false), /Directory creation failed/)
+ })
+
+ it('Should handle zip project errors', async function () {
+ sinon.stub(LspClient, 'instance').get(() => ({
+ getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'),
+ }))
+ ;(zipUtil as any).zipProject.rejects(new Error('Zip failed'))
+
+ await assert.rejects(() => zipUtil.generateZipTestGen(projectPath, false), /Zip failed/)
+ })
+
+ it('Should handle file copy to downloads folder error', async function () {
+ // Mock LSP client
+ sinon.stub(LspClient, 'instance').get(() => ({
+ getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'),
+ }))
+
+ // Mock file operations
+ const mockFs = {
+ mkdir: sinon.stub().resolves(),
+ copy: sinon.stub().rejects(new Error('Copy failed')),
+ exists: sinon.stub().resolves(true),
+ stat: sinon.stub().resolves({
+ type: vscode.FileType.File,
+ size: 1000,
+ ctime: Date.now(),
+ mtime: Date.now(),
+ } as vscode.FileStat),
+ }
+
+ // Since the function now uses Promise.all for directory creation and file operations,
+ // we need to ensure the mkdir succeeds but the copy fails
+ fs.mkdir = mockFs.mkdir
+ fs.copy = mockFs.copy
+ fs.exists = mockFs.exists
+ fs.stat = mockFs.stat
+
+ await assert.rejects(() => zipUtil.generateZipTestGen(projectPath, false), /Copy failed/)
+
+ // Verify mkdir was called for all directories
+ assert(mockFs.mkdir.called, 'mkdir should have been called')
+ assert.strictEqual(mockFs.mkdir.callCount, 4, 'mkdir should have been called 4 times')
+ })
+ })
})
diff --git a/packages/amazonq/test/unit/codewhisperer/views/securityPanelViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/views/securityPanelViewProvider.test.ts
index 0b48eac2106..ca163e86208 100644
--- a/packages/amazonq/test/unit/codewhisperer/views/securityPanelViewProvider.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/views/securityPanelViewProvider.test.ts
@@ -27,6 +27,9 @@ const codeScanIssue: CodeScanIssue[] = [
severity: 'low',
recommendation: { text: 'foo', url: 'foo' },
suggestedFixes: [],
+ visible: true,
+ language: 'python',
+ scanJobId: 'scanJob',
},
]
diff --git a/packages/core/package.json b/packages/core/package.json
index a59f345d7ed..0550107f734 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -20,6 +20,8 @@
"./auth": "./dist/src/auth/index.js",
"./amazonqGumby": "./dist/src/amazonqGumby/index.js",
"./amazonqFeatureDev": "./dist/src/amazonqFeatureDev/index.js",
+ "./amazonqScan": "./dist/src/amazonqScan/index.js",
+ "./amazonqTest": "./dist/src/amazonqTest/index.js",
"./codewhispererChat": "./dist/src/codewhispererChat/index.js",
"./test": "./dist/src/test/index.js",
"./testWeb": "./dist/src/testWeb/index.js",
@@ -53,327 +55,362 @@
"fontCharacter": "\\f1ac"
}
},
- "aws-amazonq-transform-arrow-dark": {
+ "aws-amazonq-severity-critical": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ad"
}
},
- "aws-amazonq-transform-arrow-light": {
+ "aws-amazonq-severity-high": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ae"
}
},
- "aws-amazonq-transform-default-dark": {
+ "aws-amazonq-severity-info": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1af"
}
},
- "aws-amazonq-transform-default-light": {
+ "aws-amazonq-severity-low": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b0"
}
},
- "aws-amazonq-transform-dependencies-dark": {
+ "aws-amazonq-severity-medium": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b1"
}
},
- "aws-amazonq-transform-dependencies-light": {
+ "aws-amazonq-transform-arrow-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b2"
}
},
- "aws-amazonq-transform-file-dark": {
+ "aws-amazonq-transform-arrow-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b3"
}
},
- "aws-amazonq-transform-file-light": {
+ "aws-amazonq-transform-default-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b4"
}
},
- "aws-amazonq-transform-logo": {
+ "aws-amazonq-transform-default-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b5"
}
},
- "aws-amazonq-transform-step-into-dark": {
+ "aws-amazonq-transform-dependencies-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b6"
}
},
- "aws-amazonq-transform-step-into-light": {
+ "aws-amazonq-transform-dependencies-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b7"
}
},
- "aws-amazonq-transform-variables-dark": {
+ "aws-amazonq-transform-file-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b8"
}
},
- "aws-amazonq-transform-variables-light": {
+ "aws-amazonq-transform-file-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1b9"
}
},
- "aws-applicationcomposer-icon": {
+ "aws-amazonq-transform-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ba"
}
},
- "aws-applicationcomposer-icon-dark": {
+ "aws-amazonq-transform-step-into-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bb"
}
},
- "aws-apprunner-service": {
+ "aws-amazonq-transform-step-into-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bc"
}
},
- "aws-cdk-logo": {
+ "aws-amazonq-transform-variables-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bd"
}
},
- "aws-cloudformation-stack": {
+ "aws-amazonq-transform-variables-light": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1be"
}
},
- "aws-cloudwatch-log-group": {
+ "aws-applicationcomposer-icon": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1bf"
}
},
- "aws-codecatalyst-logo": {
+ "aws-applicationcomposer-icon-dark": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c0"
}
},
- "aws-codewhisperer-icon-black": {
+ "aws-apprunner-service": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c1"
}
},
- "aws-codewhisperer-icon-white": {
+ "aws-cdk-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c2"
}
},
- "aws-codewhisperer-learn": {
+ "aws-cloudformation-stack": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c3"
}
},
- "aws-ecr-registry": {
+ "aws-cloudwatch-log-group": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c4"
}
},
- "aws-ecs-cluster": {
+ "aws-codecatalyst-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c5"
}
},
- "aws-ecs-container": {
+ "aws-codewhisperer-icon-black": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c6"
}
},
- "aws-ecs-service": {
+ "aws-codewhisperer-icon-white": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c7"
}
},
- "aws-generic-attach-file": {
+ "aws-codewhisperer-learn": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c8"
}
},
- "aws-iot-certificate": {
+ "aws-ecr-registry": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1c9"
}
},
- "aws-iot-policy": {
+ "aws-ecs-cluster": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ca"
}
},
- "aws-iot-thing": {
+ "aws-ecs-container": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cb"
}
},
- "aws-lambda-function": {
+ "aws-ecs-service": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cc"
}
},
- "aws-mynah-MynahIconBlack": {
+ "aws-generic-attach-file": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cd"
}
},
- "aws-mynah-MynahIconWhite": {
+ "aws-iot-certificate": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1ce"
}
},
- "aws-mynah-logo": {
+ "aws-iot-policy": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1cf"
}
},
- "aws-redshift-cluster": {
+ "aws-iot-thing": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d0"
}
},
- "aws-redshift-cluster-connected": {
+ "aws-lambda-function": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d1"
}
},
- "aws-redshift-database": {
+ "aws-mynah-MynahIconBlack": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d2"
}
},
- "aws-redshift-redshift-cluster-connected": {
+ "aws-mynah-MynahIconWhite": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d3"
}
},
- "aws-redshift-schema": {
+ "aws-mynah-logo": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d4"
}
},
- "aws-redshift-table": {
+ "aws-redshift-cluster": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d5"
}
},
- "aws-s3-bucket": {
+ "aws-redshift-cluster-connected": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d6"
}
},
- "aws-s3-create-bucket": {
+ "aws-redshift-database": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d7"
}
},
- "aws-schemas-registry": {
+ "aws-redshift-redshift-cluster-connected": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d8"
}
},
- "aws-schemas-schema": {
+ "aws-redshift-schema": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1d9"
}
},
- "aws-stepfunctions-preview": {
+ "aws-redshift-table": {
"description": "AWS Contributed Icon",
"default": {
"fontPath": "./resources/fonts/aws-toolkit-icons.woff",
"fontCharacter": "\\f1da"
}
+ },
+ "aws-s3-bucket": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1db"
+ }
+ },
+ "aws-s3-create-bucket": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1dc"
+ }
+ },
+ "aws-schemas-registry": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1dd"
+ }
+ },
+ "aws-schemas-schema": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1de"
+ }
+ },
+ "aws-stepfunctions-preview": {
+ "description": "AWS Contributed Icon",
+ "default": {
+ "fontPath": "./resources/fonts/aws-toolkit-icons.woff",
+ "fontCharacter": "\\f1df"
+ }
}
}
},
@@ -458,9 +495,9 @@
"dependencies": {
"@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client",
"@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming",
+ "@aws-sdk/client-cloudformation": "^3.667.0",
"@aws-sdk/client-cognito-identity": "^3.637.0",
"@aws-sdk/client-lambda": "^3.637.0",
- "@aws-sdk/client-cloudformation": "^3.667.0",
"@aws-sdk/client-sso": "^3.342.0",
"@aws-sdk/client-sso-oidc": "^3.574.0",
"@aws-sdk/credential-provider-ini": "3.46.0",
diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json
index ff4f74ab759..a0a79329f70 100644
--- a/packages/core/package.nls.json
+++ b/packages/core/package.nls.json
@@ -78,6 +78,7 @@
"AWS.configuration.description.amazonq.workspaceIndexWorkerThreads": "Number of worker threads of Amazon Q local index process. '0' will use the system default worker threads for balance performance. You may increase this number to more quickly index your workspace, but only up to your hardware's number of CPU cores. Please restart VS Code or reload the VS Code window after changing worker threads.",
"AWS.configuration.description.amazonq.workspaceIndexUseGPU": "Enable GPU to help index your local workspace files. Only applies to Linux and Windows.",
"AWS.configuration.description.amazonq.workspaceIndexMaxSize": "The maximum size of local workspace files to be indexed in MB",
+ "AWS.configuration.description.amazonq.ignoredSecurityIssues": "Specifies a list of code issue identifiers 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.command.apig.copyUrl": "Copy URL",
"AWS.command.apig.invokeRemoteRestApi": "Invoke in the cloud",
"AWS.command.apig.invokeRemoteRestApi.cn": "Invoke on Amazon",
@@ -121,8 +122,17 @@
"AWS.command.amazonq.fixCode": "Fix",
"AWS.command.amazonq.optimizeCode": "Optimize",
"AWS.command.amazonq.sendToPrompt": "Send to prompt",
- "AWS.command.amazonq.generateUnitTests": "Generate Tests (Beta)",
- "AWS.command.amazonq.security.scan": "Run Project Scan",
+ "AWS.command.amazonq.generateUnitTests": "Generate Tests",
+ "AWS.command.amazonq.security.scan": "Run Project Review",
+ "AWS.command.amazonq.security.fileScan": "Run File Review",
+ "AWS.command.amazonq.generateFix": "Generate Fix",
+ "AWS.command.amazonq.viewDetails": "View Details",
+ "AWS.command.amazonq.explainIssue": "Explain",
+ "AWS.command.amazonq.ignoreIssue": "Ignore Issue",
+ "AWS.command.amazonq.ignoreAllIssues": "Ignore Similar Issues",
+ "AWS.command.amazonq.acceptFix": "Accept Fix",
+ "AWS.command.amazonq.regenerateFix": "Regenerate Fix",
+ "AWS.command.amazonq.filterIssues": "Filter Issues",
"AWS.command.deploySamApplication": "Deploy SAM Application",
"AWS.command.aboutToolkit": "About",
"AWS.command.downloadLambda": "Download...",
@@ -247,6 +257,7 @@
"AWS.samcli.deploy.bucket.recentlyUsed": "Buckets recently used for SAM deployments",
"AWS.submenu.amazonqEditorContextSubmenu.title": "Amazon Q",
"AWS.submenu.auth.title": "Authentication",
+ "AWS.submenu.amazonqSecurityIssueTree.filters": "Filter Issues",
"AWS.generic.feedback": "Feedback",
"AWS.generic.help": "Help",
"AWS.generic.create": "Create...",
@@ -257,7 +268,10 @@
"AWS.generic.promptUpdate": "Update...",
"AWS.generic.preview": "Preview",
"AWS.generic.viewDocs": "View Documentation",
+ "AWS.generic.moreActions": "More Actions...",
"AWS.generic.dismiss": "Dismiss",
+ "AWS.generic.cancel": "Cancel",
+ "AWS.generic.cancelling": "Cancelling...",
"AWS.ssmDocument.ssm.maxItemsComputed.desc": "Controls the maximum number of problems produced by the SSM Document language server.",
"AWS.walkthrough.gettingStarted.title": "Get started with AWS",
"AWS.walkthrough.gettingStarted.description": "These walkthroughs help you set up the AWS Toolkit.",
@@ -277,28 +291,40 @@
"AWS.codewhisperer.customization.notification.new_customizations.learn_more": "Learn More",
"AWS.amazonq.title": "Amazon Q",
"AWS.amazonq.chat": "Chat",
+ "AWS.amazonq.security": "Code Issues",
"AWS.amazonq.login": "Login",
"AWS.amazonq.learnMore": "Learn More About Amazon Q",
+ "AWS.amazonq.exploreAgents": "Explore Agent Capabilities",
+ "AWS.amazonq.welcomeWalkthrough": "Welcome Walkthrough",
"AWS.amazonq.codewhisperer.title": "Amazon Q",
"AWS.amazonq.toggleCodeSuggestion": "Toggle Auto-Suggestions",
"AWS.amazonq.toggleCodeScan": "Toggle Auto-Scans",
+ "AWS.amazonq.scans.scanProgress": "Sure. This may take a few minutes. I will send a notification when it’s complete if you navigate away from this panel.",
+ "AWS.amazonq.scans.waitingForInput": "Waiting on your inputs...",
+ "AWS.amazonq.scans.chooseScan.description": "Would you like to review your active file or the workspace you have open?",
+ "AWS.amazonq.scans.runCodeScan": "Run a code review",
+ "AWS.amazonq.scans.projectScan": "Review workspace",
+ "AWS.amazonq.scans.fileScan": "Review active file",
+ "AWS.amazonq.scans.projectScanInProgress": "Workspace review is in progress...",
+ "AWS.amazonq.scans.fileScanInProgress": "File review is in progress...",
+ "AWS.amazonq.scans.noGitRepo": "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.",
"AWS.amazonq.featureDev.error.conversationIdNotFoundError": "Conversation id must exist before starting code generation",
"AWS.amazonq.featureDev.error.contentLengthError": "The folder you selected is too large for me to use as context. Please choose a smaller folder to work on. For more information on quotas, see the Amazon Q Developer documentation.",
"AWS.amazonq.featureDev.error.illegalStateTransition": "Illegal transition between states, restart the conversation",
"AWS.amazonq.featureDev.error.prepareRepoFailedError": "Sorry, I ran into an issue while trying to upload your code. Please try again.",
"AWS.amazonq.featureDev.error.promptRefusalException": "I'm sorry, I can't generate code for your request. Please make sure your message and code files comply with the AWS Responsible AI Policy.",
- "AWS.amazonq.featureDev.error.noChangeRequiredException": "I’m sorry, I ran into an issue while trying to generate your code.\n\n- `/dev` can generate code to make a change in your project. Provide a detailed description of the new feature or code changes you want to make, including the specifics of what the code should achieve.\n\n- To ask me to explain, debug, or optimize your code, you can close this chat tab to start a new conversation.",
+ "AWS.amazonq.featureDev.error.noChangeRequiredException": "I'm sorry, I ran into an issue while trying to generate your code.\n\n- `/dev` can generate code to make a change in your project. Provide a detailed description of the new feature or code changes you want to make, including the specifics of what the code should achieve.\n\n- To ask me to explain, debug, or optimize your code, you can close this chat tab to start a new conversation.",
"AWS.amazonq.featureDev.error.zipFileError": "The zip file is corrupted",
"AWS.amazonq.featureDev.error.codeIterationLimitError": "Sorry, you've reached the quota for number of iterations on code generation. You can insert this code in your files or discuss a new plan. For more information on quotas, see the Amazon Q Developer documentation.",
"AWS.amazonq.featureDev.error.tabIdNotFoundError": "I'm sorry, I'm having technical difficulties at the moment. Please try again.",
"AWS.amazonq.featureDev.error.codeGen.denyListedError": "I'm sorry, I'm having trouble generating your code and can't continue at the moment. Please try again later, and share feedback to help me improve.",
"AWS.amazonq.featureDev.error.codeGen.default": "I'm sorry, I ran into an issue while trying to generate your code. Please try again.",
"AWS.amazonq.featureDev.error.codeGen.timeout": "Code generation did not finish within the expected time",
- "AWS.amazonq.featureDev.error.uploadURLExpired": "I’m sorry, I wasn’t able to generate code. A connection timed out or became unavailable. Please try again or check the following:\n\n- Exclude non-essential files in your workspace’s `.gitignore.`\n\n- Check that your network connection is stable.",
+ "AWS.amazonq.featureDev.error.uploadURLExpired": "I’m sorry, I wasn't able to generate code. A connection timed out or became unavailable. Please try again or check the following:\n\n- Exclude non-essential files in your workspace’s `.gitignore.`\n\n- Check that your network connection is stable.",
"AWS.amazonq.featureDev.error.workspaceFolderNotFoundError": "I couldn't find a workspace folder. Open a workspace, and then open a new chat tab and enter /dev to start discussing your code task with me.",
"AWS.amazonq.featureDev.error.selectedFolderNotInWorkspaceFolderError": "The folder you chose isn't in your open workspace folder. You can add this folder to your workspace, or choose a folder in your open workspace.",
"AWS.amazonq.featureDev.error.userMessageNotFoundError": "It looks like you didn't provide an input. Please enter your message in the text bar.",
- "AWS.amazonq.featureDev.error.monthlyLimitReached": "You've reached the monthly quota for the Amazon Q agent for software development. You can try again next month. For more information on usage limits, see the Amazon Q Developer pricing page.",
+ "AWS.amazonq.featureDev.error.monthlyLimitReached": "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.",
"AWS.amazonq.featureDev.error.technicalDifficulties": "I'm sorry, I'm having technical difficulties and can't continue at the moment. Please try again later, and share feedback to help me improve.",
"AWS.amazonq.featureDev.error.throttling": "I'm sorry, I'm experiencing high demand at the moment and can't generate your code. This attempt won't count toward usage limits. Please try again.",
"AWS.amazonq.featureDev.error.submitFeedback": "'submitFeedback' command was called programmatically, but its not registered.",
@@ -325,7 +351,7 @@
"AWS.amazonq.featureDev.pillText.selectOption": "Choose an option to proceed",
"AWS.amazonq.featureDev.pillText.unableGenerateChanges": "Unable to generate any file changes",
"AWS.amazonq.featureDev.pillText.provideFeedback": "Provide feedback & regenerate",
- "AWS.amazonq.featureDev.answer.generateSuggestion": "Would you like to generate a suggestion for this? You’ll review a file diff before inserting into your project.",
+ "AWS.amazonq.featureDev.answer.generateSuggestion": "Would you like to generate a suggestion for this? You'll review a file diff before inserting into your project.",
"AWS.amazonq.featureDev.answer.qGeneratedCode": "The Amazon Q Developer Agent for software development has generated code for you to review",
"AWS.amazonq.featureDev.answer.howCodeCanBeImproved": "How can I improve the code for your use case?",
"AWS.amazonq.featureDev.answer.updateCode": "Okay, I updated your code files. Would you like to work on another task?",
@@ -336,6 +362,28 @@
"AWS.amazonq.featureDev.placeholder.feedback": "Provide feedback or comments",
"AWS.amazonq.featureDev.placeholder.describe": "Describe your task or issue in detail",
"AWS.amazonq.featureDev.placeholder.sessionClosed": "Open a new chat tab to continue",
+ "AWS.amazonq.doc.pillText.selectOption": "Choose an option to continue",
+ "AWS.amazonq.doc.answer.createReadme": "Create a README for this project?",
+ "AWS.amazonq.doc.answer.updateReadme": "Update the README for this project?",
+ "AWS.amazonq.doc.answer.editReadme": "Okay, let's work on your README. Describe the changes you would like to make. For example, you can ask me to:\n- Correct something\n- Expand on something\n- Add a section\n- Remove a section",
+ "AWS.amazonq.doc.answer.readmeCreated": "I've created a README for your code.",
+ "AWS.amazonq.doc.answer.readmeUpdated": "I've updated your README.",
+ "AWS.amazonq.doc.answer.codeResult": "You can accept the changes to your files, or describe any additional changes you'd like me to make.",
+ "AWS.amazonq.doc.answer.scanning": "Scanning source files",
+ "AWS.amazonq.doc.answer.summarizing": "Summarizing source files",
+ "AWS.amazonq.doc.answer.generating": "Generating documentation",
+ "AWS.amazonq.doc.answer.creating": "Okay, I'm creating a README for your project. This may take a few minutes.",
+ "AWS.amazonq.doc.answer.updating": "Okay, I'm updating the README to reflect your code changes. This may take a few minutes.",
+ "AWS.amazonq.doc.error.contentLengthError": "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.",
+ "AWS.amazonq.doc.error.readmeTooLarge": "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.",
+ "AWS.amazonq.doc.error.workspaceEmpty": "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.",
+ "AWS.amazonq.doc.error.promptTooVague": "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.",
+ "AWS.amazonq.doc.error.promptUnrelated": "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\n For more information on prompt best practices, see the Amazon Q Developer documentation.",
+ "AWS.amazonq.doc.error.docGen.default": "I'm sorry, I ran into an issue while trying to generate your documentation. Please try again.",
+ "AWS.amazonq.doc.error.noChangeRequiredException": "I couldn't find any code changes to update in the README. Try another documentation task.",
+ "AWS.amazonq.doc.error.promptRefusal": "I'm sorry, I can't generate documentation for this folder. Please make sure your message and code files comply with the Please make sure your message and code files comply with the AWS Responsible AI Policy.",
+ "AWS.amazonq.doc.placeholder.editReadme": "Describe documentation changes",
+ "AWS.amazonq.doc.pillText.closeSession": "End session",
"AWS.amazonq.inline.invokeChat": "Inline chat",
"AWS.toolkit.lambda.walkthrough.quickpickTitle": "Application Builder Walkthrough",
"AWS.toolkit.lambda.walkthrough.title": "Get started building your application",
diff --git a/packages/core/resources/css/base.css b/packages/core/resources/css/base.css
index 35a7cc736b9..eab33956f19 100644
--- a/packages/core/resources/css/base.css
+++ b/packages/core/resources/css/base.css
@@ -142,6 +142,7 @@ button,
button:disabled {
/* TODO: use VSC webcomponent library instead */
filter: brightness(0.8);
+ cursor: default;
}
/* Text area */
diff --git a/packages/core/resources/css/securityIssue.css b/packages/core/resources/css/securityIssue.css
index f818ffdf14b..e102f5bc0f6 100644
--- a/packages/core/resources/css/securityIssue.css
+++ b/packages/core/resources/css/securityIssue.css
@@ -179,9 +179,10 @@ body.wordWrap pre {
pre:not(.hljs),
pre.hljs code > div {
- padding: 0 16px 16px 16px;
+ padding: 16px;
border-radius: 3px;
overflow: auto;
+ margin-bottom: 0;
}
pre code {
@@ -192,9 +193,11 @@ pre code {
pre {
background-color: var(--vscode-textCodeBlock-background);
border: 1px solid var(--vscode-widget-border);
+ font-size: 12px;
+ line-height: 1rem;
}
-code.language-diff {
+code[class^='language-'] {
background-color: unset;
}
@@ -222,8 +225,164 @@ code.language-diff {
color: var(--vscode-editorOverviewRuler-selectionHighlightForeground);
display: inline-block;
width: 100%;
- margin: 0 -16px;
- padding: 8px 16px;
+ margin: 0 -16px 4px -16px;
+ padding: 4px 16px;
+}
+
+.hljs-meta * {
+ color: unset !important;
+}
+
+.hljs-keyword,
+.hljs-literal,
+.hljs-symbol,
+.hljs-name {
+ color: #569cd6;
+}
+.hljs-link {
+ color: #569cd6;
+ text-decoration: underline;
+}
+
+.hljs-built_in,
+.hljs-type {
+ color: #4ec9b0;
+}
+
+.hljs-number,
+.hljs-class {
+ color: #b8d7a3;
+}
+
+.hljs-string,
+.hljs-meta-string {
+ color: #d69d85;
+}
+
+.hljs-regexp,
+.hljs-template-tag {
+ color: #9a5334;
+}
+
+.hljs-subst,
+.hljs-function,
+.hljs-title,
+.hljs-params,
+.hljs-formula {
+ color: #dcdcdc;
+}
+
+.hljs-comment,
+.hljs-quote {
+ color: #57a64a;
+ font-style: italic;
+}
+
+.hljs-doctag {
+ color: #608b4e;
+}
+
+.hljs-meta-keyword,
+.hljs-tag {
+ color: #9b9b9b;
+}
+
+.hljs-variable,
+.hljs-template-variable {
+ color: #bd63c5;
+}
+
+.hljs-attr,
+.hljs-attribute,
+.hljs-builtin-name {
+ color: #9cdcfe;
+}
+
+.hljs-section {
+ color: gold;
+}
+
+.hljs-emphasis {
+ font-style: italic;
+}
+
+.hljs-strong {
+ font-weight: bold;
+}
+
+.hljs-bullet,
+.hljs-selector-tag,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+ color: #d7ba7d;
+}
+
+.vscode-light .hljs-function,
+.vscode-light .hljs-params,
+.vscode-light .hljs-number,
+.vscode-light .hljs-class {
+ color: inherit;
+}
+
+.vscode-light .hljs-comment,
+.vscode-light .hljs-quote,
+.vscode-light .hljs-number,
+.vscode-light .hljs-class,
+.vscode-light .hljs-variable {
+ color: #008000;
+}
+
+.vscode-light .hljs-keyword,
+.vscode-light .hljs-selector-tag,
+.vscode-light .hljs-name,
+.vscode-light .hljs-tag {
+ color: #00f;
+}
+
+.vscode-light .hljs-built_in,
+.vscode-light .hljs-builtin-name {
+ color: #007acc;
+}
+
+.vscode-light .hljs-string,
+.vscode-light .hljs-section,
+.vscode-light .hljs-attribute,
+.vscode-light .hljs-literal,
+.vscode-light .hljs-template-tag,
+.vscode-light .hljs-template-variable,
+.vscode-light .hljs-type {
+ color: #a31515;
+}
+
+.vscode-light .hljs-subst,
+.vscode-light .hljs-selector-attr,
+.vscode-light .hljs-selector-pseudo,
+.vscode-light .hljs-meta-keyword {
+ color: #2b91af;
+}
+.vscode-light .hljs-title,
+.vscode-light .hljs-doctag {
+ color: #808080;
+}
+
+.vscode-light .hljs-attr {
+ color: #f00;
+}
+
+.vscode-light .hljs-symbol,
+.vscode-light .hljs-bullet,
+.vscode-light .hljs-link {
+ color: #00b0e8;
+}
+
+.vscode-light .hljs-emphasis {
+ font-style: italic;
+}
+
+.vscode-light .hljs-strong {
+ font-weight: bold;
}
input[type='submit'] {
@@ -270,3 +429,208 @@ hr {
img.severity {
height: 0.75em;
}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.spinner {
+ display: inline-block;
+ animation: spin 1s infinite;
+}
+
+button.button-theme-primary,
+button.button-theme-secondary {
+ padding: 6px 14px;
+ border-radius: 5px;
+}
+
+.code-diff-actions {
+ width: 100%;
+ height: 26px;
+ background-color: var(--vscode-editorMarkerNavigationInfo-headerBackground);
+ border-radius: 0 0 3px 3px;
+ overflow: auto;
+ display: flex;
+ flex-direction: row-reverse;
+}
+
+.code-diff-action-button {
+ font-size: 12px;
+ padding: 1px 6.5px;
+ margin: 1px 0;
+ border-radius: 3px;
+ color: currentColor;
+ background-color: rgba(0, 0, 0, 0);
+ transition: all 600ms cubic-bezier(0.25, 1, 0, 1);
+ transform: translate3d(0, 0, 0) scale(1.00001);
+ gap: calc(0.25rem * 1);
+ filter: brightness(0.925);
+ border: none;
+}
+
+.code-diff-action-button:hover {
+ filter: brightness(1);
+}
+
+.code-diff-action-button:hover:after {
+ transform: translate3d(0%, 0, 0);
+ opacity: 0.15;
+}
+
+.code-diff-action-button::after {
+ content: '';
+ pointer-events: none;
+ transition: all 600ms cubic-bezier(0.25, 1, 0, 1);
+ opacity: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ filter: brightness(1.35) saturate(0.75);
+ border-radius: inherit;
+ background-color: currentColor;
+ transform: translate3d(-5%, 0, 0) scale(0.93);
+}
+
+.container-bottom {
+ bottom: 0;
+ justify-content: unset;
+ border-top: 1px solid var(--vscode-menu-separatorBackground);
+ border-bottom: none;
+}
+
+.button-container {
+ padding: 10px 0;
+ flex-wrap: wrap;
+}
+
+.button-container > button {
+ white-space: nowrap;
+ margin-bottom: 8px;
+}
+
+pre.center {
+ display: flex;
+ justify-content: center;
+ padding-top: 16px;
+}
+
+pre.error {
+ color: var(--vscode-diffEditorOverview-removedForeground);
+}
+
+.dot-typing {
+ position: relative;
+ left: -9999px;
+ width: 8px;
+ height: 8px;
+ border-radius: 5px;
+ background-color: var(--vscode-editor-foreground);
+ color: var(--vscode-editor-foreground);
+ box-shadow:
+ 9984px 0 0 0 var(--vscode-editor-foreground),
+ 9999px 0 0 0 var(--vscode-editor-foreground),
+ 10014px 0 0 0 var(--vscode-editor-foreground);
+ animation: dot-typing 1.5s infinite linear;
+}
+
+@keyframes dot-typing {
+ 0% {
+ box-shadow:
+ 9984px 0 0 0 var(--vscode-editor-foreground),
+ 9999px 0 0 0 var(--vscode-editor-foreground),
+ 10014px 0 0 0 var(--vscode-editor-foreground);
+ }
+ 16.667% {
+ box-shadow:
+ 9984px -10px 0 0 var(--vscode-editor-foreground),
+ 9999px 0 0 0 var(--vscode-editor-foreground),
+ 10014px 0 0 0 var(--vscode-editor-foreground);
+ }
+ 33.333% {
+ box-shadow:
+ 9984px 0 0 0 var(--vscode-editor-foreground),
+ 9999px 0 0 0 var(--vscode-editor-foreground),
+ 10014px 0 0 0 var(--vscode-editor-foreground);
+ }
+ 50% {
+ box-shadow:
+ 9984px 0 0 0 var(--vscode-editor-foreground),
+ 9999px -10px 0 0 var(--vscode-editor-foreground),
+ 10014px 0 0 0 var(--vscode-editor-foreground);
+ }
+ 66.667% {
+ box-shadow:
+ 9984px 0 0 0 var(--vscode-editor-foreground),
+ 9999px 0 0 0 var(--vscode-editor-foreground),
+ 10014px 0 0 0 var(--vscode-editor-foreground);
+ }
+ 83.333% {
+ box-shadow:
+ 9984px 0 0 0 var(--vscode-editor-foreground),
+ 9999px 0 0 0 var(--vscode-editor-foreground),
+ 10014px -10px 0 0 var(--vscode-editor-foreground);
+ }
+ 100% {
+ box-shadow:
+ 9984px 0 0 0 var(--vscode-editor-foreground),
+ 9999px 0 0 0 var(--vscode-editor-foreground),
+ 10014px 0 0 0 var(--vscode-editor-foreground);
+ }
+}
+
+.code-block {
+ max-width: fit-content;
+ min-width: 500px;
+}
+
+.code-block pre {
+ border-radius: 3px 3px 0 0;
+}
+
+.line-number {
+ display: inline-block;
+ color: var(--vscode-editorOverviewRuler-selectionHighlightForeground);
+ width: 16px;
+ margin-right: 24px;
+ text-align: right;
+}
+
+.highlight {
+ display: inline-block;
+ background-color: var(--vscode-editorOverviewRuler-selectionHighlightForeground);
+}
+
+.reference-tracker {
+ cursor: help;
+}
+
+.reference-tracker .tooltip {
+ visibility: hidden;
+ opacity: 0;
+ transition:
+ visibility 0s 0.1s,
+ opacity 0.1s linear;
+ position: absolute;
+ background-color: var(--vscode-editor-background);
+ color: var(--vscode-editor-foreground);
+ border: 1px solid var(--vscode-menu-separatorBackground);
+ border-radius: 5px;
+ padding: 16px;
+ margin-top: -80px;
+ margin-left: 20px;
+ cursor: default;
+}
+
+.reference-tracker:hover .tooltip {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity 0.1s linear;
+}
diff --git a/packages/core/resources/icons/aws/amazonq/severity-critical.svg b/packages/core/resources/icons/aws/amazonq/severity-critical.svg
new file mode 100644
index 00000000000..7733994d24e
--- /dev/null
+++ b/packages/core/resources/icons/aws/amazonq/severity-critical.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/core/resources/icons/aws/amazonq/severity-high.svg b/packages/core/resources/icons/aws/amazonq/severity-high.svg
new file mode 100644
index 00000000000..ff92aebc817
--- /dev/null
+++ b/packages/core/resources/icons/aws/amazonq/severity-high.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/core/resources/icons/aws/amazonq/severity-info.svg b/packages/core/resources/icons/aws/amazonq/severity-info.svg
new file mode 100644
index 00000000000..dbf78609170
--- /dev/null
+++ b/packages/core/resources/icons/aws/amazonq/severity-info.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/core/resources/icons/aws/amazonq/severity-low.svg b/packages/core/resources/icons/aws/amazonq/severity-low.svg
new file mode 100644
index 00000000000..4ca6d96961e
--- /dev/null
+++ b/packages/core/resources/icons/aws/amazonq/severity-low.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/core/resources/icons/aws/amazonq/severity-medium.svg b/packages/core/resources/icons/aws/amazonq/severity-medium.svg
new file mode 100644
index 00000000000..a906d9b4873
--- /dev/null
+++ b/packages/core/resources/icons/aws/amazonq/severity-medium.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/core/src/amazonq/commons/baseChatStorage.ts b/packages/core/src/amazonq/commons/baseChatStorage.ts
new file mode 100644
index 00000000000..b0a10c8977b
--- /dev/null
+++ b/packages/core/src/amazonq/commons/baseChatStorage.ts
@@ -0,0 +1,38 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import AsyncLock from 'async-lock'
+
+export abstract class BaseChatSessionStorage {
+ private lock = new AsyncLock()
+ protected sessions: Map = new Map()
+
+ abstract createSession(tabID: string): Promise
+
+ public async getSession(tabID: string): Promise {
+ /**
+ * The lock here is added in order to mitigate amazon Q's eventing fire & forget design when integrating with mynah-ui that creates a race condition here.
+ * The race condition happens when handleDevFeatureCommand in src/amazonq/webview/ui/quickActions/handler.ts is firing two events after each other to amazonqFeatureDev controller
+ * This eventually may make code generation fail as at the moment of that event it may get from the storage a session that has not been properly updated.
+ */
+ return this.lock.acquire(tabID, async () => {
+ const sessionFromStorage = this.sessions.get(tabID)
+ if (sessionFromStorage === undefined) {
+ // If a session doesn't already exist just create it
+ return this.createSession(tabID)
+ }
+ return sessionFromStorage
+ })
+ }
+
+ // Find all sessions that are currently waiting to be authenticated
+ public getAuthenticatingSessions(): T[] {
+ return Array.from(this.sessions.values()).filter((session) => session.isAuthenticating)
+ }
+
+ public deleteSession(tabID: string) {
+ this.sessions.delete(tabID)
+ }
+}
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts b/packages/core/src/amazonq/commons/connector/baseMessenger.ts
similarity index 68%
rename from packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts
rename to packages/core/src/amazonq/commons/connector/baseMessenger.ts
index 29aa741dc80..ab053333432 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/amazonq/commons/connector/baseMessenger.ts
@@ -3,29 +3,37 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { DeletedFileInfo, FollowUpTypes, NewFileInfo } from '../../../types'
-import { AuthFollowUpType, AuthMessageDataMap } from '../../../../amazonq/auth/model'
+import { ChatItemAction, ProgressField } from '@aws/mynah-ui'
+import { AuthFollowUpType, AuthMessageDataMap } from '../../../amazonq/auth/model'
+import { FeatureAuthState } from '../../../codewhisperer'
+import { i18n } from '../../../shared/i18n-helper'
+import { CodeReference } from '../../../amazonq/webview/ui/connector'
+
+import { MessengerTypes } from '../../../amazonqFeatureDev/controllers/chat/messenger/constants'
import {
- ChatMessage,
+ AppToWebViewMessageDispatcher,
AsyncEventProgressMessage,
- CodeResultMessage,
- UpdatePlaceholderMessage,
- ChatInputEnabledMessage,
AuthenticationUpdateMessage,
AuthNeededException,
- OpenNewTabMessage,
+ ChatInputEnabledMessage,
+ ChatMessage,
+ CodeResultMessage,
FileComponent,
+ FolderConfirmationMessage,
+ OpenNewTabMessage,
UpdateAnswerMessage,
-} from '../../../views/connector/connector'
-import { AppToWebViewMessageDispatcher } from '../../../views/connector/connector'
-import { ChatItemAction } from '@aws/mynah-ui'
-import { messageWithConversationId } from '../../../userFacingText'
-import { MessengerTypes } from './constants'
-import { FeatureAuthState } from '../../../../codewhisperer'
-import { CodeReference } from '../../../../codewhispererChat/view/connector/connector'
-import { i18n } from '../../../../shared/i18n-helper'
+ UpdatePlaceholderMessage,
+ UpdatePromptProgressMessage,
+} from './connectorMessages'
+import { FollowUpTypes } from '../types'
+import { messageWithConversationId } from '../../../amazonqFeatureDev/userFacingText'
+import { DeletedFileInfo, NewFileInfo } from '../../../amazonqFeatureDev/types'
+
export class Messenger {
- public constructor(private readonly dispatcher: AppToWebViewMessageDispatcher) {}
+ public constructor(
+ private readonly dispatcher: AppToWebViewMessageDispatcher,
+ private readonly sender: string
+ ) {}
public sendAnswer(params: {
message?: string
@@ -35,6 +43,7 @@ export class Messenger {
canBeVoted?: boolean
snapToTop?: boolean
messageId?: string
+ disableChatInput?: boolean
}) {
this.dispatcher.sendChatMessage(
new ChatMessage(
@@ -47,9 +56,13 @@ export class Messenger {
snapToTop: params.snapToTop ?? false,
messageId: params.messageId,
},
- params.tabID
+ params.tabID,
+ this.sender
)
)
+ if (params.disableChatInput) {
+ this.sendChatInputEnabled(params.tabID, false)
+ }
}
public sendFeedback(tabID: string) {
@@ -76,6 +89,23 @@ export class Messenger {
this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.chatInputDisabled'))
}
+ public sendUpdatePromptProgress(tabID: string, progressField: ProgressField | null) {
+ this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, this.sender, progressField))
+ }
+
+ public sendFolderConfirmationMessage(
+ tabID: string,
+ message: string,
+ folderPath: string,
+ followUps?: ChatItemAction[]
+ ) {
+ this.dispatcher.sendFolderConfirmationMessage(
+ new FolderConfirmationMessage(tabID, this.sender, message, folderPath, followUps)
+ )
+
+ this.sendChatInputEnabled(tabID, false)
+ }
+
public sendErrorMessage(
errorMessage: string,
tabID: string,
@@ -123,12 +153,12 @@ export class Messenger {
codeGenerationId: string
) {
this.dispatcher.sendCodeResult(
- new CodeResultMessage(filePaths, deletedFiles, references, tabID, uploadId, codeGenerationId)
+ new CodeResultMessage(filePaths, deletedFiles, references, tabID, this.sender, uploadId, codeGenerationId)
)
}
public sendAsyncEventProgress(tabID: string, inProgress: boolean, message: string | undefined) {
- this.dispatcher.sendAsyncEventProgress(new AsyncEventProgressMessage(tabID, inProgress, message))
+ this.dispatcher.sendAsyncEventProgress(new AsyncEventProgressMessage(tabID, this.sender, inProgress, message))
}
public updateFileComponent(
@@ -139,7 +169,7 @@ export class Messenger {
disableFileActions: boolean
) {
this.dispatcher.updateFileComponent(
- new FileComponent(tabID, filePaths, deletedFiles, messageId, disableFileActions)
+ new FileComponent(tabID, this.sender, filePaths, deletedFiles, messageId, disableFileActions)
)
}
@@ -148,16 +178,16 @@ export class Messenger {
}
public sendUpdatePlaceholder(tabID: string, newPlaceholder: string) {
- this.dispatcher.sendPlaceholder(new UpdatePlaceholderMessage(tabID, newPlaceholder))
+ this.dispatcher.sendPlaceholder(new UpdatePlaceholderMessage(tabID, this.sender, newPlaceholder))
}
public sendChatInputEnabled(tabID: string, enabled: boolean) {
- this.dispatcher.sendChatInputEnabled(new ChatInputEnabledMessage(tabID, enabled))
+ this.dispatcher.sendChatInputEnabled(new ChatInputEnabledMessage(tabID, this.sender, enabled))
}
- public sendAuthenticationUpdate(featureDevEnabled: boolean, authenticatingTabIDs: string[]) {
+ public sendAuthenticationUpdate(enabled: boolean, authenticatingTabIDs: string[]) {
this.dispatcher.sendAuthenticationUpdate(
- new AuthenticationUpdateMessage(featureDevEnabled, authenticatingTabIDs)
+ new AuthenticationUpdateMessage(this.sender, enabled, authenticatingTabIDs)
)
}
@@ -180,10 +210,10 @@ export class Messenger {
break
}
- this.dispatcher.sendAuthNeededExceptionMessage(new AuthNeededException(message, authType, tabID))
+ this.dispatcher.sendAuthNeededExceptionMessage(new AuthNeededException(message, authType, tabID, this.sender))
}
public openNewTask() {
- this.dispatcher.sendOpenNewTask(new OpenNewTabMessage())
+ this.dispatcher.sendOpenNewTask(new OpenNewTabMessage(this.sender))
}
}
diff --git a/packages/core/src/amazonqFeatureDev/views/connector/connector.ts b/packages/core/src/amazonq/commons/connector/connectorMessages.ts
similarity index 71%
rename from packages/core/src/amazonqFeatureDev/views/connector/connector.ts
rename to packages/core/src/amazonq/commons/connector/connectorMessages.ts
index a45731cc11a..f5ac0d4b21a 100644
--- a/packages/core/src/amazonqFeatureDev/views/connector/connector.ts
+++ b/packages/core/src/amazonq/commons/connector/connectorMessages.ts
@@ -3,20 +3,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { AuthFollowUpType } from '../../../amazonq/auth/model'
-import { MessagePublisher } from '../../../amazonq/messages/messagePublisher'
-import { CodeReference } from '../../../amazonq/webview/ui/connector'
-import { featureDevChat, licenseText } from '../../constants'
-import { ChatItemAction, SourceLink } from '@aws/mynah-ui'
-import { DeletedFileInfo, NewFileInfo } from '../../types'
-import { ChatItemType } from '../../../amazonq/commons/model'
+import { AuthFollowUpType } from '../../auth/model'
+import { MessagePublisher } from '../../messages/messagePublisher'
+import { CodeReference } from '../../webview/ui/connector'
+import { ChatItemAction, ProgressField, SourceLink } from '@aws/mynah-ui'
+import { ChatItemType } from '../model'
+import { DeletedFileInfo, NewFileInfo } from '../../../amazonqFeatureDev/types'
+import { licenseText } from '../../../amazonqFeatureDev/constants'
class UiMessage {
readonly time: number = Date.now()
- readonly sender: string = featureDevChat
readonly type: string = ''
- public constructor(protected tabID: string) {}
+ public constructor(
+ protected tabID: string,
+ protected sender: string
+ ) {}
}
export class ErrorMessage extends UiMessage {
@@ -24,8 +26,8 @@ export class ErrorMessage extends UiMessage {
readonly message!: string
override type = 'errorMessage'
- constructor(title: string, message: string, tabID: string) {
- super(tabID)
+ constructor(title: string, message: string, tabID: string, sender: string) {
+ super(tabID, sender)
this.title = title
this.message = message
}
@@ -49,10 +51,11 @@ export class CodeResultMessage extends UiMessage {
readonly deletedFiles: DeletedFileInfo[],
references: CodeReference[],
tabID: string,
+ sender: string,
conversationID: string,
codeGenerationId: string
) {
- super(tabID)
+ super(tabID, sender)
this.references = references
.filter((ref) => ref.licenseName && ref.repository && ref.url)
.map((ref) => {
@@ -71,17 +74,51 @@ export class CodeResultMessage extends UiMessage {
}
}
+export class FolderConfirmationMessage extends UiMessage {
+ readonly folderPath: string
+ readonly message: string
+ readonly followUps?: ChatItemAction[]
+ override type = 'folderConfirmationMessage'
+ constructor(tabID: string, sender: string, message: string, folderPath: string, followUps?: ChatItemAction[]) {
+ super(tabID, sender)
+ this.message = message
+ this.folderPath = folderPath
+ this.followUps = followUps
+ }
+}
+
+export class UpdatePromptProgressMessage extends UiMessage {
+ readonly progressField: ProgressField | null
+ override type = 'updatePromptProgress'
+ constructor(tabID: string, sender: string, progressField: ProgressField | null) {
+ super(tabID, sender)
+ this.progressField = progressField
+ }
+}
+
export class AsyncEventProgressMessage extends UiMessage {
readonly inProgress: boolean
readonly message: string | undefined
override type = 'asyncEventProgressMessage'
- constructor(tabID: string, inProgress: boolean, message: string | undefined) {
- super(tabID)
+ constructor(tabID: string, sender: string, inProgress: boolean, message: string | undefined) {
+ super(tabID, sender)
this.inProgress = inProgress
this.message = message
}
}
+
+export class AuthenticationUpdateMessage {
+ readonly time: number = Date.now()
+ readonly type = 'authenticationUpdateMessage'
+
+ constructor(
+ readonly sender: string,
+ readonly featureEnabled: boolean,
+ readonly authenticatingTabIDs: string[]
+ ) {}
+}
+
export class FileComponent extends UiMessage {
readonly filePaths: NewFileInfo[]
readonly deletedFiles: DeletedFileInfo[]
@@ -91,12 +128,13 @@ export class FileComponent extends UiMessage {
constructor(
tabID: string,
+ sender: string,
filePaths: NewFileInfo[],
deletedFiles: DeletedFileInfo[],
messageId: string,
disableFileActions: boolean
) {
- super(tabID)
+ super(tabID, sender)
this.filePaths = filePaths
this.deletedFiles = deletedFiles
this.messageId = messageId
@@ -108,8 +146,8 @@ export class UpdatePlaceholderMessage extends UiMessage {
readonly newPlaceholder: string
override type = 'updatePlaceholderMessage'
- constructor(tabID: string, newPlaceholder: string) {
- super(tabID)
+ constructor(tabID: string, sender: string, newPlaceholder: string) {
+ super(tabID, sender)
this.newPlaceholder = newPlaceholder
}
}
@@ -118,29 +156,17 @@ export class ChatInputEnabledMessage extends UiMessage {
readonly enabled: boolean
override type = 'chatInputEnabledMessage'
- constructor(tabID: string, enabled: boolean) {
- super(tabID)
+ constructor(tabID: string, sender: string, enabled: boolean) {
+ super(tabID, sender)
this.enabled = enabled
}
}
export class OpenNewTabMessage {
readonly time: number = Date.now()
- readonly sender: string = featureDevChat
readonly type = 'openNewTabMessage'
-}
-
-export class AuthenticationUpdateMessage {
- readonly time: number = Date.now()
- readonly sender: string = featureDevChat
- readonly featureDevEnabled: boolean
- readonly authenticatingTabIDs: string[]
- readonly type = 'authenticationUpdateMessage'
- constructor(featureDevEnabled: boolean, authenticatingTabIDs: string[]) {
- this.featureDevEnabled = featureDevEnabled
- this.authenticatingTabIDs = authenticatingTabIDs
- }
+ constructor(protected sender: string) {}
}
export class AuthNeededException extends UiMessage {
@@ -148,8 +174,8 @@ export class AuthNeededException extends UiMessage {
readonly authType: AuthFollowUpType
override type = 'authNeededException'
- constructor(message: string, authType: AuthFollowUpType, tabID: string) {
- super(tabID)
+ constructor(message: string, authType: AuthFollowUpType, tabID: string, sender: string) {
+ super(tabID, sender)
this.message = message
this.authType = authType
}
@@ -176,8 +202,8 @@ export class ChatMessage extends UiMessage {
readonly messageId: string | undefined
override type = 'chatMessage'
- constructor(props: ChatMessageProps, tabID: string) {
- super(tabID)
+ constructor(props: ChatMessageProps, tabID: string, sender: string) {
+ super(tabID, sender)
this.message = props.message
this.messageType = props.messageType
this.followUps = props.followUps
@@ -200,8 +226,8 @@ export class UpdateAnswerMessage extends UiMessage {
readonly followUps: ChatItemAction[] | undefined
override type = 'updateChatAnswer'
- constructor(props: UpdateAnswerMessageProps, tabID: string) {
- super(tabID)
+ constructor(props: UpdateAnswerMessageProps, tabID: string, sender: string) {
+ super(tabID, sender)
this.messageId = props.messageId
this.messageType = props.messageType
this.followUps = props.followUps
@@ -223,6 +249,14 @@ export class AppToWebViewMessageDispatcher {
this.appsToWebViewMessagePublisher.publish(message)
}
+ public sendUpdatePromptProgress(message: UpdatePromptProgressMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
+ public sendFolderConfirmationMessage(message: FolderConfirmationMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
public sendAsyncEventProgress(message: AsyncEventProgressMessage) {
this.appsToWebViewMessagePublisher.publish(message)
}
@@ -235,11 +269,11 @@ export class AppToWebViewMessageDispatcher {
this.appsToWebViewMessagePublisher.publish(message)
}
- public sendAuthenticationUpdate(message: AuthenticationUpdateMessage) {
+ public sendAuthNeededExceptionMessage(message: AuthNeededException) {
this.appsToWebViewMessagePublisher.publish(message)
}
- public sendAuthNeededExceptionMessage(message: AuthNeededException) {
+ public sendAuthenticationUpdate(message: AuthenticationUpdateMessage) {
this.appsToWebViewMessagePublisher.publish(message)
}
diff --git a/packages/core/src/amazonq/commons/controllers/contentController.ts b/packages/core/src/amazonq/commons/controllers/contentController.ts
index 821e2988f96..0af3b317025 100644
--- a/packages/core/src/amazonq/commons/controllers/contentController.ts
+++ b/packages/core/src/amazonq/commons/controllers/contentController.ts
@@ -17,7 +17,7 @@ import {
} from '../../../shared/utilities/textDocumentUtilities'
import { extractFileAndCodeSelectionFromMessage, fs, getErrorMsg, ToolkitError } from '../../../shared'
-class ContentProvider implements vscode.TextDocumentContentProvider {
+export class ContentProvider implements vscode.TextDocumentContentProvider {
constructor(private uri: vscode.Uri) {}
provideTextDocumentContent(_uri: vscode.Uri) {
diff --git a/packages/core/src/amazonq/commons/diff.ts b/packages/core/src/amazonq/commons/diff.ts
index 97fde96c389..beb45d88096 100644
--- a/packages/core/src/amazonq/commons/diff.ts
+++ b/packages/core/src/amazonq/commons/diff.ts
@@ -4,34 +4,37 @@
*/
import * as vscode from 'vscode'
-import { featureDevScheme } from '../../amazonqFeatureDev/constants'
import { fs } from '../../shared'
import { diffLines } from 'diff'
-export async function openDiff(leftPath: string, rightPath: string, tabId: string) {
- const { left, right } = await getFileDiffUris(leftPath, rightPath, tabId)
+export async function openDiff(leftPath: string, rightPath: string, tabId: string, scheme: string) {
+ const { left, right } = await getFileDiffUris(leftPath, rightPath, tabId, scheme)
await vscode.commands.executeCommand('vscode.diff', left, right)
}
-export async function openDeletedDiff(filePath: string, name: string, tabId: string) {
- const left = await getOriginalFileUri(filePath, tabId)
- const right = createAmazonQUri('empty', tabId)
+export async function openDeletedDiff(filePath: string, name: string, tabId: string, scheme: string) {
+ const left = await getOriginalFileUri(filePath, tabId, scheme)
+ const right = createAmazonQUri('empty', tabId, scheme)
await vscode.commands.executeCommand('vscode.diff', left, right, `${name} (Deleted)`)
}
-export async function getOriginalFileUri(fullPath: string, tabId: string) {
- return (await fs.exists(fullPath)) ? vscode.Uri.file(fullPath) : createAmazonQUri('empty', tabId)
+export async function getOriginalFileUri(fullPath: string, tabId: string, scheme: string) {
+ return (await fs.exists(fullPath)) ? vscode.Uri.file(fullPath) : createAmazonQUri('empty', tabId, scheme)
}
-export async function getFileDiffUris(leftPath: string, rightPath: string, tabId: string) {
- const left = await getOriginalFileUri(leftPath, tabId)
- const right = createAmazonQUri(rightPath, tabId)
+export async function getFileDiffUris(leftPath: string, rightPath: string, tabId: string, scheme: string) {
+ const left = await getOriginalFileUri(leftPath, tabId, scheme)
+ const right = createAmazonQUri(rightPath, tabId, scheme)
return { left, right }
}
-export async function computeDiff(leftPath: string, rightPath: string, tabId: string) {
- const { left, right } = await getFileDiffUris(leftPath, rightPath, tabId)
+export function createAmazonQUri(path: string, tabId: string, scheme: string) {
+ return vscode.Uri.from({ scheme: scheme, path, query: `tabID=${tabId}` })
+}
+
+export async function computeDiff(leftPath: string, rightPath: string, tabId: string, scheme: string) {
+ const { left, right } = await getFileDiffUris(leftPath, rightPath, tabId, scheme)
const leftFile = await vscode.workspace.openTextDocument(left)
const rightFile = await vscode.workspace.openTextDocument(right)
@@ -57,8 +60,3 @@ export async function computeDiff(leftPath: string, rightPath: string, tabId: st
})
return { changes, charsAdded, linesAdded, charsRemoved, linesRemoved }
}
-
-export function createAmazonQUri(path: string, tabId: string) {
- // TODO change the featureDevScheme to a more general amazon q scheme
- return vscode.Uri.from({ scheme: featureDevScheme, path, query: `tabID=${tabId}` })
-}
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionConfigFactory.ts b/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts
similarity index 69%
rename from packages/core/src/amazonqFeatureDev/session/sessionConfigFactory.ts
rename to packages/core/src/amazonq/commons/session/sessionConfigFactory.ts
index 6f98e1b3664..d6dff48fbe5 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionConfigFactory.ts
+++ b/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts
@@ -4,11 +4,9 @@
*/
import * as vscode from 'vscode'
-import { featureDevScheme } from '../constants'
-import { VirtualFileSystem } from '../../shared/virtualFilesystem'
-import { VirtualMemoryFile } from '../../shared/virtualMemoryFile'
-import { WorkspaceFolderNotFoundError } from '../errors'
-import { CurrentWsFolders } from '../types'
+import { WorkspaceFolderNotFoundError } from '../../../amazonqFeatureDev/errors'
+import { VirtualFileSystem, VirtualMemoryFile } from '../../../shared'
+import { CurrentWsFolders } from '../../../amazonqFeatureDev/types'
export interface SessionConfig {
// The paths on disk to where the source code lives
@@ -21,7 +19,7 @@ export interface SessionConfig {
* Factory method for creating session configurations
* @returns An instantiated SessionConfig, using either the arguments provided or the defaults
*/
-export async function createSessionConfig(): Promise {
+export async function createSessionConfig(scheme: string): Promise {
const workspaceFolders = vscode.workspace.workspaceFolders
const firstFolder = workspaceFolders?.[0]
if (workspaceFolders === undefined || workspaceFolders.length === 0 || firstFolder === undefined) {
@@ -33,10 +31,7 @@ export async function createSessionConfig(): Promise {
const fs = new VirtualFileSystem()
// Register an empty featureDev file that's used when a new file is being added by the LLM
- fs.registerProvider(
- vscode.Uri.from({ scheme: featureDevScheme, path: 'empty' }),
- new VirtualMemoryFile(new Uint8Array())
- )
+ fs.registerProvider(vscode.Uri.from({ scheme, path: 'empty' }), new VirtualMemoryFile(new Uint8Array()))
return Promise.resolve({ workspaceRoots, fs, workspaceFolders: [firstFolder, ...workspaceFolders.slice(1)] })
}
diff --git a/packages/core/src/amazonq/commons/types.ts b/packages/core/src/amazonq/commons/types.ts
new file mode 100644
index 00000000000..1016f5c0669
--- /dev/null
+++ b/packages/core/src/amazonq/commons/types.ts
@@ -0,0 +1,38 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export enum FollowUpTypes {
+ //UnitTestGeneration
+ ViewDiff = 'ViewDiff',
+ AcceptCode = 'AcceptCode',
+ RejectCode = 'RejectCode',
+ BuildAndExecute = 'BuildAndExecute',
+ ModifyCommands = 'ModifyCommands',
+ SkipBuildAndFinish = 'SkipBuildAndFinish',
+ InstallDependenciesAndContinue = 'InstallDependenciesAndContinue',
+ ContinueBuildAndExecute = 'ContinueBuildAndExecute',
+ ViewCodeDiffAfterIteration = 'ViewCodeDiffAfterIteration',
+ //FeatureDev
+ GenerateCode = 'GenerateCode',
+ InsertCode = 'InsertCode',
+ ProvideFeedbackAndRegenerateCode = 'ProvideFeedbackAndRegenerateCode',
+ Retry = 'Retry',
+ ModifyDefaultSourceFolder = 'ModifyDefaultSourceFolder',
+ DevExamples = 'DevExamples',
+ NewTask = 'NewTask',
+ CloseSession = 'CloseSession',
+ SendFeedback = 'SendFeedback',
+ // Doc
+ CreateDocumentation = 'CreateDocumentation',
+ ChooseFolder = 'ChooseFolder',
+ UpdateDocumentation = 'UpdateDocumentation',
+ SynchronizeDocumentation = 'SynchronizeDocumentation',
+ EditDocumentation = 'EditDocumentation',
+ AcceptChanges = 'AcceptChanges',
+ RejectChanges = 'RejectChanges',
+ MakeChanges = 'MakeChanges',
+ ProceedFolderSelection = 'ProceedFolderSelection',
+ CancelFolderSelection = 'CancelFolderSelection',
+}
diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts
index fefa706b382..5bd20e4dfd0 100644
--- a/packages/core/src/amazonq/index.ts
+++ b/packages/core/src/amazonq/index.ts
@@ -22,6 +22,8 @@ export { AmazonQChatViewProvider } from './webview/webView'
export { init as cwChatAppInit } from '../codewhispererChat/app'
export { init as featureDevChatAppInit } from '../amazonqFeatureDev/app'
export { init as gumbyChatAppInit } from '../amazonqGumby/app'
+export { init as testChatAppInit } from '../amazonqTest/app'
+export { init as docChatAppInit } from '../amazonqDoc/app'
export { activateBadge } from './util/viewBadgeHandler'
export { amazonQHelpUrl } from '../shared/constants'
export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/statusBarMenu'
@@ -35,9 +37,12 @@ export {
getFileDiffUris,
computeDiff,
} from './commons/diff'
+export { AuthFollowUpType, AuthMessageDataMap } from './auth/model'
+export { ChatItemType } from './commons/model'
+export { ExtensionMessage } from '../amazonq/webview/ui/commands'
export { CodeReference } from '../codewhispererChat/view/connector/connector'
-export { AuthMessageDataMap, AuthFollowUpType } from './auth/model'
export { extractAuthFollowUp } from './util/authUtils'
+export { Messenger } from './commons/connector/baseMessenger'
import { FeatureContext } from '../shared'
/**
@@ -54,7 +59,9 @@ export function createMynahUI(
) {
if (typeof window !== 'undefined') {
const mynahUI = require('./webview/ui/main')
- return mynahUI.createMynahUI(ideApi, amazonQEnabled, featureConfigsSerialized, disabledCommands)
+ return mynahUI.createMynahUI(ideApi, amazonQEnabled, featureConfigsSerialized, true, disabledCommands)
}
throw new Error('Not implemented for node')
}
+
+export * from './commons/types'
diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts
index bfc88125abd..1d3dd2743e9 100644
--- a/packages/core/src/amazonq/lsp/lspClient.ts
+++ b/packages/core/src/amazonq/lsp/lspClient.ts
@@ -26,6 +26,8 @@ import {
QueryVectorIndexRequestType,
UpdateIndexV2RequestPayload,
UpdateIndexV2RequestType,
+ QueryRepomapIndexRequestType,
+ GetRepomapIndexJSONRequestType,
Usage,
} from './types'
import { Writable } from 'stream'
@@ -139,6 +141,31 @@ export class LspClient {
return undefined
}
}
+ async queryRepomapIndex(filePaths: string[]) {
+ try {
+ const request = JSON.stringify({
+ filePaths: filePaths,
+ })
+ const resp: any = await this.client?.sendRequest(QueryRepomapIndexRequestType, await this.encrypt(request))
+ return resp
+ } catch (e) {
+ getLogger().error(`LspClient: QueryRepomapIndex error: ${e}`)
+ throw e
+ }
+ }
+ async getRepoMapJSON() {
+ try {
+ const request = JSON.stringify({})
+ const resp: any = await this.client?.sendRequest(
+ GetRepomapIndexJSONRequestType,
+ await this.encrypt(request)
+ )
+ return resp
+ } catch (e) {
+ getLogger().error(`LspClient: queryInlineProjectContext error: ${e}`)
+ throw e
+ }
+ }
}
/**
* Activates the language server, this will start LSP server running over IPC protocol.
diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts
index 29fd0ab68d3..7a74318dd14 100644
--- a/packages/core/src/amazonq/lsp/lspController.ts
+++ b/packages/core/src/amazonq/lsp/lspController.ts
@@ -372,6 +372,9 @@ export class LspController {
})
} finally {
this._isIndexingInProgress = false
+ const repomapFile = await LspClient.instance.getRepoMapJSON()
+ // console.log(repomapFile)
+ getLogger().info(`File path ${repomapFile}`)
}
}
diff --git a/packages/core/src/amazonq/lsp/types.ts b/packages/core/src/amazonq/lsp/types.ts
index 6a2cab57d8e..fe1df5ed3bc 100644
--- a/packages/core/src/amazonq/lsp/types.ts
+++ b/packages/core/src/amazonq/lsp/types.ts
@@ -65,3 +65,14 @@ export const QueryVectorIndexRequestType: RequestType = new RequestType(
+ 'lsp/queryRepomapIndex'
+)
+export type GetRepomapIndexJSONRequest = string
+export const GetRepomapIndexJSONRequestType: RequestType = new RequestType(
+ 'lsp/getRepomapIndexJSON'
+)
diff --git a/packages/core/src/amazonq/onboardingPage/walkthrough.ts b/packages/core/src/amazonq/onboardingPage/walkthrough.ts
index 63c5db3a87f..30e31ac1055 100644
--- a/packages/core/src/amazonq/onboardingPage/walkthrough.ts
+++ b/packages/core/src/amazonq/onboardingPage/walkthrough.ts
@@ -34,7 +34,7 @@ export async function showAmazonQWalkthroughOnce(showWalkthrough = () => openAma
* Opens the Amazon Q Walkthrough.
* We wrap the actual command so that we can get telemetry from it.
*/
-export const openAmazonQWalkthrough = Commands.declare(`_aws.amazonq.walkthrough.show`, () => async () => {
+export const openAmazonQWalkthrough = Commands.declare(`aws.amazonq.walkthrough.show`, () => async () => {
await vscode.commands.executeCommand(
'workbench.action.openWalkthrough',
`${VSCODE_EXTENSION_ID.amazonq}#aws.amazonq.walkthrough`
@@ -69,7 +69,7 @@ fake_users = [
export const walkthroughSecurityScanExample = Commands.declare(
`_aws.amazonq.walkthrough.securityScanExample`,
() => async () => {
- const filterText = localize('AWS.command.amazonq.security.scan', 'Run Project Scan')
+ const filterText = localize('AWS.command.amazonq.security.scan', 'Run Project Review')
void vscode.commands.executeCommand('workbench.action.quickOpen', `> ${filterText}`)
}
)
diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts
index da1492f467f..fb83ab895a6 100644
--- a/packages/core/src/amazonq/webview/generators/webViewContent.ts
+++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts
@@ -23,7 +23,7 @@ export class WebViewContentGenerator {
return JSON.stringify(Array.from(featureConfigs.entries()))
}
- public async generate(extensionURI: Uri, webView: Webview): Promise {
+ public async generate(extensionURI: Uri, webView: Webview, showWelcomePage: boolean): Promise {
const entrypoint = process.env.WEBPACK_DEVELOPER_SERVER
? 'http: localhost'
: 'https: file+.vscode-resources.vscode-cdn.net'
@@ -47,14 +47,14 @@ export class WebViewContentGenerator {
Amazon Q (Preview)
- ${await this.generateJS(extensionURI, webView)}
+ ${await this.generateJS(extensionURI, webView, showWelcomePage)}