diff --git a/.eslintrc.js b/.eslintrc.js
index 5d196c59541..a25a9d18003 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -12,7 +12,7 @@ module.exports = {
mocha: true,
es2024: true,
},
- plugins: ['@typescript-eslint', 'unicorn', 'header', 'security-node', 'aws-toolkits'],
+ plugins: ['@typescript-eslint', '@stylistic', 'unicorn', 'header', 'security-node', 'aws-toolkits'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
@@ -113,6 +113,20 @@ module.exports = {
'no-constant-condition': ['error', { checkLoops: false }],
'no-empty': 'off',
+ // https://eslint.style/rules/default/spaced-comment
+ // Require space after // comment.
+ '@stylistic/spaced-comment': [
+ 'error',
+ 'always',
+ {
+ block: {
+ markers: ['!'], // Allow the /*!…*/ license header.
+ // exceptions: ['*'],
+ // balanced: true
+ },
+ },
+ ],
+
// Rules from https://github.com/sindresorhus/eslint-plugin-unicorn
// TODO: 'unicorn/no-useless-promise-resolve-reject': 'error',
// TODO: 'unicorn/prefer-at': 'error',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9d6c9e267f7..c32f432ddac 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,6 +1,6 @@
* @aws/aws-ides-team
packages/core/src/codewhisperer/ @aws/codewhisperer-team
packages/core/src/amazonqFeatureDev/ @aws/earlybird
-packages/core/src/codewhispererChat/ @aws/dexp
-packages/core/src/amazonq/ @aws/dexp
+packages/core/src/codewhispererChat/ @aws/flare
+packages/core/src/amazonq/ @aws/flare
packages/core/src/awsService/accessanalyzer/ @aws/access-analyzer
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 7889305394b..929388bc246 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -6,6 +6,7 @@
---
-
+- Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time.
+- Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
diff --git a/.github/workflows/copyPasteDetection.yml b/.github/workflows/copyPasteDetection.yml
deleted file mode 100644
index ad78e409fca..00000000000
--- a/.github/workflows/copyPasteDetection.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-# # github actions: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs
-# # setup-node: https://github.com/actions/setup-node
-
-# name: Copy-Paste Detection
-
-# on:
-# pull_request:
-# branches: [master, feature/*, staging]
-
-# jobs:
-# jscpd:
-# runs-on: ubuntu-latest
-# strategy:
-# matrix:
-# node-version: [18.x]
-# env:
-# NODE_OPTIONS: '--max-old-space-size=8192'
-
-# steps:
-# - uses: actions/checkout@v4
-# with:
-# fetch-depth: 0
-
-# - name: Use Node.js ${{ matrix.node-version }}
-# uses: actions/setup-node@v4
-# with:
-# node-version: ${{ matrix.node-version }}
-
-# - name: Fetch fork upstream
-# run: |
-# git remote add forkUpstream https://github.com/${{ github.event.pull_request.head.repo.full_name }} # URL of the fork
-# git fetch forkUpstream # Fetch fork
-
-# - name: Determine base and target branches for comparison.
-# run: |
-# echo "CURRENT_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
-# echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV
-# - run: git diff --name-only origin/$TARGET_BRANCH forkUpstream/$CURRENT_BRANCH > diff_output.txt
-# - run: |
-# npm install -g jscpd
-
-# - run: jscpd --config "$GITHUB_WORKSPACE/.github/workflows/jscpd.json"
-
-# - if: always()
-# uses: actions/upload-artifact@v4
-# with:
-# name: unfiltered-jscpd-report
-# path: ./jscpd-report.json
-
-# - name: Filter jscpd report for changed files
-# run: |
-# if [ ! -f ./jscpd-report.json ]; then
-# echo "jscpd-report.json not found"
-# exit 1
-# fi
-# echo "Filtering jscpd report for changed files..."
-# CHANGED_FILES=$(jq -R -s -c 'split("\n")[:-1]' diff_output.txt)
-# echo "Changed files: $CHANGED_FILES"
-# jq --argjson changed_files "$CHANGED_FILES" '
-# .duplicates | map(select(
-# (.firstFile?.name as $fname | $changed_files | any(. == $fname)) or
-# (.secondFile?.name as $sname | $changed_files | any(. == $sname))
-# ))
-# ' ./jscpd-report.json > filtered-jscpd-report.json
-# cat filtered-jscpd-report.json
-
-# - name: Check for duplicates
-# run: |
-# if [ $(wc -l < ./filtered-jscpd-report.json) -gt 1 ]; then
-# echo "filtered_report_exists=true" >> $GITHUB_ENV
-# else
-# echo "filtered_report_exists=false" >> $GITHUB_ENV
-# fi
-# - name: upload filtered report (if applicable)
-# if: env.filtered_report_exists == 'true'
-# uses: actions/upload-artifact@v4
-# with:
-# name: filtered-jscpd-report
-# path: ./filtered-jscpd-report.json
-
-# - name: Fail and log found duplicates.
-# if: env.filtered_report_exists == 'true'
-# run: |
-# cat ./filtered-jscpd-report.json
-# echo "Duplications found, failing the check."
-# exit 1
diff --git a/.github/workflows/lintbranch.js b/.github/workflows/lintbranch.js
new file mode 100644
index 00000000000..05fc677dac5
--- /dev/null
+++ b/.github/workflows/lintbranch.js
@@ -0,0 +1,67 @@
+// Check that branch name conforms to GitHub naming convention:
+// https://docs.github.com/en/get-started/using-git/dealing-with-special-characters-in-branch-and-tag-names#naming-branches-and-tags
+
+// To run self-tests,
+// node lintbranch.js test
+// TODO: deduplicate code from lintbranch.js and lintcommit.js.
+
+function isValid(branchName) {
+ const branchNameRegex = /^[a-zA-Z][a-zA-Z0-9._/-]*$/
+
+ return branchNameRegex.test(branchName)
+}
+
+function run(branchName) {
+ if (isValid(branchName)) {
+ console.log(`Branch name "${branchName}" is valid.`)
+ process.exit(0)
+ } else {
+ const helpUrl =
+ 'https://docs.github.com/en/get-started/using-git/dealing-with-special-characters-in-branch-and-tag-names#naming-branches-and-tags'
+ console.log(`Branch name "${branchName}" is invalid see ${helpUrl} for more information.`)
+ process.exit(1)
+ }
+}
+
+function _test() {
+ const tests = {
+ 'feature/branch-name': true,
+ feature_123: true,
+ 'my-branch': true,
+ '123invalid-start': false,
+ '!invalid@start': false,
+ '': false,
+ 'another/valid-name134': true,
+ 'feature/123";id;{echo,Y2F0IC9ldGMvcGFzc3dk}|{base64,-d}|{bash,-i};#': false,
+ }
+
+ let passed = 0
+ let failed = 0
+
+ for (const [branchName, expected] of Object.entries(tests)) {
+ const result = isValid(branchName)
+ if (result === expected) {
+ console.log(`✅ Test passed for "${branchName}"`)
+ passed++
+ } else {
+ console.log(`❌ Test failed for "${branchName}" (expected "${expected}", got "${result}")`)
+ failed++
+ }
+ }
+
+ console.log(`\n${passed} tests passed, ${failed} tests failed`)
+}
+
+function main() {
+ const mode = process.argv[2]
+
+ if (mode === 'test') {
+ _test()
+ } else if (mode === 'run') {
+ run(process.argv[3])
+ } else {
+ throw new Error(`Unknown mode: ${mode}`)
+ }
+}
+
+main()
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index b9e1529958f..a7541f39b9c 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -32,6 +32,12 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '20'
+ - name: Validate Branch name
+ if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref != ''}}
+ env:
+ BRANCH_NAME: ${{ github.event.pull_request.head.ref }}
+ run: |
+ node "$GITHUB_WORKSPACE/.github/workflows/lintbranch.js" run "$BRANCH_NAME"
- name: Check PR title
run: |
node "$GITHUB_WORKSPACE/.github/workflows/lintcommit.js"
@@ -55,6 +61,87 @@ jobs:
- run: npm run testCompile
- run: npm run lint
+ jscpd:
+ needs: lint-commits
+ if: ${{ github.event_name == 'pull_request'}}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [18.x]
+ env:
+ NODE_OPTIONS: '--max-old-space-size=8192'
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - name: Fetch fork upstream
+ env:
+ REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
+ run: |
+ git remote add forkUpstream https://github.com/$REPO_NAME # URL of the fork
+ git fetch forkUpstream # Fetch fork
+
+ - name: Compute git diff
+ env:
+ CURRENT_BRANCH: ${{ github.head_ref }}
+ TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
+ run: git diff --name-only origin/$TARGET_BRANCH forkUpstream/$CURRENT_BRANCH > diff_output.txt
+
+ - run: npm install -g jscpd
+
+ - run: jscpd --config "$GITHUB_WORKSPACE/.github/workflows/jscpd.json"
+
+ - if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: unfiltered-jscpd-report
+ path: ./jscpd-report.json
+
+ - name: Filter jscpd report for changed files
+ run: |
+ if [ ! -f ./jscpd-report.json ]; then
+ echo "jscpd-report.json not found"
+ exit 1
+ fi
+ echo "Filtering jscpd report for changed files..."
+ CHANGED_FILES=$(jq -R -s -c 'split("\n")[:-1]' diff_output.txt)
+ echo "Changed files: $CHANGED_FILES"
+ jq --argjson changed_files "$CHANGED_FILES" '
+ .duplicates | map(select(
+ (.firstFile?.name as $fname | $changed_files | any(. == $fname)) or
+ (.secondFile?.name as $sname | $changed_files | any(. == $sname))
+ ))
+ ' ./jscpd-report.json > filtered-jscpd-report.json
+ cat filtered-jscpd-report.json
+
+ - name: Check for duplicates
+ run: |
+ if [ $(wc -l < ./filtered-jscpd-report.json) -gt 1 ]; then
+ echo "filtered_report_exists=true" >> $GITHUB_ENV
+ else
+ echo "filtered_report_exists=false" >> $GITHUB_ENV
+ fi
+ - name: upload filtered report (if applicable)
+ if: env.filtered_report_exists == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: filtered-jscpd-report
+ path: ./filtered-jscpd-report.json
+
+ - name: Fail and log found duplicates.
+ if: env.filtered_report_exists == 'true'
+ run: |
+ cat ./filtered-jscpd-report.json
+ echo "Duplications found, failing the check."
+ exit 1
+
macos:
needs: lint-commits
name: test macOS
@@ -84,7 +171,7 @@ jobs:
env:
# Unset NODE_OPTIONS because of https://github.com/codecov/uploader/issues/475
NODE_OPTIONS: ''
- if: ${{ github.repository == 'aws/aws-toolkit-vscode' && github.ref == 'master' }}
+ if: ${{ github.repository == 'aws/aws-toolkit-vscode' && github.event_name == 'pull_request' && github.base_ref == 'master' }}
uses: codecov/codecov-action@v5
with:
flags: macos-toolkit-unittests
@@ -95,7 +182,7 @@ jobs:
env:
# Unset NODE_OPTIONS because of https://github.com/codecov/uploader/issues/475
NODE_OPTIONS: ''
- if: ${{ github.repository == 'aws/aws-toolkit-vscode' && github.ref == 'master' }}
+ if: ${{ github.repository == 'aws/aws-toolkit-vscode' && github.event_name == 'pull_request' && github.base_ref == 'master' }}
uses: codecov/codecov-action@v5
with:
flags: macos-amazonq-unittests
@@ -154,7 +241,7 @@ jobs:
env:
# Unset NODE_OPTIONS because of https://github.com/codecov/uploader/issues/475
NODE_OPTIONS: ''
- if: ${{ github.repository == 'aws/aws-toolkit-vscode' && github.ref == 'master' }}
+ if: ${{ github.repository == 'aws/aws-toolkit-vscode' && github.event_name == 'pull_request' && github.base_ref == 'master' }}
uses: codecov/codecov-action@v5
with:
flags: windows-unittests
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 964f6da9df9..c4dba3042e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -238,9 +238,12 @@ You can find documentation to create VSCode IDE settings for CodeCatalyst bluepr
Before sending a pull request:
+1. Treat all work as PUBLIC. Private `feature/x` branches will _not_ be squash-merged at release time. This has several benefits:
+ - Avoids mistakes (accidental exposure to public)!
+ - Avoids needing to erase (squash-merge) history.
1. Check that you are working against the latest source on the `master` branch.
-2. Check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
-3. Open an issue to discuss any significant work.
+1. Check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
+1. Open an issue to discuss any significant work.
To send a pull request:
diff --git a/buildspec/shared/common.sh b/buildspec/shared/common.sh
index 704f97799d8..a0dbc2e5837 100644
--- a/buildspec/shared/common.sh
+++ b/buildspec/shared/common.sh
@@ -6,13 +6,9 @@
# Ignore these patterns when deciding if the build should fail.
# - "waiting for browser": from `ssoAccessTokenProvider.test.ts`, unclear how to fix it.
-# - "Webview is disposed": only happens on vscode "minimum" (1.68.0)
# - "HTTPError: Response code …": caused by github rate-limiting.
# - "npm WARN deprecated querystring": transitive dep of aws sdk v2 (check `npm ls querystring`), so that's blocked until we migrate to v3.
-_ignore_pat='Timed-out waiting for browser login flow\|HTTPError: Response code 403\|HTTPError: Response code 404\|npm WARN deprecated querystring\|npm WARN deprecated'
-if [ "$VSCODE_TEST_VERSION" = 'minimum' ]; then
- _ignore_pat="$_ignore_pat"'\|Webview is disposed'
-fi
+_ignore_pat='HTTPError: Response code 403\|HTTPError: Response code 404\|npm WARN deprecated querystring\|npm WARN deprecated'
# Do not print (noisy) lines matching these patterns.
# - "ERROR:bus… Failed to connect to the bus": noise related to "xvfb". https://github.com/cypress-io/cypress/issues/19299
diff --git a/codecov.yml b/codecov.yml
index caf79e23a73..6da7d75aec2 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -5,6 +5,12 @@ codecov:
notify:
require_ci_to_pass: no
+ignore:
+ - 'node_modules/'
+ - 'plugins/'
+ - 'scripts/'
+ - 'src.gen/'
+
coverage:
precision: 2
round: down
@@ -12,38 +18,85 @@ coverage:
status:
project:
+ # Note: `default` measures the entire project.
+ # It does NOT define defaults "inherited" by other `project` items.
+ # https://docs.codecov.com/docs/commit-status
default:
- target: 70%
- threshold: 5%
- informational: true
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
+ target: 80
+ threshold: 5
+ only_pulls: true
codewhisperer:
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
paths:
- - packages/core/src/codewhisperer/*
+ - 'packages/core/src/codewhisperer/'
+ - 'packages/core/src/amazonq*/*'
+ - 'packages/amazonq/src/'
flags:
- 'codewhisperer'
+ target: 80
+ threshold: 5
+ only_pulls: true
amazonqFeatureDev:
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
paths:
- - packages/core/src/amazonqFeatureDev/*
+ - 'packages/core/src/amazonqFeatureDev/*'
flags:
- 'amazonqFeatureDev'
+ target: 80
+ threshold: 5
+ only_pulls: true
amazonqGumby:
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
paths:
- - packages/core/src/amazonqGumby/*
+ - 'packages/core/src/amazonqGumby/*'
+ target: 80
+ threshold: 5
+ only_pulls: true
codewhispererChat:
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
paths:
- - packages/core/src/codewhispererChat/*
+ - 'packages/core/src/codewhispererChat/*'
+ target: 80
+ threshold: 5
+ only_pulls: true
applicationcomposer:
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
paths:
- - packages/core/src/applicationcomposer/*
+ - 'packages/core/src/applicationcomposer/*'
+ target: 80
+ threshold: 5
+ only_pulls: true
stepFunctions:
- target: 50%
- threshold: 10%
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
paths:
- - packages/core/src/stepFunctions/*
+ - 'packages/core/src/stepFunctions/*'
+ target: 50
+ threshold: 10
+ only_pulls: true
threatComposer:
+ informational: true # Always pass. Ignore `target`, `threshold`, etc.
+ paths:
+ - 'packages/core/src/threatComposer/*'
+ target: 80
+ threshold: 5
+ only_pulls: true
+ tests:
+ # Most code in test/ should always be "covered"!
+ target: 95
paths:
- - packages/core/src/threatComposer/*
- patch: false
+ - '**/test/**'
+ patch:
+ default:
+ # Note: `default` measures the entire project.
+ # It does NOT define defaults "inherited" by other `project` items.
+ # https://docs.codecov.com/docs/commit-status
+ target: 90
+ threshold: 5
+ only_pulls: true
+ informational: false # Fail if PR changes are not covered.
+ # branches:
+ # - master
changes: false
comment: false
@@ -54,7 +107,9 @@ github_checks:
flags:
codewhisperer:
paths:
- - packages/core/src/codewhisperer/
+ - 'packages/core/src/codewhisperer/'
+ - 'packages/core/src/amazonq*/*'
+ - 'packages/amazonq/src/'
amazonqFeatureDev:
paths:
- - packages/core/src/amazonqFeatureDev/
+ - 'packages/core/src/amazonqFeatureDev/'
diff --git a/docs/arch_features.md b/docs/arch_features.md
index c76874f7b6e..e697874678d 100644
--- a/docs/arch_features.md
+++ b/docs/arch_features.md
@@ -43,6 +43,7 @@ For EC2 specifically, there are a few additional steps:
1. If connecting to EC2 instance via remote window, the toolkit generates temporary SSH keys (30 second lifetime), with the public key sent to the remote instance.
- Key type is ed25519 if supported, or RSA otherwise.
+ - This connection will overwrite the `.ssh/authorized_keys` file on the remote machine with each connection.
1. If insufficient permissions are detected on the attached IAM role, toolkit will prompt to add an inline policy with the necessary actions.
1. If SSM sessions remain open after closing the window/terminal, the toolkit will terminate them on-shutdown, or when starting another session to the same instance.
diff --git a/package-lock.json b/package-lock.json
index 92e5e525861..a5749953917 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"devDependencies": {
"@aws-toolkits/telemetry": "^1.0.287",
"@playwright/browser-chromium": "^1.43.1",
+ "@stylistic/eslint-plugin": "^2.11.0",
"@types/he": "^1.2.3",
"@types/vscode": "^1.68.0",
"@types/vscode-webview": "^1.57.1",
@@ -6083,9 +6084,9 @@
}
},
"node_modules/@aws/mynah-ui": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.18.1.tgz",
- "integrity": "sha512-531FL5509O081eWIk0P0reQhGTm/ZaXhRu6FLNqMvAKySPtJyyxee0ieeGAR8h5CVI75learQbXJEGJC6XibAA==",
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.21.0.tgz",
+ "integrity": "sha512-hEo9T1FeDoh79xA2nXaz3Le0SJES8YTuAOlleR8UKfbmzyfWzvvgO/yDIQ6dK39NO0cuQw+6XYGlIVDFpKfEkA==",
"hasInstallScript": true,
"dependencies": {
"escape-html": "^1.0.3",
@@ -8647,6 +8648,213 @@
"node": ">=16.0.0"
}
},
+ "node_modules/@stylistic/eslint-plugin": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz",
+ "integrity": "sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^8.13.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "estraverse": "^5.3.0",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=8.40.0"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz",
+ "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.16.0",
+ "@typescript-eslint/visitor-keys": "8.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz",
+ "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz",
+ "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "8.16.0",
+ "@typescript-eslint/visitor-keys": "8.16.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/utils": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz",
+ "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.16.0",
+ "@typescript-eslint/types": "8.16.0",
+ "@typescript-eslint/typescript-estree": "8.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz",
+ "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.16.0",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"license": "MIT",
@@ -9957,7 +10165,9 @@
}
},
"node_modules/acorn": {
- "version": "8.12.0",
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -10073,8 +10283,9 @@
"link": true
},
"node_modules/amazon-states-language-service": {
- "version": "1.11.0",
- "license": "MIT",
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/amazon-states-language-service/-/amazon-states-language-service-1.13.0.tgz",
+ "integrity": "sha512-XT/7LL9+TRCB8H3t0kM6h2uivHa7Pn2lZGpvHKujH1MM+lQ7aaprAKrnZkfSk9++VFNbFJBAnKW+5NN2xVcvlA==",
"dependencies": {
"js-yaml": "^4.1.0",
"vscode-json-languageservice": "5.3.5",
@@ -13045,9 +13256,9 @@
}
},
"node_modules/express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
@@ -13069,7 +13280,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -13084,6 +13295,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/array-flatten": {
@@ -13121,9 +13336,10 @@
"license": "MIT"
},
"node_modules/express/node_modules/path-to-regexp": {
- "version": "0.1.10",
- "dev": true,
- "license": "MIT"
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "dev": true
},
"node_modules/express/node_modules/statuses": {
"version": "2.0.1",
@@ -20917,7 +21133,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.39.0-SNAPSHOT",
+ "version": "1.40.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
@@ -20947,7 +21163,7 @@
"@aws-sdk/property-provider": "3.46.0",
"@aws-sdk/smithy-client": "^3.46.0",
"@aws-sdk/util-arn-parser": "^3.46.0",
- "@aws/mynah-ui": "^4.18.1",
+ "@aws/mynah-ui": "^4.21.0",
"@gerhobbelt/gitignore-parser": "^0.2.0-9",
"@iarna/toml": "^2.2.5",
"@smithy/middleware-retry": "^2.3.1",
@@ -20958,13 +21174,13 @@
"@vscode/debugprotocol": "^1.57.0",
"@zip.js/zip.js": "^2.7.41",
"adm-zip": "^0.5.10",
- "amazon-states-language-service": "^1.11.0",
+ "amazon-states-language-service": "^1.13.0",
"async-lock": "^1.4.0",
"aws-sdk": "^2.1384.0",
"aws-ssm-document-language-service": "^1.0.0",
"bytes": "^3.1.2",
"cross-fetch": "^4.0.0",
- "cross-spawn": "^7.0.3",
+ "cross-spawn": "^7.0.5",
"diff": "^5.1.0",
"fast-json-patch": "^3.1.1",
"glob": "^10.3.10",
@@ -21077,7 +21293,7 @@
},
"packages/toolkit": {
"name": "aws-toolkit-vscode",
- "version": "3.37.0-SNAPSHOT",
+ "version": "3.38.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/package.json b/package.json
index 872e525de2c..20d53676e49 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"devDependencies": {
"@aws-toolkits/telemetry": "^1.0.287",
"@playwright/browser-chromium": "^1.43.1",
+ "@stylistic/eslint-plugin": "^2.11.0",
"@types/he": "^1.2.3",
"@types/vscode": "^1.68.0",
"@types/vscode-webview": "^1.57.1",
diff --git a/packages/amazonq/.changes/1.39.0.json b/packages/amazonq/.changes/1.39.0.json
new file mode 100644
index 00000000000..750b30ba03a
--- /dev/null
+++ b/packages/amazonq/.changes/1.39.0.json
@@ -0,0 +1,34 @@
+{
+ "date": "2024-12-03",
+ "version": "1.39.0",
+ "entries": [
+ {
+ "type": "Feature",
+ "description": "Added a getting started page for exploring amazon q agents"
+ },
+ {
+ "type": "Feature",
+ "description": "`/test` in Q chat to generate unit tests for java and python"
+ },
+ {
+ "type": "Feature",
+ "description": "`/doc` in Q chat to generate and update documentation for your project"
+ },
+ {
+ "type": "Feature",
+ "description": "Amazon Q Code Scan is now Amazon Q Code Review"
+ },
+ {
+ "type": "Feature",
+ "description": "`/review` in Q chat to scan your code for vulnerabilities and quality issues, and generate fixes"
+ },
+ {
+ "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."
+ },
+ {
+ "type": "Feature",
+ "description": "Security Scan: Added ability to suppress or ignore security issues"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-0c35b577-da17-4132-a0b7-e0d630e73cb9.json b/packages/amazonq/.changes/next-release/Bug Fix-0c35b577-da17-4132-a0b7-e0d630e73cb9.json
new file mode 100644
index 00000000000..c7d24855d6b
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-0c35b577-da17-4132-a0b7-e0d630e73cb9.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Improved LLM code review for file review."
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-0e68107f-cd6c-488d-a457-d765b37a49c3.json b/packages/amazonq/.changes/next-release/Bug Fix-0e68107f-cd6c-488d-a457-d765b37a49c3.json
new file mode 100644
index 00000000000..18b94f4225c
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-0e68107f-cd6c-488d-a457-d765b37a49c3.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "@workspace is missing from the welcome to q chat tab"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-17772b9d-7f10-4e16-8b53-0561d6b08f4a.json b/packages/amazonq/.changes/next-release/Bug Fix-17772b9d-7f10-4e16-8b53-0561d6b08f4a.json
new file mode 100644
index 00000000000..02b38b02eb5
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-17772b9d-7f10-4e16-8b53-0561d6b08f4a.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Fix chat syntax highlighting when using several different themes"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-357cef5f-a2e7-43c4-b4ca-0e89bc8fccd7.json b/packages/amazonq/.changes/next-release/Bug Fix-357cef5f-a2e7-43c4-b4ca-0e89bc8fccd7.json
new file mode 100644
index 00000000000..85b6ac5c070
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-357cef5f-a2e7-43c4-b4ca-0e89bc8fccd7.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Amazon Q /doc: progress bar persists after cancelling README creation"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-56306583-37cf-4364-ba41-ac69d0ffdf44.json b/packages/amazonq/.changes/next-release/Bug Fix-56306583-37cf-4364-ba41-ac69d0ffdf44.json
new file mode 100644
index 00000000000..bbcdd4e40a8
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-56306583-37cf-4364-ba41-ac69d0ffdf44.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Code Review: Fixed a bug where some issues are missing from the code issues view for workspaces with custom names"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-570a8c06-4da6-4585-b589-be1fba2ce20f.json b/packages/amazonq/.changes/next-release/Bug Fix-570a8c06-4da6-4585-b589-be1fba2ce20f.json
new file mode 100644
index 00000000000..fe0706a5bd8
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-570a8c06-4da6-4585-b589-be1fba2ce20f.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Amazon Q /doc: Prompt user to choose a folder in chat"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-5f545950-eae0-4ee9-bb41-3c0ac9b7dcb1.json b/packages/amazonq/.changes/next-release/Bug Fix-5f545950-eae0-4ee9-bb41-3c0ac9b7dcb1.json
new file mode 100644
index 00000000000..cdddf910441
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-5f545950-eae0-4ee9-bb41-3c0ac9b7dcb1.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Amazon Q /dev not adding Dockerfiles in nested folders"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-75ff3f7a-3cfb-491c-9557-ce8c04f54ccc.json b/packages/amazonq/.changes/next-release/Bug Fix-75ff3f7a-3cfb-491c-9557-ce8c04f54ccc.json
new file mode 100644
index 00000000000..b79066d4070
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-75ff3f7a-3cfb-491c-9557-ce8c04f54ccc.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Improved Code Fix generation for code review issues"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-80f16b7a-440d-4e06-89ad-1454c58ff28f.json b/packages/amazonq/.changes/next-release/Bug Fix-80f16b7a-440d-4e06-89ad-1454c58ff28f.json
new file mode 100644
index 00000000000..0200cbbad42
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-80f16b7a-440d-4e06-89ad-1454c58ff28f.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Q feature dev: recognize .bms, .pli code files"
+}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-bc24902f-4602-491e-9ad8-959eea73d20b.json b/packages/amazonq/.changes/next-release/Bug Fix-bc24902f-4602-491e-9ad8-959eea73d20b.json
new file mode 100644
index 00000000000..e540866201e
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-bc24902f-4602-491e-9ad8-959eea73d20b.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Fix the quick start buttons on the explore page to show amazon q colours on hover"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-2e0fcda2-695f-4646-8ccd-20e9e56647a6.json b/packages/amazonq/.changes/next-release/Feature-2e0fcda2-695f-4646-8ccd-20e9e56647a6.json
new file mode 100644
index 00000000000..7aaf8b7077f
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-2e0fcda2-695f-4646-8ccd-20e9e56647a6.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Amazon Q Code Transformation: show job ID in Transformation Hub"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-3e91ab38-b708-4fcc-888e-7a532c210a69.json b/packages/amazonq/.changes/next-release/Feature-3e91ab38-b708-4fcc-888e-7a532c210a69.json
new file mode 100644
index 00000000000..5e592eca298
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-3e91ab38-b708-4fcc-888e-7a532c210a69.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "UI improvements to Amazon Q Chat: New splash loader animation, initial streaming card animation, improved button colours"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-b92f1e8b-78a0-46a6-ba9b-2300c884fcbc.json b/packages/amazonq/.changes/next-release/Feature-b92f1e8b-78a0-46a6-ba9b-2300c884fcbc.json
new file mode 100644
index 00000000000..20664e7f542
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-b92f1e8b-78a0-46a6-ba9b-2300c884fcbc.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Add acknowledgement button for amazon q chat disclaimer"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-c0fdc8fe-e0e5-422c-a76a-1d7461460e81.json b/packages/amazonq/.changes/next-release/Feature-c0fdc8fe-e0e5-422c-a76a-1d7461460e81.json
new file mode 100644
index 00000000000..33bc4142322
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-c0fdc8fe-e0e5-422c-a76a-1d7461460e81.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Navigate through prompt history by using the up/down arrows"
+}
diff --git a/packages/amazonq/.changes/next-release/Feature-fd23658e-ecc3-4d17-8944-240cbe24a7cc.json b/packages/amazonq/.changes/next-release/Feature-fd23658e-ecc3-4d17-8944-240cbe24a7cc.json
new file mode 100644
index 00000000000..c7df441d68b
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-fd23658e-ecc3-4d17-8944-240cbe24a7cc.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Amazon Q: Simplify log channel"
+}
diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md
index 8aeebb487f6..4d40556f351 100644
--- a/packages/amazonq/CHANGELOG.md
+++ b/packages/amazonq/CHANGELOG.md
@@ -1,3 +1,13 @@
+## 1.39.0 2024-12-03
+
+- **Feature** Added a getting started page for exploring amazon q agents
+- **Feature** `/test` in Q chat to generate unit tests for java and python
+- **Feature** `/doc` in Q chat to generate and update documentation for your project
+- **Feature** Amazon Q Code Scan is now Amazon Q Code Review
+- **Feature** `/review` in Q chat to scan your code for vulnerabilities and quality issues, and generate fixes
+- **Feature** 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.
+- **Feature** Security Scan: Added ability to suppress or ignore security issues
+
## 1.38.0 2024-11-27
- **Feature** Amazon Q /dev: support `Dockerfile` files
diff --git a/packages/amazonq/README.md b/packages/amazonq/README.md
index beee7557b6a..46091a98d10 100644
--- a/packages/amazonq/README.md
+++ b/packages/amazonq/README.md
@@ -3,62 +3,60 @@
[](https://www.youtube.com/@amazonwebservices)

-# Getting Started
+# Agent capabilities
-**Free Tier** - create or log in with an AWS Builder ID (a personal profile from AWS).
+### Implement new features
-**Pro Tier** - if your organization is on the Amazon Q Developer Pro tier, log in with single sign-on.
+`/dev` to task Amazon Q with generating new code across your entire project and implement features.
-
+### Generate documentation
-# Features
+`/doc` to task Amazon Q with writing API, technical design, and onboarding documentation.
-## Inline code suggestions
+### Automate code reviews
-Code faster with inline code suggestions as you type.
+`/review` to ask Amazon Q to perform code reviews, flagging suspicious code patterns and assessing deployment risk.
-
+### Generate unit tests
-[_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)
-
-## Chat
+`/test` to ask Amazon Q to generate unit tests and add them to your project, helping you improve code quality, fast.
-Generate code, explain code, and get answers to questions about software development.
+### 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
-[_10 languages supported including Python, TypeScript, C#, and more_](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/security-scans.html)
+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".
-## Agent for software development
+### Chat
-Amazon Q can implement new functionality across multiple files in your workspace.
+Generate code, explain code, and get answers about software development.
-Type “/” in chat to open the quick actions menu and choose the “/dev” action.
+### Inline suggestions
-
+Receive real-time code suggestions ranging from snippets to full functions based on your comments and existing code.
-_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.
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 4fbace6f050..d41b80b8d3f 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -1,8 +1,8 @@
{
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
- "description": "Amazon Q is your generative AI-powered assistant across the software development lifecycle.",
- "version": "1.39.0-SNAPSHOT",
+ "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI",
+ "version": "1.40.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..455a4ebf4af
--- /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/src/extension.ts b/packages/amazonq/src/extension.ts
index add823b5a9e..a4b53dbf66d 100644
--- a/packages/amazonq/src/extension.ts
+++ b/packages/amazonq/src/extension.ts
@@ -53,7 +53,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
errors.init(fs.getUsername(), env.isAutomation())
await initializeComputeRegion()
- globals.contextPrefix = 'amazonq.' //todo: disconnect from above line
+ globals.contextPrefix = 'amazonq.' // todo: disconnect from above line
// Avoid activation if older toolkit is installed
// Amazon Q is only compatible with AWS Toolkit >= 3.0.0
@@ -97,10 +97,8 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
globals.manifestPaths.endpoints = context.asAbsolutePath(join('resources', 'endpoints.json'))
globals.regionProvider = RegionProvider.fromEndpointsProvider(makeEndpointsProvider())
- const qOutputChannel = vscode.window.createOutputChannel('Amazon Q', { log: true })
const qLogChannel = vscode.window.createOutputChannel('Amazon Q Logs', { log: true })
- await activateLogger(context, amazonQContextPrefix, qOutputChannel, qLogChannel)
- globals.outputChannel = qOutputChannel
+ await activateLogger(context, amazonQContextPrefix, qLogChannel)
globals.logOutputChannel = qLogChannel
globals.loginManager = new LoginManager(globals.awsContext, new CredentialsStore())
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..c9953d6dd41 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
@@ -59,6 +59,14 @@ export class Messenger {
this.mynahUIProps.onFollowUpClicked(this.tabID, lastChatItem?.messageId ?? '', option[0])
}
+ clickCustomFormButton(action: { id: string; text?: string; formItemValues?: Record }) {
+ if (!this.mynahUIProps.onCustomFormAction) {
+ assert.fail('onCustomFormAction must be defined to use it in the tests')
+ }
+
+ this.mynahUIProps.onCustomFormAction(this.tabID, action)
+ }
+
clickFileActionButton(filePath: string, actionName: string) {
if (!this.mynahUIProps.onFileActionClick) {
assert.fail('onFileActionClick must be defined to use it in the tests')
@@ -173,7 +181,9 @@ export class Messenger {
// Do another check just in case the waitUntil time'd out
if (!event()) {
- assert.fail(`Event has not finished loading in: ${this.waitTimeoutInMs} ms`)
+ assert.fail(
+ `Event has not finished loading in: ${waitOverrides ? waitOverrides.waitTimeoutInMs : this.waitTimeoutInMs} ms`
+ )
}
}
diff --git a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts
new file mode 100644
index 00000000000..8d7543211ba
--- /dev/null
+++ b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts
@@ -0,0 +1,347 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert'
+import { qTestingFramework } from './framework/framework'
+import sinon from 'sinon'
+import { Messenger } from './framework/messenger'
+import { JDKVersion, TransformationType, transformByQState } from 'aws-core-vscode/codewhisperer'
+import { GumbyController, startTransformByQ, TabsStorage } from 'aws-core-vscode/amazonqGumby'
+import { using, registerAuthHook, TestFolder } from 'aws-core-vscode/test'
+import { loginToIdC } from './utils/setup'
+import { fs } from 'aws-core-vscode/shared'
+import path from 'path'
+
+describe('Amazon Q Code Transformation', function () {
+ let framework: qTestingFramework
+ let tab: Messenger
+
+ before(async function () {
+ await using(registerAuthHook('amazonq-test-account'), async () => {
+ await loginToIdC()
+ })
+ })
+
+ beforeEach(() => {
+ registerAuthHook('amazonq-test-account')
+ framework = new qTestingFramework('gumby', true, [])
+ tab = framework.createTab()
+ })
+
+ afterEach(() => {
+ framework.removeTab(tab.tabID)
+ framework.dispose()
+ sinon.restore()
+ })
+
+ describe('Quick action availability', () => {
+ it('Can invoke /transform when QCT is enabled', async () => {
+ const command = tab.findCommand('/transform')
+ if (!command) {
+ assert.fail('Could not find command')
+ }
+
+ if (command.length > 1) {
+ assert.fail('Found too many commands with the name /transform')
+ }
+ })
+
+ it('CANNOT invoke /transform when QCT is NOT enabled', () => {
+ framework.dispose()
+ framework = new qTestingFramework('gumby', false, [])
+ const tab = framework.createTab()
+ const command = tab.findCommand('/transform')
+ if (command.length > 0) {
+ assert.fail('Found command when it should not have been found')
+ }
+ })
+ })
+
+ describe('Starting a transformation from chat', () => {
+ it('Can click through all user input forms for a Java upgrade', async () => {
+ sinon.stub(startTransformByQ, 'getValidSQLConversionCandidateProjects').resolves([])
+ sinon.stub(GumbyController.prototype, 'validateLanguageUpgradeProjects' as keyof GumbyController).resolves([
+ {
+ name: 'qct-sample-java-8-app-main',
+ path: '/Users/alias/Desktop/qct-sample-java-8-app-main',
+ JDKVersion: JDKVersion.JDK8,
+ },
+ ])
+
+ tab.addChatMessage({ command: '/transform' })
+
+ // wait for /transform to respond with some intro messages and the first user input form
+ await tab.waitForEvent(() => tab.getChatItems().length > 3, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const projectForm = tab.getChatItems().pop()
+ assert.strictEqual(projectForm?.formItems?.[0]?.id ?? undefined, 'GumbyTransformLanguageUpgradeProjectForm')
+
+ const projectFormItemValues = {
+ GumbyTransformLanguageUpgradeProjectForm: '/Users/alias/Desktop/qct-sample-java-8-app-main',
+ GumbyTransformJdkFromForm: '8',
+ GumbyTransformJdkToForm: '17',
+ }
+ const projectFormValues: Record = { ...projectFormItemValues }
+ // TODO: instead of stubbing, can we create a tab in qTestingFramework with tabType passed in?
+ // Mynah-UI updates tab type like this: this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'gumby')
+ sinon
+ .stub(TabsStorage.prototype, 'getTab')
+ .returns({ id: tab.tabID, status: 'free', type: 'gumby', isSelected: true })
+ tab.clickCustomFormButton({
+ id: 'gumbyLanguageUpgradeTransformFormConfirm',
+ text: 'Confirm',
+ formItemValues: projectFormValues,
+ })
+
+ // 3 additional chat messages (including message with 2nd form) get sent after 1st form submitted; wait for all of them
+ await tab.waitForEvent(() => tab.getChatItems().length > 6, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const skipTestsForm = tab.getChatItems().pop()
+ assert.strictEqual(skipTestsForm?.formItems?.[0]?.id ?? undefined, 'GumbyTransformSkipTestsForm')
+
+ const skipTestsFormItemValues = {
+ GumbyTransformSkipTestsForm: 'Run unit tests',
+ }
+ const skipTestsFormValues: Record = { ...skipTestsFormItemValues }
+ tab.clickCustomFormButton({
+ id: 'gumbyTransformSkipTestsFormConfirm',
+ text: 'Confirm',
+ formItemValues: skipTestsFormValues,
+ })
+
+ // 3 additional chat messages (including message with 3rd form) get sent after 2nd form submitted; wait for all of them
+ await tab.waitForEvent(() => tab.getChatItems().length > 9, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const multipleDiffsForm = tab.getChatItems().pop()
+ assert.strictEqual(
+ multipleDiffsForm?.formItems?.[0]?.id ?? undefined,
+ 'GumbyTransformOneOrMultipleDiffsForm'
+ )
+
+ const oneOrMultipleDiffsFormItemValues = {
+ GumbyTransformOneOrMultipleDiffsForm: 'One diff',
+ }
+ const oneOrMultipleDiffsFormValues: Record = { ...oneOrMultipleDiffsFormItemValues }
+ tab.clickCustomFormButton({
+ id: 'gumbyTransformOneOrMultipleDiffsFormConfirm',
+ text: 'Confirm',
+ formItemValues: oneOrMultipleDiffsFormValues,
+ })
+
+ // 2 additional chat messages (including message with 4th form) get sent after 3rd form submitted; wait for both of them
+ await tab.waitForEvent(() => tab.getChatItems().length > 11, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const jdkPathPrompt = tab.getChatItems().pop()
+ assert.strictEqual(jdkPathPrompt?.body?.includes('Enter the path to JDK'), true)
+
+ // 2 additional chat messages get sent after 4th form submitted; wait for both of them
+ tab.addChatMessage({ prompt: '/dummy/path/to/jdk8' })
+ await tab.waitForEvent(() => tab.getChatItems().length > 13, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const jdkPathResponse = tab.getChatItems().pop()
+ // this 'Sorry' message is OK - just making sure that the UI components are working correctly
+ assert.strictEqual(jdkPathResponse?.body?.includes("Sorry, I couldn't locate your Java installation"), true)
+ })
+
+ it('Can provide metadata file for a SQL conversion', async () => {
+ sinon.stub(startTransformByQ, 'getValidSQLConversionCandidateProjects').resolves([
+ {
+ name: 'OracleExample',
+ path: '/Users/alias/Desktop/OracleExample',
+ JDKVersion: JDKVersion.JDK17,
+ },
+ ])
+ sinon.stub(startTransformByQ, 'getValidLanguageUpgradeCandidateProjects').resolves([])
+ sinon.stub(GumbyController.prototype, 'validateSQLConversionProjects' as keyof GumbyController).resolves([
+ {
+ name: 'OracleExample',
+ path: '/Users/alias/Desktop/OracleExample',
+ JDKVersion: JDKVersion.JDK17,
+ },
+ ])
+
+ tab.addChatMessage({ command: '/transform' })
+
+ // wait for /transform to respond with some intro messages and the first user input message
+ await tab.waitForEvent(() => tab.getChatItems().length > 3, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const selectMetadataMessage = tab.getChatItems().pop()
+ assert.strictEqual(
+ selectMetadataMessage?.body?.includes('I can convert the embedded SQL') ?? undefined,
+ true
+ )
+
+ // verify that we processed the metadata file
+ const processMetadataFileStub = sinon.stub(
+ GumbyController.prototype,
+ 'processMetadataFile' as keyof GumbyController
+ )
+ tab.clickCustomFormButton({
+ id: 'gumbySQLConversionMetadataTransformFormConfirm',
+ text: 'Select metadata file',
+ })
+ sinon.assert.calledOnce(processMetadataFileStub)
+ })
+
+ it('Can choose "language upgrade" when eligible for a Java upgrade AND SQL conversion', async () => {
+ sinon.stub(startTransformByQ, 'getValidSQLConversionCandidateProjects').resolves([
+ {
+ name: 'OracleExample',
+ path: '/Users/alias/Desktop/OracleExample',
+ JDKVersion: JDKVersion.JDK17,
+ },
+ ])
+ sinon.stub(startTransformByQ, 'getValidLanguageUpgradeCandidateProjects').resolves([
+ {
+ name: 'qct-sample-java-8-app-main',
+ path: '/Users/alias/Desktop/qct-sample-java-8-app-main',
+ JDKVersion: JDKVersion.JDK8,
+ },
+ ])
+
+ tab.addChatMessage({ command: '/transform' })
+
+ // wait for /transform to respond with some intro messages and a prompt asking user what they want to do
+ await tab.waitForEvent(() => tab.getChatItems().length > 2, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const prompt = tab.getChatItems().pop()
+ assert.strictEqual(
+ prompt?.body?.includes('You can enter "language upgrade" or "sql conversion"') ?? undefined,
+ true
+ )
+
+ // 3 additional chat messages get sent after user enters a choice; wait for all of them
+ tab.addChatMessage({ prompt: 'language upgrade' })
+ await tab.waitForEvent(() => tab.getChatItems().length > 5, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const projectForm = tab.getChatItems().pop()
+ assert.strictEqual(projectForm?.formItems?.[0]?.id ?? undefined, 'GumbyTransformLanguageUpgradeProjectForm')
+ })
+
+ it('Can choose "sql conversion" when eligible for a Java upgrade AND SQL conversion', async () => {
+ sinon.stub(startTransformByQ, 'getValidSQLConversionCandidateProjects').resolves([
+ {
+ name: 'OracleExample',
+ path: '/Users/alias/Desktop/OracleExample',
+ JDKVersion: JDKVersion.JDK17,
+ },
+ ])
+ sinon.stub(startTransformByQ, 'getValidLanguageUpgradeCandidateProjects').resolves([
+ {
+ name: 'qct-sample-java-8-app-main',
+ path: '/Users/alias/Desktop/qct-sample-java-8-app-main',
+ JDKVersion: JDKVersion.JDK8,
+ },
+ ])
+
+ tab.addChatMessage({ command: '/transform' })
+
+ // wait for /transform to respond with some intro messages and a prompt asking user what they want to do
+ await tab.waitForEvent(() => tab.getChatItems().length > 2, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const prompt = tab.getChatItems().pop()
+ assert.strictEqual(
+ prompt?.body?.includes('You can enter "language upgrade" or "sql conversion"') ?? undefined,
+ true
+ )
+
+ // 3 additional chat messages get sent after user enters a choice; wait for all of them
+ tab.addChatMessage({ prompt: 'sql conversion' })
+ await tab.waitForEvent(() => tab.getChatItems().length > 5, {
+ waitTimeoutInMs: 5000,
+ waitIntervalInMs: 1000,
+ })
+ const selectMetadataMessage = tab.getChatItems().pop()
+ assert.strictEqual(
+ selectMetadataMessage?.body?.includes('I can convert the embedded SQL') ?? undefined,
+ true
+ )
+ })
+ })
+
+ // TODO: enable when we no longer get throttled on CreateUploadUrl and other APIs
+ describe.skip('Running a Java upgrade from start to finish', async function () {
+ let tempDir = ''
+ let tempFileName = ''
+ let tempFilePath = ''
+
+ const javaFileContents = `public class MyApp {
+ public static void main(String[] args) {
+ Integer temp = new Integer("1234");
+ }
+ }`
+
+ const pomXmlContents = `
+
+ 4.0.0
+
+ com.example
+ basic-java-app
+ 1.0-SNAPSHOT
+
+
+ 1.8
+ 1.8
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 1.8
+ 1.8
+
+
+
+
+ `
+
+ before(async function () {
+ tempDir = path.join((await TestFolder.create()).path, 'qct-java-upgrade-test')
+ tempFileName = 'MyApp.java'
+ tempFilePath = path.join(tempDir, tempFileName)
+ await fs.writeFile(tempFilePath, javaFileContents)
+ tempFileName = 'pom.xml'
+ tempFilePath = path.join(tempDir, tempFileName)
+ await fs.writeFile(tempFilePath, pomXmlContents)
+ })
+
+ after(async function () {
+ await fs.delete(tempDir, { recursive: true })
+ })
+
+ it('WHEN transforming a Java 8 project E2E THEN job is successful', async function () {
+ transformByQState.setTransformationType(TransformationType.LANGUAGE_UPGRADE)
+ await startTransformByQ.setMaven()
+ await startTransformByQ.processLanguageUpgradeTransformFormInput(tempDir, JDKVersion.JDK8, JDKVersion.JDK17)
+ await startTransformByQ.startTransformByQ()
+ assert.strictEqual(transformByQState.getPolledJobStatus(), 'COMPLETED')
+ })
+ })
+})
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 0a5a9e3e784..88173eeea15 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"
+ }
}
}
},
@@ -459,9 +496,9 @@
"@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-cloudwatch-logs": "^3.666.0",
+ "@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",
@@ -470,7 +507,7 @@
"@aws-sdk/property-provider": "3.46.0",
"@aws-sdk/smithy-client": "^3.46.0",
"@aws-sdk/util-arn-parser": "^3.46.0",
- "@aws/mynah-ui": "^4.18.1",
+ "@aws/mynah-ui": "^4.21.0",
"@gerhobbelt/gitignore-parser": "^0.2.0-9",
"@iarna/toml": "^2.2.5",
"@smithy/middleware-retry": "^2.3.1",
@@ -481,13 +518,13 @@
"@vscode/debugprotocol": "^1.57.0",
"@zip.js/zip.js": "^2.7.41",
"adm-zip": "^0.5.10",
- "amazon-states-language-service": "^1.11.0",
+ "amazon-states-language-service": "^1.13.0",
"async-lock": "^1.4.0",
"aws-sdk": "^2.1384.0",
"aws-ssm-document-language-service": "^1.0.0",
"bytes": "^3.1.2",
"cross-fetch": "^4.0.0",
- "cross-spawn": "^7.0.3",
+ "cross-spawn": "^7.0.5",
"diff": "^5.1.0",
"fast-json-patch": "^3.1.1",
"glob": "^10.3.10",
diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json
index eddb03ebd59..2f92a23b5ac 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...",
@@ -248,6 +258,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...",
@@ -258,7 +269,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.",
@@ -278,28 +292,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.",
@@ -326,7 +352,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?",
@@ -337,6 +363,30 @@
"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.answer.chooseFolder": "Choose a folder to continue.",
+ "AWS.amazonq.doc.error.noFolderSelected": "It looks like you didn't choose a folder. Choose a folder to continue.",
+ "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/ec2_connect b/packages/core/resources/ec2_connect
index 518a5e86f58..06ae8d39686 100755
--- a/packages/core/resources/ec2_connect
+++ b/packages/core/resources/ec2_connect
@@ -2,7 +2,7 @@
# Usage:
# When connecting to a dev environment
-# AWS_REGION=… AWS_SSM_CLI=… STREAM_URL=… TOKEN=… LOG_FILE_LOCATION==… ./ec2_connect
+# AWS_REGION=… AWS_SSM_CLI=… STREAM_URL=… TOKEN=… LOG_FILE_LOCATION==… DEBUG_LOG=… ./ec2_connect
set -e
set -u
@@ -44,13 +44,21 @@ _ec2() {
_main() {
_log "=============================================================================="
-
- _require AWS_SSM_CLI "${AWS_SSM_CLI:-}"
+ _require DEBUG_LOG "${DEBUG_LOG:-}"
_require AWS_REGION "${AWS_REGION:-}"
- _require STREAM_URL "${STREAM_URL:-}"
- _require TOKEN "${TOKEN:-}"
+
_require SESSION_ID "${SESSION_ID:-}"
- _require LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}"
+ _require_nolog STREAM_URL "${STREAM_URL:-}"
+ _require_nolog TOKEN "${TOKEN:-}"
+
+ # Only log file paths when debug level is enabled.
+ if [ "${DEBUG_LOG:-}" -eq 1 ]; then
+ _require AWS_SSM_CLI "${AWS_SSM_CLI:-}"
+ _require LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}"
+ else
+ _require_nolog AWS_SSM_CLI "${AWS_SSM_CLI:-}"
+ _require_nolog LOG_FILE_LOCATION "${LOG_FILE_LOCATION:-}"
+ fi
_ec2 "$AWS_SSM_CLI" "$AWS_REGION" "$STREAM_URL" "$TOKEN" "$SESSION_ID"
}
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/resources/js/graphStateMachine.js b/packages/core/resources/js/graphStateMachine.js
index d2ec7ca11ab..9ccff2145fa 100644
--- a/packages/core/resources/js/graphStateMachine.js
+++ b/packages/core/resources/js/graphStateMachine.js
@@ -108,7 +108,7 @@ zoomoutBtn.addEventListener('click', () => {
// Message passing from extension to webview.
// Capture state machine definition
-window.addEventListener('message', event => {
+window.addEventListener('message', (event) => {
// event.data is object passed in from postMessage from vscode
const message = event.data
switch (message.command) {
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..f5724a13872
--- /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..359d8d24256 100644
--- a/packages/core/src/amazonq/lsp/lspClient.ts
+++ b/packages/core/src/amazonq/lsp/lspClient.ts
@@ -26,11 +26,13 @@ import {
QueryVectorIndexRequestType,
UpdateIndexV2RequestPayload,
UpdateIndexV2RequestType,
+ QueryRepomapIndexRequestType,
+ GetRepomapIndexJSONRequestType,
Usage,
} from './types'
import { Writable } from 'stream'
import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings'
-import { fs, getLogger } from '../../shared'
+import { fs, getLogger, globals } from '../../shared'
const localize = nls.loadMessageBundle()
@@ -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.
@@ -201,6 +228,9 @@ export async function activate(extensionContext: ExtensionContext) {
// this is used by LSP to determine index cache path, move to this folder so that when extension updates index is not deleted.
extensionPath: path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'cache'),
},
+ // Log to the Amazon Q Logs so everything is in a single channel
+ // TODO: Add prefix to the language server logs so it is easier to search
+ outputChannel: globals.logOutputChannel,
}
// Create the language client and start the client.
diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts
index 29fd0ab68d3..1b948625e76 100644
--- a/packages/core/src/amazonq/lsp/lspController.ts
+++ b/packages/core/src/amazonq/lsp/lspController.ts
@@ -360,7 +360,7 @@ export class LspController {
})
}
} catch (error) {
- //TODO: use telemetry.run()
+ // TODO: use telemetry.run()
getLogger().error(`LspController: Failed to build index of project`)
telemetry.amazonq_indexWorkspace.emit({
duration: performance.now() - start,
@@ -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..3af943cb97d 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..6b935e83642 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)}