diff --git a/.changes/3.42.json b/.changes/3.42.json new file mode 100644 index 0000000000..dacf014390 --- /dev/null +++ b/.changes/3.42.json @@ -0,0 +1,23 @@ +{ + "date" : "2024-11-27", + "version" : "3.42", + "entries" : [ { + "type" : "feature", + "description" : "Amazon Q /dev: support `Dockerfile` files" + }, { + "type" : "feature", + "description" : "Feature(Amazon Q Code Transformation): allow users to view results in 5 smaller diffs" + }, { + "type" : "feature", + "description" : "Introduce @workspace command to enhance chat context fetching for Chat" + }, { + "type" : "bugfix", + "description" : "Correct search text for Amazon Q inline suggestion keybindings" + }, { + "type" : "bugfix", + "description" : "Fix(Amazon Q Code Transformation): always show user latest/correct transformation results" + }, { + "type" : "bugfix", + "description" : "Amazon Q /dev: Fix error when accepting changes if leading slash is present." + } ] +} \ No newline at end of file diff --git a/.changes/3.43.json b/.changes/3.43.json new file mode 100644 index 0000000000..3ff7f67e04 --- /dev/null +++ b/.changes/3.43.json @@ -0,0 +1,17 @@ +{ + "date" : "2024-12-03", + "version" : "3.43", + "entries" : [ { + "type" : "feature", + "description" : "`/review` in Q chat to scan your code for vulnerabilities and quality issues, and generate fixes" + }, { + "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" : "Added system notifications to inform users about critical plugin updates and potential issues with available workarounds" + } ] +} \ No newline at end of file diff --git a/.changes/3.44.json b/.changes/3.44.json new file mode 100644 index 0000000000..22545117d6 --- /dev/null +++ b/.changes/3.44.json @@ -0,0 +1,20 @@ +{ + "date" : "2024-12-04", + "version" : "3.44", + "entries" : [ { + "type" : "feature", + "description" : "Amazon Q: UI improvements to chat: New splash loader animation, initial streaming card animation, improved button colours" + }, { + "type" : "feature", + "description" : "Amazon Q: Navigate through prompt history by using the up/down arrows" + }, { + "type" : "bugfix", + "description" : "Fix issue where Amazon Q Code Transform is unable to start" + }, { + "type" : "bugfix", + "description" : "Fix DynamoDB viewer throwing 'ActionGroup should be registered using tag' on IDE start (#5012) (#5120)" + }, { + "type" : "bugfix", + "description" : "Amazon Q: Fix chat syntax highlighting when using several different themes" + } ] +} \ No newline at end of file diff --git a/.changes/3.45.json b/.changes/3.45.json new file mode 100644 index 0000000000..001c781e7b --- /dev/null +++ b/.changes/3.45.json @@ -0,0 +1,23 @@ +{ + "date" : "2024-12-10", + "version" : "3.45", + "entries" : [ { + "type" : "feature", + "description" : "Add acknowledgement button for Amazon Q Chat disclaimer" + }, { + "type" : "bugfix", + "description" : " Chosing cancel on sign out confirmation now cancels the sign out and does not delete profiles from ~/.aws/config (#5167)" + }, { + "type" : "bugfix", + "description" : "Fix `@workspace` missing from the Amazon Q Chat welcome tab" + }, { + "type" : "bugfix", + "description" : "Fix for /review LLM based code issues for file review on windows" + }, { + "type" : "bugfix", + "description" : "Fix for File Review payload and Regex error for payload generation" + }, { + "type" : "bugfix", + "description" : "Amazon Q Code Transformation: show build logs when server-side build fails" + } ] +} \ No newline at end of file diff --git a/.changes/3.46.json b/.changes/3.46.json new file mode 100644 index 0000000000..4d998c9ac6 --- /dev/null +++ b/.changes/3.46.json @@ -0,0 +1,26 @@ +{ + "date" : "2024-12-17", + "version" : "3.46", + "entries" : [ { + "type" : "feature", + "description" : "/review: Code fix automatically scrolls into view after generation." + }, { + "type" : "feature", + "description" : "Chat: improve font size and line-height in footer (below prompt input field)" + }, { + "type" : "feature", + "description" : "Adds capability to send new context commands to AB groups" + }, { + "type" : "bugfix", + "description" : "Chat: When writing a prompt without sending it, navigating via up/down arrows sometimes deletes the unsent prompt." + }, { + "type" : "bugfix", + "description" : "Fix chat not retaining history when interaction is through onboarding tab type (#5189)" + }, { + "type" : "bugfix", + "description" : "Chat: When navigating to previous prompts, code attachments are sometimes displayed incorrectly" + }, { + "type" : "bugfix", + "description" : "Reduce frequency of system information query" + } ] +} \ No newline at end of file diff --git a/.changes/3.47.json b/.changes/3.47.json new file mode 100644 index 0000000000..c7e3dbe830 --- /dev/null +++ b/.changes/3.47.json @@ -0,0 +1,35 @@ +{ + "date" : "2025-01-09", + "version" : "3.47", + "entries" : [ { + "type" : "bugfix", + "description" : "Fix issue where users are unable to login to Amazon Q if they have previously authenticated (#5214)" + }, { + "type" : "bugfix", + "description" : "Fix incorrect text shown while updating documentation in /doc" + }, { + "type" : "bugfix", + "description" : "Amazon Q Code Transformation: retry initial project upload on failure" + }, { + "type" : "bugfix", + "description" : "/transform: use correct doc link in SQL conversion help message" + }, { + "type" : "bugfix", + "description" : "Amazon Q /dev: Fix issue when files are deleted while preparing context" + }, { + "type" : "bugfix", + "description" : "Amazon Q /test: Test generation fails for files outside the project" + }, { + "type" : "bugfix", + "description" : "Amazon Q Code Transformation: allow PostgreSQL as target DB for SQL conversions" + }, { + "type" : "bugfix", + "description" : "Fix incorrect accept and reject buttons shows up while hovering over the generated file" + }, { + "type" : "bugfix", + "description" : "Prevent customization override if user has manually selected a customization" + }, { + "type" : "bugfix", + "description" : "Align UX text of document generation flow with vs code version" + } ] +} \ No newline at end of file diff --git a/.changes/3.48.json b/.changes/3.48.json new file mode 100644 index 0000000000..c3ed0456a7 --- /dev/null +++ b/.changes/3.48.json @@ -0,0 +1,32 @@ +{ + "date" : "2025-01-16", + "version" : "3.48", + "entries" : [ { + "type" : "feature", + "description" : "Enhance Q inline completion context fetching for better suggestion quality" + }, { + "type" : "feature", + "description" : "/doc: Add error message if updated README is too large" + }, { + "type" : "bugfix", + "description" : "/transform: always include button to start a new transformation at the end of a job" + }, { + "type" : "bugfix", + "description" : "Amazon Q can update mvn and gradle build files" + }, { + "type" : "bugfix", + "description" : "Fix doc generation for modules that are a part of the project" + }, { + "type" : "bugfix", + "description" : "Amazon Q /dev: Remove hard-coded limits and instead rely server-side data to communicate number of code generations remaining" + }, { + "type" : "bugfix", + "description" : "/transform: automatically open pre-build error logs when available" + }, { + "type" : "bugfix", + "description" : "/doc: Fix code generation error when cancelling a documentation task" + }, { + "type" : "bugfix", + "description" : "Amazon Q - update messaging for /doc agent" + } ] +} \ No newline at end of file diff --git a/.changes/next-release/.changes/next-release/bugfix-21701bb3-5189-474e-868b-9ec46ecde6ee.json b/.changes/next-release/.changes/next-release/bugfix-21701bb3-5189-474e-868b-9ec46ecde6ee.json deleted file mode 100644 index 8d2e0694ab..0000000000 --- a/.changes/next-release/.changes/next-release/bugfix-21701bb3-5189-474e-868b-9ec46ecde6ee.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type" : "bugfix", - "description" : "Feature Development: Fix file rejections for files outside of src/" -} diff --git a/.changes/next-release/feature-07ba60de-7043-4965-8fb5-7038c8ea6c5e.json b/.changes/next-release/feature-07ba60de-7043-4965-8fb5-7038c8ea6c5e.json deleted file mode 100644 index 93b0197b9f..0000000000 --- a/.changes/next-release/feature-07ba60de-7043-4965-8fb5-7038c8ea6c5e.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type" : "feature", - "description" : "Feature(Amazon Q Code Transformation): allow users to view results in 5 smaller diffs" -} \ No newline at end of file diff --git a/.changes/next-release/feature-b05e3cd8-abd5-47f9-bd5c-02c7b7eb378e.json b/.changes/next-release/feature-b05e3cd8-abd5-47f9-bd5c-02c7b7eb378e.json deleted file mode 100644 index f28cb4e973..0000000000 --- a/.changes/next-release/feature-b05e3cd8-abd5-47f9-bd5c-02c7b7eb378e.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type" : "feature", - "description" : "Introduce @workspace command to enhance chat context fetching for Chat" -} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 7b39413db6..55acdaad72 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,7 +42,7 @@ ij_kotlin_allow_trailing_comma = true ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_name_count_to_use_star_import = 2147483647 ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 -ij_kotlin_packages_to_use_import_on_demand = unset +ij_kotlin_packages_to_use_import_on_demand = "" [{*.markdown,*.md}] ij_markdown_force_one_space_after_blockquote_symbol = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d9f66ea3a2..bddc4d68df 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,9 +5,9 @@ codemodernizer/ @aws/elastic-gumby codetransform/ @aws/elastic-gumby codewhisperer/ @aws/codewhisperer-team -mynah-ui/ @aws/dexp -mynah-ui/package.json @aws/dexp @aws/elastic-gumby @aws/earlybird @aws/codewhisperer-team -mynah-ui/package-lock.json @aws/dexp @aws/elastic-gumby @aws/earlybird @aws/codewhisperer-team +mynah-ui/ @aws/flare +mynah-ui/package.json @aws/flare @aws/elastic-gumby @aws/earlybird @aws/codewhisperer-team +mynah-ui/package-lock.json @aws/flare @aws/elastic-gumby @aws/earlybird @aws/codewhisperer-team # nested within chat/, so needs to be after to take precedence amazonqFeatureDev/ @aws/earlybird diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 9ebe77f813..4e5d0a67b2 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -9,6 +9,9 @@ on: push: branches: [ main, feature/* ] +concurrency: + group: ${{ github.workflow }}${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: generate_artifact_toolkit_standalone: diff --git a/.github/workflows/q-mega-prerelease.yml b/.github/workflows/q-mega-prerelease.yml deleted file mode 100644 index e919987d23..0000000000 --- a/.github/workflows/q-mega-prerelease.yml +++ /dev/null @@ -1,164 +0,0 @@ -name: Prerelease Q-only -on: - workflow_dispatch: - inputs: - tag_name: - description: 'Tag name for release' - required: false - default: prerelease - push: - branches: [ feature/q-mega ] - -jobs: - time: - outputs: - time: ${{ steps.time.outputs.time }} - runs-on: ubuntu-latest - steps: - - id: time - run: | - echo "time=$(date +%s)" >> "$GITHUB_OUTPUT" - - generate_artifact_q: - needs: [ time ] - strategy: - matrix: - supported_versions: [ '2024.1', '2024.2', '2024.3' ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: remove unwanted dependencies - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - uses: actions/setup-java@v4 - with: - distribution: 'corretto' - java-version: '21' - - name: Generate artifact - run: | - sed -i.bak "s/\(toolkitVersion\s*=\s*\)\(.*\)/\1\2-${{ needs.time.outputs.time }}/" gradle.properties - ./gradlew -PideProfileName=${{ matrix.supported_versions }} :plugin-amazonq:buildPlugin - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: plugin-amazonq-${{ matrix.supported_versions }} - path: ./plugins/amazonq/build/distributions/*.zip - retention-days: 1 - - generate_artifact_core: - needs: [ time ] - strategy: - matrix: - supported_versions: [ '2024.1', '2024.2', '2024.3' ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: remove unwanted dependencies - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - uses: actions/setup-java@v4 - with: - distribution: 'corretto' - java-version: '21' - - - name: Generate artifact - run: | - sed -i.bak "s/\(toolkitVersion\s*=\s*\)\(.*\)/\1\2-${{ needs.time.outputs.time }}/" gradle.properties - ./gradlew -PideProfileName=${{ matrix.supported_versions }} :plugin-core:buildPlugin - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: plugin-core-${{ matrix.supported_versions }} - path: ./plugins/core/build/distributions/*.zip - retention-days: 1 - - generate_changelog: - needs: [ time ] - runs-on: ubuntu-latest - outputs: - feature: ${{ steps.assign_output.outputs.feature }} - tagname: ${{ steps.assign_output.outputs.tagname }} - version: ${{ steps.assign_output.outputs.version }} - changes: ${{ steps.assign_output.outputs.changes }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: 'corretto' - java-version: '21' - - - if: github.event_name == 'workflow_dispatch' - run: | - echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV - - if: github.ref_name != 'main' - run: | - TAG_NAME=${{ github.ref_name }} - FEAT_NAME=$(echo $TAG_NAME | sed 's/feature\///') - echo "FEAT_NAME=$FEAT_NAME" >> $GITHUB_ENV - echo "TAG_NAME=pre-$FEAT_NAME" >> $GITHUB_ENV - - if: github.ref_name == 'main' - run: | - echo "FEAT_NAME=" >> $GITHUB_ENV - echo "TAG_NAME=prerelease" >> $GITHUB_ENV - - name: Generate changelog - id: changelog - run: | - sed -i.bak "s/\(toolkitVersion\s*=\s*\)\(.*\)/\1\2-${{ needs.time.outputs.time }}/" gradle.properties - ./gradlew :createRelease :generateChangelog - - name: Provide the metadata to output - id: assign_output - run: | - echo "feature=$FEAT_NAME" >> $GITHUB_OUTPUT - echo "tagname=$TAG_NAME" >> $GITHUB_OUTPUT - echo "version=$(cat gradle.properties | grep toolkitVersion | cut -d'=' -f2)" >> $GITHUB_OUTPUT - echo 'changes<> $GITHUB_OUTPUT - cat CHANGELOG.md | perl -ne 'BEGIN{$/="\n\n"} print; exit if $. == 1' >> $GITHUB_OUTPUT - echo 'EOF' >> $GITHUB_OUTPUT - - publish: - needs: [ generate_artifact_core, generate_artifact_q, generate_changelog ] - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - FEAT_NAME: ${{ needs.generate_changelog.outputs.feature }} - TAG_NAME: ${{ needs.generate_changelog.outputs.tagname }} - AWS_TOOLKIT_VERSION: ${{ needs.generate_changelog.outputs.version }} - BRANCH: ${{ github.ref_name }} - AWS_TOOLKIT_CHANGES: ${{ needs.generate_changelog.outputs.changes }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - - name: Delete existing prerelease - # "prerelease" (main branch) or "pre-" - if: "env.TAG_NAME == 'prerelease' || startsWith(env.TAG_NAME, 'pre-')" - run: | - echo "SUBJECT=AWS Toolkit ${AWS_TOOLKIT_VERSION}: ${FEAT_NAME:-${TAG_NAME}}" >> $GITHUB_ENV - gh release delete "$TAG_NAME" --cleanup-tag --yes || true - - name: Publish to GitHub Releases - run: | - envsubst < "$GITHUB_WORKSPACE/.github/workflows/prerelease_notes.md" > "$RUNNER_TEMP/prerelease_notes.md" - gh release create "$TAG_NAME" --prerelease --notes-file "$RUNNER_TEMP/prerelease_notes.md" --title "$SUBJECT" --target $GITHUB_SHA - - name: Publish core - run: | - gh release upload "$TAG_NAME" plugin-core-*/*.zip - - name: Publish Q - run: | - gh release upload "$TAG_NAME" plugin-amazonq-*/*.zip - - name: Publish XML manifest - run: | - gh release view "$TAG_NAME" --repo "$GITHUB_REPOSITORY" --json assets | python3 "$GITHUB_WORKSPACE/.github/workflows/generateUpdatesXml.py" - > updatePlugins.xml - gh release upload "$TAG_NAME" updatePlugins.xml diff --git a/.github/workflows/ssm-integ.yml b/.github/workflows/ssm-integ.yml index 3d50adbb86..d18f63d2d0 100644 --- a/.github/workflows/ssm-integ.yml +++ b/.github/workflows/ssm-integ.yml @@ -9,6 +9,7 @@ on: branches: [ main, feature/* ] # PRs only need to run this if the SSM plugin logic has changed paths: + - '.github/workflows/ssm-integ.yml' - 'jetbrains-core/src/software/aws/toolkits/jetbrains/services/ssm/SsmPlugin.kt' - 'jetbrains-core/it/software/aws/toolkits/jetbrains/services/ssm/SsmPluginTest.kt' @@ -28,10 +29,10 @@ jobs: run: git config --system core.longpaths true if: ${{ matrix.os == 'windows-latest' }} - uses: actions/checkout@v2 - - name: Set up JDK 17 - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 with: - java-version: 17 + distribution: 'corretto' + java-version: '21' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle diff --git a/.gitignore b/.gitignore index 41dcb232c8..5b3693c327 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .gradle .intellijPlatform +.kotlin out/ target/ .idea diff --git a/.run/Run Amazon Q - Ultimate [2024.1].run.xml b/.run/Run Amazon Q - Ultimate [2024.1].run.xml index 4296908019..bae51e0465 100644 --- a/.run/Run Amazon Q - Ultimate [2024.1].run.xml +++ b/.run/Run Amazon Q - Ultimate [2024.1].run.xml @@ -22,4 +22,4 @@ false - \ No newline at end of file + diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a6630993..30f7bd5bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ +# _3.48_ (2025-01-16) +- **(Feature)** Enhance Q inline completion context fetching for better suggestion quality +- **(Feature)** /doc: Add error message if updated README is too large +- **(Bug Fix)** /transform: always include button to start a new transformation at the end of a job +- **(Bug Fix)** Amazon Q can update mvn and gradle build files +- **(Bug Fix)** Fix doc generation for modules that are a part of the project +- **(Bug Fix)** Amazon Q /dev: Remove hard-coded limits and instead rely server-side data to communicate number of code generations remaining +- **(Bug Fix)** /transform: automatically open pre-build error logs when available +- **(Bug Fix)** /doc: Fix code generation error when cancelling a documentation task +- **(Bug Fix)** Amazon Q - update messaging for /doc agent + +# _3.47_ (2025-01-09) +- **(Bug Fix)** Fix issue where users are unable to login to Amazon Q if they have previously authenticated ([#5214](https://github.com/aws/aws-toolkit-jetbrains/issues/5214)) +- **(Bug Fix)** Fix incorrect text shown while updating documentation in /doc +- **(Bug Fix)** Amazon Q Code Transformation: retry initial project upload on failure +- **(Bug Fix)** /transform: use correct doc link in SQL conversion help message +- **(Bug Fix)** Amazon Q /dev: Fix issue when files are deleted while preparing context +- **(Bug Fix)** Amazon Q /test: Test generation fails for files outside the project +- **(Bug Fix)** Amazon Q Code Transformation: allow PostgreSQL as target DB for SQL conversions +- **(Bug Fix)** Fix incorrect accept and reject buttons shows up while hovering over the generated file +- **(Bug Fix)** Prevent customization override if user has manually selected a customization +- **(Bug Fix)** Align UX text of document generation flow with vs code version + +# _3.46_ (2024-12-17) +- **(Feature)** /review: Code fix automatically scrolls into view after generation. +- **(Feature)** Chat: improve font size and line-height in footer (below prompt input field) +- **(Feature)** Adds capability to send new context commands to AB groups +- **(Bug Fix)** Chat: When writing a prompt without sending it, navigating via up/down arrows sometimes deletes the unsent prompt. +- **(Bug Fix)** Fix chat not retaining history when interaction is through onboarding tab type ([#5189](https://github.com/aws/aws-toolkit-jetbrains/issues/5189)) +- **(Bug Fix)** Chat: When navigating to previous prompts, code attachments are sometimes displayed incorrectly +- **(Bug Fix)** Reduce frequency of system information query + +# _3.45_ (2024-12-10) +- **(Feature)** Add acknowledgement button for Amazon Q Chat disclaimer +- **(Bug Fix)** Chosing cancel on sign out confirmation now cancels the sign out and does not delete profiles from ~/.aws/config ([#5167](https://github.com/aws/aws-toolkit-jetbrains/issues/5167)) +- **(Bug Fix)** Fix `@workspace` missing from the Amazon Q Chat welcome tab +- **(Bug Fix)** Fix for /review LLM based code issues for file review on windows +- **(Bug Fix)** Fix for File Review payload and Regex error for payload generation +- **(Bug Fix)** Amazon Q Code Transformation: show build logs when server-side build fails + +# _3.44_ (2024-12-04) +- **(Feature)** Amazon Q: UI improvements to chat: New splash loader animation, initial streaming card animation, improved button colours +- **(Feature)** Amazon Q: Navigate through prompt history by using the up/down arrows +- **(Bug Fix)** Fix issue where Amazon Q Code Transform is unable to start +- **(Bug Fix)** Fix DynamoDB viewer throwing 'ActionGroup should be registered using tag' on IDE start ([#5012](https://github.com/aws/aws-toolkit-jetbrains/issues/5012)) ([#5120](https://github.com/aws/aws-toolkit-jetbrains/issues/5120)) +- **(Bug Fix)** Amazon Q: Fix chat syntax highlighting when using several different themes + +# _3.43_ (2024-12-03) +- **(Feature)** `/review` in Q chat to scan your code for vulnerabilities and quality issues, and generate fixes +- **(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)** Added system notifications to inform users about critical plugin updates and potential issues with available workarounds + +# _3.42_ (2024-11-27) +- **(Feature)** Amazon Q /dev: support `Dockerfile` files +- **(Feature)** Feature(Amazon Q Code Transformation): allow users to view results in 5 smaller diffs +- **(Feature)** Introduce @workspace command to enhance chat context fetching for Chat +- **(Bug Fix)** Correct search text for Amazon Q inline suggestion keybindings +- **(Bug Fix)** Fix(Amazon Q Code Transformation): always show user latest/correct transformation results +- **(Bug Fix)** Amazon Q /dev: Fix error when accepting changes if leading slash is present. + # _3.41_ (2024-11-22) - **(Feature)** Amazon Q /dev: support `.gradle` files - **(Feature)** Inline Auto trigger will now happen more consistently and will not conflict with JetBrains code completion. diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a8ffb0067f..45238ea7b2 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -34,8 +34,6 @@ dependencies { testImplementation(libs.junit4) testImplementation(libs.bundles.mockito) testImplementation(gradleTestKit()) - - testRuntimeOnly(libs.junit5.jupiterVintage) } tasks.test { diff --git a/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts b/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts index b051f7e913..314ff99426 100644 --- a/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts +++ b/buildSrc/src/main/kotlin/toolkit-generate-sdks.gradle.kts @@ -38,6 +38,8 @@ java { tasks.withType().configureEach { options.encoding = "UTF-8" + sourceCompatibility = "17" + targetCompatibility = "17" } val generateTask = tasks.register("generateSdks") diff --git a/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts b/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts index 9eeb990c92..7b5994ab3e 100644 --- a/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/toolkit-publishing-conventions.gradle.kts @@ -66,3 +66,8 @@ tasks.verifyPlugin { // give each instance its own home dir systemProperty("plugin.verifier.home.dir", temporaryDir) } + +val pluginZip by configurations.creating +artifacts { + add("pluginZip", tasks.buildPlugin) +} diff --git a/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts b/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts index 9c83f195e5..ca37a88d9c 100644 --- a/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts +++ b/buildSrc/src/main/kotlin/toolkit-testing.gradle.kts @@ -20,10 +20,9 @@ dependencies { // Everything uses junit4/5 except rider, which uses TestNG testFixturesApi(platform(versionCatalog.findLibrary("junit5-bom").get())) - testFixturesApi(versionCatalog.findLibrary("junit5-jupiterApi").get()) - testFixturesApi(versionCatalog.findLibrary("junit5-jupiterParams").get()) + testFixturesApi(versionCatalog.findLibrary("junit5-jupiter").get()) - testRuntimeOnly(versionCatalog.findLibrary("junit5-jupiterEngine").get()) + testRuntimeOnly(versionCatalog.findLibrary("junit-platform-launcher").get()) testRuntimeOnly(versionCatalog.findLibrary("junit5-jupiterVintage").get()) } diff --git a/buildspec/linuxUiTests.yml b/buildspec/linuxUiTests.yml index c200f631cc..607736f9f4 100644 --- a/buildspec/linuxUiTests.yml +++ b/buildspec/linuxUiTests.yml @@ -19,7 +19,7 @@ env: phases: install: commands: - - dnf install -y marco mate-media + - dnf install -y marco mate-media e2fsprogs - startDesktop.sh # login to DockerHub so we don't get throttled @@ -45,10 +45,9 @@ phases: credential_source=EcsContainer" - chmod +x gradlew - - ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME :sandbox-all:prepareTestIdeUiSandbox --console plain --info - ffmpeg -loglevel quiet -nostdin -f x11grab -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -i ${DISPLAY} -codec:v libx264 -pix_fmt yuv420p -vf drawtext="fontsize=48:box=1:boxcolor=black@0.75:boxborderw=5:fontcolor=white:x=0:y=h-text_h:text='%{gmtime\:%H\\\\\:%M\\\\\:%S}'" -framerate 12 -g 12 /tmp/screen_recording.mp4 & - - ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME uiTestCore coverageReport --console plain --info + - ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME :ui-tests-starter:test coverageReport --console plain --info post_build: commands: diff --git a/detekt-rules/build.gradle.kts b/detekt-rules/build.gradle.kts index aa42eb403d..045281b95a 100644 --- a/detekt-rules/build.gradle.kts +++ b/detekt-rules/build.gradle.kts @@ -15,7 +15,6 @@ dependencies { // only used to make test work testRuntimeOnly(libs.slf4j.api) - testRuntimeOnly(libs.junit5.jupiterVintage) } tasks.test { diff --git a/detekt-rules/detekt.yml b/detekt-rules/detekt.yml index 3176ddae30..09fdafb5a9 100644 --- a/detekt-rules/detekt.yml +++ b/detekt-rules/detekt.yml @@ -72,7 +72,7 @@ formatting: maxLineLength: 160 ignoreBackTickedIdentifier: true NoWildcardImports: - # no `packagesToUseImportOnDemandProperty` because we don't want to allow any star imports + packagesToUseImportOnDemandProperty: "" active: true ParameterListWrapping: active: true diff --git a/gradle.properties b/gradle.properties index 3daceb7fcf..575fd640fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,13 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 # Toolkit Version -toolkitVersion=3.42-SNAPSHOT +toolkitVersion=3.49-SNAPSHOT # Publish Settings publishToken= publishChannel= -ideProfileName=2024.1 +ideProfileName=2024.2 remoteRobotPort=8080 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de7d410a66..7b55c86cec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] apache-commons-collections = "4.4" -apache-commons-io = "2.16.0" +apache-commons-io = "2.16.1" apache-commons-text = "1.12.0" assertJ = "3.26.3" # match with /settings.gradle.kts @@ -10,7 +10,7 @@ detekt = "1.23.7" diff-util = "4.12" intellijExt = "1.1.8" # match with /settings.gradle.kts -intellijGradle = "2.2.0-SNAPSHOT" +intellijGradle = "2.2.1" intellijRemoteRobot = "0.11.22" jackson = "2.17.2" jacoco = "0.8.12" @@ -27,7 +27,7 @@ mockitoKotlin = "5.4.0" mockk = "1.13.10" nimbus-jose-jwt = "9.40" node-gradle = "7.0.2" -telemetryGenerator = "1.0.278" +telemetryGenerator = "1.0.293" testLogger = "4.0.0" testRetry = "1.5.10" # test-only; platform provides slf4j transitively at runtime @@ -91,11 +91,13 @@ jackson-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xm jackson-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } + +# platfom launcher version selected by BOM +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } + junit4 = { module = "junit:junit", version.ref = "junit4" } junit5-bom = { module = "org.junit:junit-bom", version.ref = "junit5" } -junit5-jupiterApi = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } -junit5-jupiterEngine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } -junit5-jupiterParams = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } +junit5-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" } junit5-jupiterVintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } kotlin-coroutinesDebug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kotlinCoroutines" } diff --git a/noop/build.gradle.kts b/noop/build.gradle.kts new file mode 100644 index 0000000000..54de3e72a4 --- /dev/null +++ b/noop/build.gradle.kts @@ -0,0 +1,5 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// project that does nothing +tasks.register("test") diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index d08356231f..6949f4ba39 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -26,6 +26,9 @@ + + + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt new file mode 100644 index 0000000000..7741758d49 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt @@ -0,0 +1,153 @@ +// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.common.clients + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.SystemInfo +import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient +import software.amazon.awssdk.services.codewhispererruntime.model.ArtifactType +import software.amazon.awssdk.services.codewhispererruntime.model.ContentChecksumType +import software.amazon.awssdk.services.codewhispererruntime.model.CreateTaskAssistConversationRequest +import software.amazon.awssdk.services.codewhispererruntime.model.CreateTaskAssistConversationResponse +import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse +import software.amazon.awssdk.services.codewhispererruntime.model.DocGenerationEvent +import software.amazon.awssdk.services.codewhispererruntime.model.GetTaskAssistCodeGenerationResponse +import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory +import software.amazon.awssdk.services.codewhispererruntime.model.OperatingSystem +import software.amazon.awssdk.services.codewhispererruntime.model.OptOutPreference +import software.amazon.awssdk.services.codewhispererruntime.model.SendTelemetryEventResponse +import software.amazon.awssdk.services.codewhispererruntime.model.StartTaskAssistCodeGenerationResponse +import software.amazon.awssdk.services.codewhispererruntime.model.TaskAssistPlanningUploadContext +import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext +import software.amazon.awssdk.services.codewhispererruntime.model.UploadIntent +import software.amazon.awssdk.services.codewhispererruntime.model.UserContext +import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent +import software.aws.toolkits.core.utils.error +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.info +import software.aws.toolkits.jetbrains.common.session.Intent +import software.aws.toolkits.jetbrains.core.awsClient +import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager +import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection +import software.aws.toolkits.jetbrains.services.amazonq.clients.AmazonQStreamingClient +import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_EVALUATION_PRODUCT_NAME +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency +import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata +import software.aws.toolkits.jetbrains.settings.AwsSettings +import java.time.Instant +import software.amazon.awssdk.services.codewhispererruntime.model.ChatTriggerType as SyncChatTriggerType + +@Service(Service.Level.PROJECT) +class AmazonQCodeGenerateClient(private val project: Project) { + private fun getTelemetryOptOutPreference() = + if (AwsSettings.getInstance().isTelemetryEnabled) { + OptOutPreference.OPTIN + } else { + OptOutPreference.OPTOUT + } + + private val docGenerationUserContext = ClientMetadata.getDefault().let { + val osForFeatureDev: OperatingSystem = + when { + SystemInfo.isWindows -> OperatingSystem.WINDOWS + SystemInfo.isMac -> OperatingSystem.MAC + // For now, categorize everything else as "Linux" (Linux/FreeBSD/Solaris/etc.) + else -> OperatingSystem.LINUX + } + + UserContext.builder() + .ideCategory(IdeCategory.JETBRAINS) + .operatingSystem(osForFeatureDev) + .product(FEATURE_EVALUATION_PRODUCT_NAME) + .clientId(it.clientId) + .ideVersion(it.awsVersion) + .build() + } + + fun connection() = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) + ?: error("Attempted to use connection while one does not exist") + + fun bearerClient() = connection().getConnectionSettings().awsClient() + + private val amazonQStreamingClient + get() = AmazonQStreamingClient.getInstance(project) + + fun sendDocGenerationTelemetryEvent( + docGenerationEvent: DocGenerationEvent, + ): SendTelemetryEventResponse = + bearerClient().sendTelemetryEvent { requestBuilder -> + requestBuilder.telemetryEvent { telemetryEventBuilder -> + telemetryEventBuilder.docGenerationEvent(docGenerationEvent) + } + requestBuilder.optOutPreference(getTelemetryOptOutPreference()) + requestBuilder.userContext(docGenerationUserContext) + } + + fun createTaskAssistConversation(): CreateTaskAssistConversationResponse = bearerClient().createTaskAssistConversation( + CreateTaskAssistConversationRequest.builder().build() + ) + + fun createTaskAssistUploadUrl(conversationId: String, contentChecksumSha256: String, contentLength: Long): CreateUploadUrlResponse = + bearerClient().createUploadUrl { + it.contentChecksumType(ContentChecksumType.SHA_256) + .contentChecksum(contentChecksumSha256) + .contentLength(contentLength) + .artifactType(ArtifactType.SOURCE_CODE) + .uploadIntent(UploadIntent.TASK_ASSIST_PLANNING) + .uploadContext( + UploadContext.builder() + .taskAssistPlanningUploadContext( + TaskAssistPlanningUploadContext.builder() + .conversationId(conversationId) + .build() + ) + .build() + ) + } + + fun startTaskAssistCodeGeneration(conversationId: String, uploadId: String, userMessage: String, intent: Intent): StartTaskAssistCodeGenerationResponse = + bearerClient() + .startTaskAssistCodeGeneration { request -> + request + .conversationState { + it + .conversationId(conversationId) + .chatTriggerType(SyncChatTriggerType.MANUAL) + .currentMessage { cm -> cm.userInputMessage { um -> um.content(userMessage) } } + } + .workspaceState { + it + .programmingLanguage { pl -> pl.languageName("javascript") } + .uploadId(uploadId) + } + .intent(intent.name) + } + + fun getTaskAssistCodeGeneration(conversationId: String, codeGenerationId: String): GetTaskAssistCodeGenerationResponse = bearerClient() + .getTaskAssistCodeGeneration { + it + .conversationId(conversationId) + .codeGenerationId(codeGenerationId) + } + + suspend fun exportTaskAssistResultArchive(conversationId: String): MutableList = amazonQStreamingClient.exportResultArchive( + conversationId, + ExportIntent.TASK_ASSIST, + null, + { e -> + LOG.error(e) { "TaskAssist - ExportResultArchive stream exportId=$conversationId exportIntent=${ExportIntent.TASK_ASSIST} Failed: ${e.message} " } + }, + { startTime -> + LOG.info { "TaskAssist - ExportResultArchive latency: ${calculateTotalLatency(startTime, Instant.now())}" } + } + ) + + companion object { + private val LOG = getLogger() + + fun getInstance(project: Project) = project.service() + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/ConversationNotStartedState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/ConversationNotStartedState.kt new file mode 100644 index 0000000000..ad300449df --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/ConversationNotStartedState.kt @@ -0,0 +1,21 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.common.session + +import software.aws.toolkits.jetbrains.services.amazonqDoc.session.SessionStateInteraction +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStateAction +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource + +class ConversationNotStartedState( + override var approach: String, + override val tabID: String, + override var token: CancellationTokenSource?, +) : SessionState { + override val phase = SessionStatePhase.INIT + + override suspend fun interact(action: SessionStateAction): SessionStateInteraction { + error("Illegal transition between states, restart the conversation") + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionState.kt new file mode 100644 index 0000000000..1585096382 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionState.kt @@ -0,0 +1,17 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.common.session + +import software.aws.toolkits.jetbrains.services.amazonqDoc.session.SessionStateInteraction +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStateAction +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource + +interface SessionState { + val tabID: String + val phase: SessionStatePhase? + var token: CancellationTokenSource? + var approach: String + suspend fun interact(action: SessionStateAction): SessionStateInteraction +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionStateTypes.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionStateTypes.kt new file mode 100644 index 0000000000..3b50f10ca9 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/session/SessionStateTypes.kt @@ -0,0 +1,24 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.common.session + +import software.aws.toolkits.jetbrains.common.util.AmazonQCodeGenService +import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext + +open class SessionStateConfig( + open val conversationId: String, + open val repoContext: FeatureDevSessionContext, + open val amazonQCodeGenService: AmazonQCodeGenService, +) + +data class SessionStateConfigData( + override val conversationId: String, + override val repoContext: FeatureDevSessionContext, + override val amazonQCodeGenService: AmazonQCodeGenService, +) : SessionStateConfig(conversationId, repoContext, amazonQCodeGenService) + +enum class Intent { + DEV, + DOC, +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/AmazonQCodeGenService.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/AmazonQCodeGenService.kt new file mode 100644 index 0000000000..afe9da01b2 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/AmazonQCodeGenService.kt @@ -0,0 +1,214 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.common.util + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.intellij.openapi.project.Project +import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException +import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse +import software.amazon.awssdk.services.codewhispererruntime.model.GetTaskAssistCodeGenerationResponse +import software.amazon.awssdk.services.codewhispererruntime.model.ServiceQuotaExceededException +import software.amazon.awssdk.services.codewhispererruntime.model.StartTaskAssistCodeGenerationResponse +import software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException +import software.amazon.awssdk.services.codewhispererruntime.model.ValidationException +import software.amazon.awssdk.services.codewhispererstreaming.model.CodeWhispererStreamingException +import software.aws.toolkits.core.utils.debug +import software.aws.toolkits.core.utils.error +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.warn +import software.aws.toolkits.jetbrains.common.clients.AmazonQCodeGenerateClient +import software.aws.toolkits.jetbrains.common.session.Intent +import software.aws.toolkits.jetbrains.services.amazonqDoc.session.DocGenerationStreamResult +import software.aws.toolkits.jetbrains.services.amazonqDoc.session.ExportDocTaskAssistResultArchiveStreamResult +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ContentLengthException +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ExportParseException +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevOperation +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ZipFileCorruptedException +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl +import software.aws.toolkits.telemetry.AmazonqTelemetry +import software.aws.toolkits.telemetry.MetricResult + +class AmazonQCodeGenService(val proxyClient: AmazonQCodeGenerateClient, val project: Project) { + fun createConversation(): String { + val startTime = System.currentTimeMillis() + var failureReason: String? = null + var failureReasonDesc: String? = null + var result: MetricResult = MetricResult.Succeeded + var conversationId: String? = null + try { + logger.debug { "Executing createTaskAssistConversation" } + val taskAssistConversationResult = proxyClient.createTaskAssistConversation() + conversationId = taskAssistConversationResult.conversationId() + logger.debug { + "$FEATURE_NAME: Created conversation: {conversationId: $conversationId, requestId: ${ + taskAssistConversationResult.responseMetadata().requestId() + }" + } + + return conversationId + } catch (e: Exception) { + logger.warn(e) { "$FEATURE_NAME: Failed to start conversation: ${e.message}" } + result = MetricResult.Failed + failureReason = e.javaClass.simpleName + if (e is FeatureDevException) { + failureReason = e.reason() + failureReasonDesc = e.reasonDesc() + } + var errMssg = e.message + if (e is CodeWhispererRuntimeException) { + errMssg = e.awsErrorDetails().errorMessage() + logger.warn(e) { "Start conversation failed for request: ${e.requestId()}" } + + if (e is ServiceQuotaExceededException) { + throw MonthlyConversationLimitError(errMssg, operation = FeatureDevOperation.CreateConversation.toString(), desc = null, cause = e.cause) + } + } + throw FeatureDevException(errMssg, operation = FeatureDevOperation.CreateConversation.toString(), desc = null, e.cause) + } finally { + AmazonqTelemetry.startConversationInvoke( + amazonqConversationId = conversationId, + result = result, + reason = failureReason, + reasonDesc = failureReasonDesc, + duration = (System.currentTimeMillis() - startTime).toDouble(), + credentialStartUrl = getStartUrl(project = this.project), + ) + } + } + + fun createUploadUrl(conversationId: String, contentChecksumSha256: String, contentLength: Long, uploadId: String): + CreateUploadUrlResponse { + try { + logger.debug { "Executing createUploadUrl with conversationId $conversationId" } + val uploadUrlResponse = proxyClient.createTaskAssistUploadUrl( + conversationId, + contentChecksumSha256, + contentLength, + ) + logger.debug { + "$FEATURE_NAME: Created upload url: {uploadId: $uploadId, requestId: ${uploadUrlResponse.responseMetadata().requestId()}}" + } + return uploadUrlResponse + } catch (e: Exception) { + logger.warn(e) { "$FEATURE_NAME: Failed to generate presigned url: ${e.message}" } + + var errMssg = e.message + if (e is CodeWhispererRuntimeException) { + errMssg = e.awsErrorDetails().errorMessage() + logger.warn(e) { "Create UploadUrl failed for request: ${e.requestId()}" } + + if (e is ValidationException && e.message?.contains("Invalid contentLength") == true) { + throw ContentLengthException(operation = FeatureDevOperation.CreateUploadUrl.toString(), desc = null, cause = e.cause) + } + } + throw FeatureDevException(errMssg, operation = FeatureDevOperation.CreateUploadUrl.toString(), desc = null, e.cause) + } + } + + open fun startTaskAssistCodeGeneration(conversationId: String, uploadId: String, message: String, intent: Intent): + StartTaskAssistCodeGenerationResponse { + try { + logger.debug { "Executing startTaskAssistCodeGeneration with conversationId: $conversationId , uploadId: $uploadId" } + val startCodeGenerationResponse = proxyClient.startTaskAssistCodeGeneration( + conversationId, + uploadId, + message, + intent + ) + + logger.debug { "$FEATURE_NAME: Started code generation with requestId: ${startCodeGenerationResponse.responseMetadata().requestId()}" } + return startCodeGenerationResponse + } catch (e: Exception) { + logger.warn(e) { "$FEATURE_NAME: Failed to execute startTaskAssistCodeGeneration ${e.message}" } + + var errMssg = e.message + if (e is CodeWhispererRuntimeException) { + errMssg = e.awsErrorDetails().errorMessage() + logger.warn(e) { "StartTaskAssistCodeGeneration failed for request: ${e.requestId()}" } + + // API Front-end will throw Throttling if conversation limit is reached. API Front-end monitors StartCodeGeneration for throttling + if (e is software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException && + e.message?.contains("StartTaskAssistCodeGeneration reached for this month.") == true + ) { + throw MonthlyConversationLimitError(errMssg, operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause) + } + + if (e is ServiceQuotaExceededException || ( + e is ThrottlingException && e.message?.contains( + "limit for number of iterations on a code generation" + ) == true + ) + ) { + throw CodeIterationLimitException(operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause) + } else if (e is ValidationException && e.message?.contains("repo size is exceeding the limits") == true) { + throw ContentLengthException(operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, cause = e.cause) + } else if (e is ValidationException && e.message?.contains("zipped file is corrupted") == true) { + throw ZipFileCorruptedException(operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause) + } + } + throw FeatureDevException(errMssg, operation = FeatureDevOperation.StartTaskAssistCodeGeneration.toString(), desc = null, e.cause) + } + } + + fun getTaskAssistCodeGeneration(conversationId: String, codeGenerationId: String): GetTaskAssistCodeGenerationResponse { + try { + logger.debug { "Executing GetTaskAssistCodeGeneration with conversationId: $conversationId , codeGenerationId: $codeGenerationId" } + val getCodeGenerationResponse = proxyClient.getTaskAssistCodeGeneration(conversationId, codeGenerationId) + + logger.debug { + "$FEATURE_NAME: Received code generation status $getCodeGenerationResponse with requestId ${ + getCodeGenerationResponse.responseMetadata() + .requestId() + }" + } + return getCodeGenerationResponse + } catch (e: Exception) { + logger.warn(e) { "$FEATURE_NAME: Failed to execute GetTaskAssistCodeGeneration ${e.message}" } + + var errMssg = e.message + if (e is CodeWhispererRuntimeException) { + errMssg = e.awsErrorDetails().errorMessage() + logger.warn(e) { "GetTaskAssistCodeGeneration failed for request: ${e.requestId()}" } + } + throw FeatureDevException(errMssg, operation = FeatureDevOperation.GetTaskAssistCodeGeneration.toString(), desc = null, e.cause) + } + } + + suspend fun exportTaskAssistArchiveResult(conversationId: String): DocGenerationStreamResult { + val exportResponse: MutableList + try { + exportResponse = proxyClient.exportTaskAssistResultArchive(conversationId) + logger.debug { "$FEATURE_NAME: Received export task assist result archive response" } + } catch (e: Exception) { + logger.warn(e) { "$FEATURE_NAME: Failed to export archive result: ${e.message}" } + + var errMssg = e.message + if (e is CodeWhispererStreamingException) { + errMssg = e.awsErrorDetails().errorMessage() + logger.warn(e) { "ExportTaskAssistArchiveResult failed for request: ${e.requestId()}" } + } + throw FeatureDevException(errMssg, operation = FeatureDevOperation.ExportTaskAssistArchiveResult.toString(), desc = null, e.cause) + } + + val parsedResult: ExportDocTaskAssistResultArchiveStreamResult + try { + val result = exportResponse.reduce { acc, next -> acc + next } // To map the result it is needed to combine the full byte array + parsedResult = jacksonObjectMapper().readValue(result) + } catch (e: Exception) { + logger.error(e) { "Failed to parse downloaded code results" } + throw ExportParseException(operation = FeatureDevOperation.ExportTaskAssistArchiveResult.toString(), desc = null, e.cause) + } + + return parsedResult.codeGenerationResult + } + + companion object { + private val logger = getLogger() + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/FileUtils.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/FileUtils.kt new file mode 100644 index 0000000000..6cf8cc2476 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/util/FileUtils.kt @@ -0,0 +1,82 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.common.util + +import com.github.difflib.DiffUtils +import com.github.difflib.patch.DeltaType +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.CharsetToolkit +import com.intellij.openapi.vfs.VirtualFile +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteIfExists +import kotlin.io.path.writeBytes + +fun resolveAndCreateOrUpdateFile(projectRootPath: Path, relativeFilePath: String, fileContent: String) { + val filePath = projectRootPath.resolve(relativeFilePath) + filePath.parent.createDirectories() // Create directories if needed + filePath.writeBytes(fileContent.toByteArray(Charsets.UTF_8)) +} + +fun resolveAndDeleteFile(projectRootPath: Path, relativePath: String) { + val filePath = projectRootPath.resolve(relativePath) + filePath.deleteIfExists() +} + +fun selectFolder(project: Project, openOn: VirtualFile): VirtualFile? { + val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor() + return FileChooser.chooseFile(fileChooserDescriptor, project, openOn) +} + +fun readFileToString(file: File): String { + val charsetToolkit = CharsetToolkit(file.readBytes(), Charset.forName("UTF-8"), false) + val charset = charsetToolkit.guessEncoding(4096) + return file.readText(charset) +} + +/** + * Calculates the number of added characters and lines between existing content and LLM response + * + * @param existingContent The original text content before changes + * @param llmResponse The new text content from the LLM + * @return A Map containing: + * - "addedChars": Total number of new characters added + * - "addedLines": Total number of new lines added + */ +data class DiffResult(val addedChars: Int, val addedLines: Int) + +fun getDiffCharsAndLines( + existingContent: String, + llmResponse: String, +): DiffResult { + var addedChars = 0 + var addedLines = 0 + + val existingLines = existingContent.lines() + val llmLines = llmResponse.lines() + + val patch = DiffUtils.diff(existingLines, llmLines) + + for (delta in patch.deltas) { + when (delta.type) { + DeltaType.INSERT -> { + addedChars += delta.target.lines.sumOf { it.length } + addedLines += delta.target.lines.size + } + + DeltaType.CHANGE -> { + addedChars += delta.target.lines.sumOf { it.length } + addedLines += delta.target.lines.size + } + + else -> {} // Do nothing for DELETE + } + } + + return DiffResult(addedChars, addedLines) +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt index 95fbabd3a4..de01e99bad 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt @@ -9,6 +9,8 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.wm.ToolWindowManager import icons.AwsIcons import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID +import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.runScanKey import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend import software.aws.toolkits.resources.message import software.aws.toolkits.telemetry.UiTelemetry @@ -19,5 +21,8 @@ class QOpenPanelAction : AnAction(message("action.q.openchat.text"), null, AwsIc val project = e.getRequiredData(CommonDataKeys.PROJECT) UiTelemetry.click(project, "q_openChat") ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, true) + if (e.getData(runScanKey) == true) { + AmazonQToolWindow.openScanTab(project) + } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt index e5673cc6da..15a533d119 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt @@ -24,9 +24,14 @@ import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType +import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.runCodeScanMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable +import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable import javax.swing.JComponent @@ -72,6 +77,14 @@ class AmazonQToolWindow private constructor( } } + private fun sendMessageAppToUi(message: AmazonQMessage, tabType: String) { + appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach { + scope.launch { + it.messagesFromAppToUi.publish(message) + } + } + } + private fun initConnections() { val apps = appSource.getApps(project) apps.forEach { app -> @@ -110,7 +123,11 @@ class AmazonQToolWindow private constructor( chatBrowser.init( isCodeTransformAvailable = isCodeTransformAvailable(project), - isFeatureDevAvailable = isFeatureDevAvailable(project) + isFeatureDevAvailable = isFeatureDevAvailable(project), + isCodeScanAvailable = isCodeScanAvailable(project), + isCodeTestAvailable = isCodeTestAvailable(project), + isDocAvailable = isDocAvailable(project), + highlightCommand = highlightCommand() ) scope.launch { @@ -134,17 +151,25 @@ class AmazonQToolWindow private constructor( companion object { fun getInstance(project: Project): AmazonQToolWindow = project.service() + private fun showChatWindow(project: Project) = runInEdt { + val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID) + toolWindow?.show() + } + fun getStarted(project: Project) { // Make sure the window is shown - runInEdt { - val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID) - toolWindow?.show() - } + showChatWindow(project) // Send the interaction message val window = getInstance(project) window.sendMessage(OnboardingPageInteraction(OnboardingPageInteractionType.CwcButtonClick), "cwc") } + + fun openScanTab(project: Project) { + showChatWindow(project) + val window = getInstance(project) + window.sendMessageAppToUi(runCodeScanMessage, tabType = "codescan") + } } override fun dispose() { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt index e31a74e57d..f540bfeca8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt @@ -10,8 +10,8 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.openapi.wm.ex.ToolWindowEx -import com.intellij.ui.content.Content -import com.intellij.ui.content.ContentManager +import com.intellij.ui.components.panels.Wrapper +import com.intellij.util.ui.components.BorderLayoutPanel import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection @@ -22,6 +22,8 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener +import software.aws.toolkits.jetbrains.core.notifications.NotificationPanel +import software.aws.toolkits.jetbrains.core.notifications.ProcessNotificationsBase import software.aws.toolkits.jetbrains.core.webview.BrowserState import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener @@ -35,7 +37,21 @@ import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val mainPanel = BorderLayoutPanel() + val qPanel = Wrapper() + val notificationPanel = NotificationPanel() + + mainPanel.addToTop(notificationPanel) + mainPanel.add(qPanel) + val notifListener = ProcessNotificationsBase.getInstance(project) + notifListener.addListenerForNotification { bannerContent -> + runInEdt { + notificationPanel.updateNotificationPanel(bannerContent) + } + } + if (toolWindow is ToolWindowEx) { val actionManager = ActionManager.getInstance() toolWindow.setTitleActions(listOf(actionManager.getAction("aws.q.toolwindow.titleBar"))) @@ -46,7 +62,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { ToolkitConnectionManagerListener.TOPIC, object : ToolkitConnectionManagerListener { override fun activeConnectionChanged(newConnection: ToolkitConnection?) { - onConnectionChanged(project, newConnection, toolWindow) + onConnectionChanged(project, newConnection, qPanel) } } ) @@ -56,8 +72,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { object : RefreshQChatPanelButtonPressedListener { override fun onRefresh() { runInEdt { - contentManager.removeAllContents(true) - prepareChatContent(project, contentManager) + prepareChatContent(project, qPanel) } } } @@ -68,43 +83,37 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { object : BearerTokenProviderListener { override fun onChange(providerId: String, newScopes: List?) { if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) { - val content = contentManager.factory.createContent(AmazonQToolWindow.getInstance(project).component, null, false).also { - it.isCloseable = true - it.isPinnable = true - } + val qComponent = AmazonQToolWindow.getInstance(project).component runInEdt { - contentManager.removeAllContents(true) - contentManager.addContent(content) + qPanel.setContent(qComponent) } } } } ) - val content = prepareChatContent(project, contentManager) + prepareChatContent(project, qPanel) + val content = contentManager.factory.createContent(mainPanel, null, false).also { + it.isCloseable = true + it.isPinnable = true + } toolWindow.activate(null) - contentManager.setSelectedContent(content) + contentManager.addContent(content) } private fun prepareChatContent( project: Project, - contentManager: ContentManager, - ): Content { + qPanel: Wrapper, + ) { val component = if (isQConnected(project) && !isQExpired(project)) { AmazonQToolWindow.getInstance(project).component } else { QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ)) QWebviewPanel.getInstance(project).component } - - val content = contentManager.factory.createContent(component, null, false).also { - it.isCloseable = true - it.isPinnable = true - } - contentManager.addContent(content) - return content + qPanel.setContent(component) } override fun init(toolWindow: ToolWindow) { @@ -125,8 +134,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { override fun shouldBeAvailable(project: Project): Boolean = isQWebviewsAvailable() - private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, toolWindow: ToolWindow) { - val contentManager = toolWindow.contentManager + private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, qPanel: Wrapper) { val isNewConnectionForQ = newConnection?.let { (it as? AwsBearerTokenConnection)?.let { conn -> val scopeShouldHave = Q_SCOPES @@ -151,15 +159,8 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { LOG.debug { "returning login window; no Q connection found" } QWebviewPanel.getInstance(project).component } - - val content = contentManager.factory.createContent(component, null, false).also { - it.isCloseable = true - it.isPinnable = true - } - runInEdt { - contentManager.removeAllContents(true) - contentManager.addContent(content) + qPanel.setContent(component) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt new file mode 100644 index 0000000000..72524e49b2 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt @@ -0,0 +1,39 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.toolwindow + +import com.intellij.openapi.project.Project +import com.intellij.ui.components.panels.Wrapper +import com.intellij.util.ui.components.BorderLayoutPanel +import software.aws.toolkits.core.utils.error +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.jetbrains.core.webview.BrowserState +import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel +import software.aws.toolkits.jetbrains.utils.isQConnected +import software.aws.toolkits.jetbrains.utils.isQExpired +import software.aws.toolkits.telemetry.FeatureId +import javax.swing.JComponent + +class OuterAmazonQPanel(val project: Project) : BorderLayoutPanel() { + private val wrapper = Wrapper() + init { + isOpaque = false + addToCenter(wrapper) + val component = if (isQConnected(project) && !isQExpired(project)) { + AmazonQToolWindow.getInstance(project).component + } else { + QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ)) + QWebviewPanel.getInstance(project).component + } + updateQPanel(component) + } + + fun updateQPanel(content: JComponent) { + try { + wrapper.setContent(content) + } catch (e: Exception) { + getLogger().error { "Error while creating window" } + } + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/util/HighlightCommand.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/util/HighlightCommand.kt new file mode 100644 index 0000000000..5019d051c1 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/util/HighlightCommand.kt @@ -0,0 +1,16 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.util + +import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService + +data class HighlightCommand(val command: String, val description: String) + +fun highlightCommand(): HighlightCommand? { + val feature = CodeWhispererFeatureConfigService.getInstance().getHighlightCommandFeature() + + if (feature == null || feature.value.stringValue().isEmpty()) return null + + return HighlightCommand(feature.value.stringValue(), feature.variation) +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt index 9a4f30222d..cc39985dd3 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt @@ -3,11 +3,14 @@ package software.aws.toolkits.jetbrains.services.amazonq.webview +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.ui.jcef.JBCefJSQuery import org.cef.CefApp +import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser +import software.aws.toolkits.jetbrains.settings.MeetQSettings /* Displays the web view for the Amazon Q tool window @@ -18,7 +21,14 @@ class Browser(parent: Disposable) : Disposable { val receiveMessageQuery = JBCefJSQuery.create(jcefBrowser) - fun init(isCodeTransformAvailable: Boolean, isFeatureDevAvailable: Boolean) { + fun init( + isCodeTransformAvailable: Boolean, + isFeatureDevAvailable: Boolean, + isDocAvailable: Boolean, + isCodeScanAvailable: Boolean, + isCodeTestAvailable: Boolean, + highlightCommand: HighlightCommand?, + ) { // register the scheme handler to route http://mynah/ URIs to the resources/assets directory on classpath CefApp.getInstance() .registerSchemeHandlerFactory( @@ -27,7 +37,7 @@ class Browser(parent: Disposable) : Disposable { AssetResourceHandler.AssetResourceHandlerFactory(), ) - loadWebView(isCodeTransformAvailable, isFeatureDevAvailable) + loadWebView(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand) } override fun dispose() { @@ -42,19 +52,35 @@ class Browser(parent: Disposable) : Disposable { .executeJavaScript("window.postMessage(JSON.stringify($message))", jcefBrowser.cefBrowser.url, 0) // Load the chat web app into the jcefBrowser - private fun loadWebView(isCodeTransformAvailable: Boolean, isFeatureDevAvailable: Boolean) { + private fun loadWebView( + isCodeTransformAvailable: Boolean, + isFeatureDevAvailable: Boolean, + isDocAvailable: Boolean, + isCodeScanAvailable: Boolean, + isCodeTestAvailable: Boolean, + highlightCommand: HighlightCommand?, + ) { // setup empty state. The message request handlers use this for storing state // that's persistent between page loads. jcefBrowser.setProperty("state", "") // load the web app - jcefBrowser.loadHTML(getWebviewHTML(isCodeTransformAvailable, isFeatureDevAvailable)) + jcefBrowser.loadHTML( + getWebviewHTML(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand) + ) } /** * Generate index.html for the web view * @return HTML source */ - private fun getWebviewHTML(isCodeTransformAvailable: Boolean, isFeatureDevAvailable: Boolean): String { + private fun getWebviewHTML( + isCodeTransformAvailable: Boolean, + isFeatureDevAvailable: Boolean, + isDocAvailable: Boolean, + isCodeScanAvailable: Boolean, + isCodeTestAvailable: Boolean, + highlightCommand: HighlightCommand?, + ): String { val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)") val jsScripts = """ @@ -67,9 +93,15 @@ class Browser(parent: Disposable) : Disposable { $postMessageToJavaJsCode } }, + ${MeetQSettings.getInstance().reinvent2024OnboardingCount < MAX_ONBOARDING_PAGE_COUNT}, + ${MeetQSettings.getInstance().disclaimerAcknowledged}, $isFeatureDevAvailable, // whether /dev is available $isCodeTransformAvailable, // whether /transform is available - ); + $isDocAvailable, // whether /doc is available + $isCodeScanAvailable, // whether /scan is available + $isCodeTestAvailable, // whether /test is available + ${OBJECT_MAPPER.writeValueAsString(highlightCommand)} + ); } """.trimIndent() @@ -89,5 +121,7 @@ class Browser(parent: Disposable) : Disposable { companion object { private const val WEB_SCRIPT_URI = "http://mynah/js/mynah-ui.js" + private const val MAX_ONBOARDING_PAGE_COUNT = 3 + private val OBJECT_MAPPER = jacksonObjectMapper() } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index b2571123b7..af9c4b5fce 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -3,6 +3,8 @@ package software.aws.toolkits.jetbrains.services.amazonq.webview +import com.intellij.ide.BrowserUtil +import com.intellij.ide.util.RunOnceUtil import com.intellij.ui.jcef.JBCefJSQuery.Response import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.awaitClose @@ -21,6 +23,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.util.command import software.aws.toolkits.jetbrains.services.amazonq.util.tabType import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.AmazonQTheme import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.ThemeBrowserAdapter +import software.aws.toolkits.jetbrains.settings.MeetQSettings +import software.aws.toolkits.telemetry.Telemetry import java.util.function.Function class BrowserConnector( @@ -37,9 +41,40 @@ class BrowserConnector( addMessageHook(browser) .onEach { json -> val node = serializer.toNode(json) - if (node.command == "ui-is-ready") { - uiReady.complete(true) + when (node.command) { + "ui-is-ready" -> { + uiReady.complete(true) + RunOnceUtil.runOnceForApp("AmazonQ-UI-Ready") { + MeetQSettings.getInstance().reinvent2024OnboardingCount += 1 + } + } + + "disclaimer-acknowledged" -> { + MeetQSettings.getInstance().disclaimerAcknowledged = true + } + + // some weird issue preventing deserialization from working + "open-user-guide" -> { + BrowserUtil.browse(node.get("userGuideLink").asText()) + } + "send-telemetry" -> { + val source = node.get("source") + val module = node.get("module") + val trigger = node.get("trigger") + + if (source != null) { + Telemetry.ui.click.use { + it.elementId(source.asText()) + } + } else if (module != null && trigger != null) { + Telemetry.toolkit.openModule.use { + it.module(module.asText()) + it.source(trigger.asText()) + } + } + } } + val tabType = node.tabType ?: return@onEach connections.filter { connection -> connection.app.tabTypes.contains(tabType) }.forEach { connection -> launch { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt new file mode 100644 index 0000000000..166223d623 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt @@ -0,0 +1,175 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection +import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection +import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager +import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener +import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection +import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState +import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider +import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController +import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanActionMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanMessageListener +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.controller.CodeScanChatController +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.AuthenticationNeededExceptionMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.AuthenticationUpdateMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CODE_SCAN_TAB_NAME +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.IncomingCodeScanMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage +import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable +import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable +import java.util.concurrent.atomic.AtomicBoolean + +private enum class CodeScanMessageTypes(val type: String) { + ClearChat("clear"), + Help("help"), + TabCreated("new-tab-was-created"), + TabRemoved("tab-was-removed"), + Scan("scan"), + StartProjectScan("codescan_start_project_scan"), + StartFileScan("codescan_start_file_scan"), + StopFileScan("codescan_stop_file_scan"), + StopProjectScan("codescan_stop_project_scan"), + OpenIssuesPanel("codescan_open_issues"), + ResponseBodyLinkClicked("response-body-link-click"), +} + +class CodeScanChatApp(private val scope: CoroutineScope) : AmazonQApp { + private val isProcessingAuthChanged = AtomicBoolean(false) + override val tabTypes = listOf(CODE_SCAN_TAB_NAME) + override fun init(context: AmazonQAppInitContext) { + val chatSessionStorage = ChatSessionStorage() + val inboundAppMessagesHandler: InboundAppMessagesHandler = CodeScanChatController(context, chatSessionStorage) + + context.messageTypeRegistry.register( + CodeScanMessageTypes.ClearChat.type to IncomingCodeScanMessage.ClearChat::class, + CodeScanMessageTypes.Help.type to IncomingCodeScanMessage.Help::class, + CodeScanMessageTypes.TabCreated.type to IncomingCodeScanMessage.TabCreated::class, + CodeScanMessageTypes.TabRemoved.type to IncomingCodeScanMessage.TabRemoved::class, + CodeScanMessageTypes.Scan.type to IncomingCodeScanMessage.Scan::class, + CodeScanMessageTypes.StartProjectScan.type to IncomingCodeScanMessage.StartProjectScan::class, + CodeScanMessageTypes.StartFileScan.type to IncomingCodeScanMessage.StartFileScan::class, + CodeScanMessageTypes.StopProjectScan.type to IncomingCodeScanMessage.StopProjectScan::class, + CodeScanMessageTypes.StopFileScan.type to IncomingCodeScanMessage.StopFileScan::class, + CodeScanMessageTypes.ResponseBodyLinkClicked.type to IncomingCodeScanMessage.ResponseBodyLinkClicked::class, + CodeScanMessageTypes.OpenIssuesPanel.type to IncomingCodeScanMessage.OpenIssuesPanel::class + ) + + scope.launch { + merge(service().flow, context.messagesFromUiToApp.flow).collect { message -> + // Launch a new coroutine to handle each message + scope.launch { handleMessage(message, inboundAppMessagesHandler) } + } + } + + fun authChanged() { + val isAnotherThreadProcessing = !isProcessingAuthChanged.compareAndSet(false, true) + if (isAnotherThreadProcessing) return + scope.launch { + val authController = AuthController() + val credentialState = authController.getAuthNeededStates(context.project).amazonQ + if (credentialState == null) { + // Notify tabs about restoring authentication + context.messagesFromAppToUi.publish( + AuthenticationUpdateMessage( + featureDevEnabled = isFeatureDevAvailable(context.project), + codeTransformEnabled = isCodeTransformAvailable(context.project), + codeScanEnabled = isCodeScanAvailable(context.project), + codeTestEnabled = isCodeTestAvailable(context.project), + docEnabled = isDocAvailable(context.project), + authenticatingTabIDs = chatSessionStorage.getAuthenticatingSessions().map { it.tabId } + ) + ) + + chatSessionStorage.changeAuthenticationNeeded(false) + chatSessionStorage.changeAuthenticationNeededNotified(false) + } else { + chatSessionStorage.changeAuthenticationNeeded(true) + + // Ask for reauth + chatSessionStorage.getAuthenticatingSessions().filter { !it.authNeededNotified }.forEach { + context.messagesFromAppToUi.publish( + AuthenticationNeededExceptionMessage( + tabId = it.tabId, + authType = credentialState.authType, + message = credentialState.message + ) + ) + } + + // Prevent multiple calls to activeConnectionChanged + chatSessionStorage.changeAuthenticationNeededNotified(true) + } + isProcessingAuthChanged.set(false) + } + } + + ApplicationManager.getApplication().messageBus.connect(this).subscribe( + BearerTokenProviderListener.TOPIC, + object : BearerTokenProviderListener { + override fun onChange(providerId: String, newScopes: List?) { + val qProvider = getQTokenProvider(context.project) + val isQ = qProvider?.id == providerId + val isAuthorized = qProvider?.state() == BearerTokenAuthState.AUTHORIZED + if (!isQ || !isAuthorized) return + authChanged() + } + } + ) + + ApplicationManager.getApplication().messageBus.connect(this).subscribe( + ToolkitConnectionManagerListener.TOPIC, + object : ToolkitConnectionManagerListener { + override fun activeConnectionChanged(newConnection: ToolkitConnection?) { + authChanged() + } + } + ) + } + + private fun getQTokenProvider(project: Project) = ( + ToolkitConnectionManager + .getInstance(project) + .activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection + ) + ?.getConnectionSettings() + ?.tokenProvider + ?.delegate as? BearerTokenProvider + + private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) { + when (message) { + is IncomingCodeScanMessage.ClearChat -> inboundAppMessagesHandler.processClearQuickAction(message) + is IncomingCodeScanMessage.Help -> inboundAppMessagesHandler.processHelpQuickAction(message) + is IncomingCodeScanMessage.TabCreated -> inboundAppMessagesHandler.processTabCreated(message) + is IncomingCodeScanMessage.TabRemoved -> inboundAppMessagesHandler.processTabRemoved(message) + is IncomingCodeScanMessage.Scan -> inboundAppMessagesHandler.processScanQuickAction(message) + is IncomingCodeScanMessage.StartProjectScan -> inboundAppMessagesHandler.processStartProjectScan(message) + is IncomingCodeScanMessage.StartFileScan -> inboundAppMessagesHandler.processStartFileScan(message) + is CodeScanActionMessage -> inboundAppMessagesHandler.processCodeScanCommand(message) + is IncomingCodeScanMessage.StopProjectScan -> inboundAppMessagesHandler.processStopProjectScan(message) + is IncomingCodeScanMessage.StopFileScan -> inboundAppMessagesHandler.processStopFileScan(message) + is IncomingCodeScanMessage.ResponseBodyLinkClicked -> inboundAppMessagesHandler.processResponseBodyLinkClicked(message) + is IncomingCodeScanMessage.OpenIssuesPanel -> inboundAppMessagesHandler.processOpenIssuesPanel(message) + } + } + + override fun dispose() { + // nothing to do + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatAppFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatAppFactory.kt new file mode 100644 index 0000000000..2ac70b38b0 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatAppFactory.kt @@ -0,0 +1,12 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan + +import com.intellij.openapi.project.Project +import kotlinx.coroutines.CoroutineScope +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppFactory + +class CodeScanChatAppFactory(private val cs: CoroutineScope) : AmazonQAppFactory { + override fun createApp(project: Project) = CodeScanChatApp(cs) +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatItems.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatItems.kt new file mode 100644 index 0000000000..ee2dc09e3c --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatItems.kt @@ -0,0 +1,165 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan + +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.Button +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanButtonId +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessageContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.ProgressField +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.PromptProgressMessage +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.IssueSeverity +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticPrompt +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.StaticTextResponse +import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType +import software.aws.toolkits.resources.message +import java.util.UUID + +private val projectScanButton = Button( + id = CodeScanButtonId.StartProjectScan.id, + text = message("codescan.chat.message.button.projectScan") +) + +private val fileScanButton = Button( + id = CodeScanButtonId.StartFileScan.id, + text = message("codescan.chat.message.button.fileScan") +) + +private val openIssuesPanelButton = Button( + id = CodeScanButtonId.OpenIssuesPanel.id, + text = message("codescan.chat.message.button.openIssues"), + keepCardAfterClick = true +) + +fun buildStartNewScanChatContent() = CodeScanChatMessageContent( + type = ChatMessageType.Answer, + message = message("codescan.chat.new_scan.input.message"), + buttons = listOf( + fileScanButton, + projectScanButton + ), + canBeVoted = false +) + +// TODO: Replace StaticPrompt and StaticTextResponse message according to Fnf +fun buildHelpChatPromptContent() = CodeScanChatMessageContent( + type = ChatMessageType.Prompt, + message = StaticPrompt.Help.message, + canBeVoted = false +) + +fun buildHelpChatAnswerContent() = CodeScanChatMessageContent( + type = ChatMessageType.Answer, + message = StaticTextResponse.Help.message, + canBeVoted = false +) + +fun buildUserSelectionProjectScanChatContent() = CodeScanChatMessageContent( + type = ChatMessageType.Prompt, + message = message("codescan.chat.message.button.projectScan"), + canBeVoted = false +) + +fun buildUserSelectionFileScanChatContent() = CodeScanChatMessageContent( + type = ChatMessageType.Prompt, + message = message("codescan.chat.message.button.fileScan"), + canBeVoted = false +) + +fun buildNotInGitRepoChatContent() = CodeScanChatMessageContent( + type = ChatMessageType.Answer, + message = message("codescan.chat.message.not_git_repo"), + canBeVoted = false +) + +fun buildScanInProgressChatContent(currentStep: Int, isProject: Boolean) = CodeScanChatMessageContent( + type = ChatMessageType.AnswerPart, + message = buildString { + appendLine(if (isProject) message("codescan.chat.message.scan_begin_project") else message("codescan.chat.message.scan_begin_file")) + appendLine("") + appendLine(message("codescan.chat.message.scan_begin_wait_time")) + appendLine("") + appendLine("${getIconForStep(0, currentStep)} " + message("codescan.chat.message.scan_step_1")) + appendLine("${getIconForStep(1, currentStep)} " + message("codescan.chat.message.scan_step_2")) + appendLine("${getIconForStep(2, currentStep)} " + message("codescan.chat.message.scan_step_3")) + } +) + +val cancellingProgressField = ProgressField( + status = "warning", + text = message("general.canceling"), + value = -1, + actions = emptyList() +) + +fun buildScanCompleteChatContent(issues: List, isProject: Boolean = false): CodeScanChatMessageContent { + val issueCountMap = IssueSeverity.entries.associate { it.displayName to 0 }.toMutableMap() + val aggregatedIssues = issues.groupBy { it.severity } + aggregatedIssues.forEach { (key, list) -> if (list.isNotEmpty()) issueCountMap[key] = list.size } + + val message = buildString { + appendLine(if (isProject) message("codewhisperer.codescan.scan_complete_project") else message("codewhisperer.codescan.scan_complete_file")) + issueCountMap.entries.forEach { (severity, count) -> + appendLine(message("codewhisperer.codescan.scan_complete_count", severity, count)) + } + } + + return CodeScanChatMessageContent( + type = ChatMessageType.Answer, + message = message, + buttons = listOf( + openIssuesPanelButton + ), + ) +} + +fun buildPromptProgressMessage(tabId: String, isProject: Boolean = false, isCanceling: Boolean = false) = PromptProgressMessage( + progressField = when { + isCanceling -> cancellingProgressField + isProject -> projectScanProgressField + else -> fileScanProgressField + }, + tabId = tabId +) + +fun buildClearPromptProgressMessage(tabId: String) = PromptProgressMessage( + tabId = tabId +) + +val runCodeScanMessage + get() = CodeScanChatMessage(messageType = ChatMessageType.Prompt, command = "review", tabId = UUID.randomUUID().toString()) + +val cancelFileScanButton = Button( + id = CodeScanButtonId.StopFileScan.id, + text = message("general.cancel"), + icon = "cancel" +) + +val cancelProjectScanButton = cancelFileScanButton.copy( + id = CodeScanButtonId.StopProjectScan.id +) + +val fileScanProgressField = ProgressField( + status = "default", + text = message("codescan.chat.message.scan_file_in_progress"), + value = -1, + actions = listOf(cancelFileScanButton) +) + +val projectScanProgressField = fileScanProgressField.copy( + text = message("codescan.chat.message.scan_project_in_progress"), + actions = listOf(cancelProjectScanButton) +) + +fun buildProjectScanFailedChatContent(errorMessage: String?) = CodeScanChatMessageContent( + type = ChatMessageType.Answer, + message = errorMessage ?: message("codescan.chat.message.project_scan_failed") +) + +fun getIconForStep(targetStep: Int, currentStep: Int) = when { + currentStep == targetStep -> "☐" + currentStep > targetStep -> "☑" + else -> "☐" +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanConstants.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanConstants.kt new file mode 100644 index 0000000000..c27b7ef416 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanConstants.kt @@ -0,0 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan + +const val FEATURE_NAME = "Amazon Q Code Scan" diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/InboundAppMessagesHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/InboundAppMessagesHandler.kt new file mode 100644 index 0000000000..31ca09503d --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/InboundAppMessagesHandler.kt @@ -0,0 +1,33 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan + +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanActionMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.IncomingCodeScanMessage + +interface InboundAppMessagesHandler { + suspend fun processScanQuickAction(message: IncomingCodeScanMessage.Scan) + + suspend fun processStartProjectScan(message: IncomingCodeScanMessage.StartProjectScan) + + suspend fun processStartFileScan(message: IncomingCodeScanMessage.StartFileScan) + + suspend fun processStopProjectScan(message: IncomingCodeScanMessage.StopProjectScan) + + suspend fun processStopFileScan(message: IncomingCodeScanMessage.StopFileScan) + + suspend fun processTabCreated(message: IncomingCodeScanMessage.TabCreated) + + suspend fun processClearQuickAction(message: IncomingCodeScanMessage.ClearChat) + + suspend fun processHelpQuickAction(message: IncomingCodeScanMessage.Help) + + suspend fun processTabRemoved(message: IncomingCodeScanMessage.TabRemoved) + + suspend fun processCodeScanCommand(message: CodeScanActionMessage) + + suspend fun processResponseBodyLinkClicked(message: IncomingCodeScanMessage.ResponseBodyLinkClicked) + + suspend fun processOpenIssuesPanel(message: IncomingCodeScanMessage.OpenIssuesPanel) +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/auth/CodeScanAuthUtils.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/auth/CodeScanAuthUtils.kt new file mode 100644 index 0000000000..2e5744b176 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/auth/CodeScanAuthUtils.kt @@ -0,0 +1,11 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth + +import com.intellij.openapi.project.Project +import software.aws.toolkits.jetbrains.core.gettingstarted.editor.ActiveConnection +import software.aws.toolkits.jetbrains.core.gettingstarted.editor.BearerTokenFeatureSet +import software.aws.toolkits.jetbrains.core.gettingstarted.editor.checkBearerConnectionValidity + +fun isCodeScanAvailable(project: Project): Boolean = checkBearerConnectionValidity(project, BearerTokenFeatureSet.Q) is ActiveConnection.ValidBearer diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanActionMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanActionMessage.kt new file mode 100644 index 0000000000..01532c154f --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanActionMessage.kt @@ -0,0 +1,16 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands + +import com.intellij.openapi.project.Project +import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants + +data class CodeScanActionMessage( + val command: CodeScanCommand, + val project: Project, + val scanResult: CodeScanResponse? = null, + val scope: CodeWhispererConstants.CodeAnalysisScope, +) : AmazonQMessage diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanCommand.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanCommand.kt new file mode 100644 index 0000000000..f7b16705b1 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanCommand.kt @@ -0,0 +1,8 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands + +enum class CodeScanCommand { + ScanComplete, +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanMessageListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanMessageListener.kt new file mode 100644 index 0000000000..832741a1fc --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/commands/CodeScanMessageListener.kt @@ -0,0 +1,21 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands + +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants + +@Service +class CodeScanMessageListener { + private val _messages by lazy { MutableSharedFlow(extraBufferCapacity = 10) } + val flow = _messages.asSharedFlow() + + fun onScanResult(result: CodeScanResponse?, scope: CodeWhispererConstants.CodeAnalysisScope, project: Project) { + _messages.tryEmit(CodeScanActionMessage(CodeScanCommand.ScanComplete, scanResult = result, scope = scope, project = project)) + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatController.kt new file mode 100644 index 0000000000..59a69c7ce4 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatController.kt @@ -0,0 +1,197 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan.controller + +import com.intellij.ide.BrowserUtil +import software.aws.toolkits.core.utils.debug +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.FEATURE_NAME +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.InboundAppMessagesHandler +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildHelpChatAnswerContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildHelpChatPromptContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildNotInGitRepoChatContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildProjectScanFailedChatContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildScanCompleteChatContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildScanInProgressChatContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildStartNewScanChatContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildUserSelectionFileScanChatContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildUserSelectionProjectScanChatContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanActionMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanCommand +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.AuthenticationNeededExceptionMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.IncomingCodeScanMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse +import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager +import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants +import software.aws.toolkits.jetbrains.services.codewhisperer.util.getAuthType +import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType +import software.aws.toolkits.resources.message + +class CodeScanChatController( + private val context: AmazonQAppInitContext, + private val chatSessionStorage: ChatSessionStorage, + private val authController: AuthController = AuthController(), +) : InboundAppMessagesHandler { + + private val messenger = context.messagesFromAppToUi + private val codeScanManager = CodeWhispererCodeScanManager.getInstance(context.project) + private val codeScanChatHelper = CodeScanChatHelper(context.messagesFromAppToUi, chatSessionStorage) + private val scanInProgressMessageId = "scanProgressMessage" + + override suspend fun processScanQuickAction(message: IncomingCodeScanMessage.Scan) { + // TODO: telemetry + + if (!checkForAuth(message.tabId)) { + return + } + if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return + + codeScanChatHelper.setActiveCodeScanTabId(message.tabId) + codeScanChatHelper.addNewMessage(buildStartNewScanChatContent()) + codeScanChatHelper.sendChatInputEnabledMessage(false) + } + + override suspend fun processStartProjectScan(message: IncomingCodeScanMessage.StartProjectScan) { + if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return + codeScanChatHelper.addNewMessage(buildUserSelectionProjectScanChatContent()) + if (!codeScanManager.isInsideWorkTree()) { + codeScanChatHelper.addNewMessage(buildNotInGitRepoChatContent()) + } + codeScanChatHelper.addNewMessage(buildScanInProgressChatContent(currentStep = 1, isProject = true), messageIdOverride = scanInProgressMessageId) + codeScanManager.runCodeScan(CodeWhispererConstants.CodeAnalysisScope.PROJECT, initiatedByChat = true) + codeScanChatHelper.updateProgress(isProject = true, isCanceling = false) + } + + override suspend fun processStopProjectScan(message: IncomingCodeScanMessage.StopProjectScan) { + if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return + codeScanChatHelper.updateProgress(isProject = true, isCanceling = true) + codeScanManager.stopCodeScan(CodeWhispererConstants.CodeAnalysisScope.PROJECT) + } + + override suspend fun processStopFileScan(message: IncomingCodeScanMessage.StopFileScan) { + if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return + codeScanChatHelper.updateProgress(isProject = false, isCanceling = true) + codeScanManager.stopCodeScan(CodeWhispererConstants.CodeAnalysisScope.FILE) + } + + override suspend fun processStartFileScan(message: IncomingCodeScanMessage.StartFileScan) { + if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return + codeScanChatHelper.addNewMessage(buildUserSelectionFileScanChatContent()) + codeScanChatHelper.addNewMessage(buildScanInProgressChatContent(currentStep = 1, isProject = false), messageIdOverride = scanInProgressMessageId) + codeScanManager.runCodeScan(CodeWhispererConstants.CodeAnalysisScope.FILE, initiatedByChat = true) + codeScanChatHelper.updateProgress(isProject = false, isCanceling = false) + } + + override suspend fun processTabCreated(message: IncomingCodeScanMessage.TabCreated) { + logger.debug { "$FEATURE_NAME: New tab created: $message" } + codeScanChatHelper.setActiveCodeScanTabId(message.tabId) + CodeWhispererTelemetryService.getInstance().sendCodeScanNewTabEvent(getAuthType(context.project)) + } + + override suspend fun processClearQuickAction(message: IncomingCodeScanMessage.ClearChat) { + chatSessionStorage.deleteSession(message.tabId) + } + + override suspend fun processHelpQuickAction(message: IncomingCodeScanMessage.Help) { + if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return + codeScanChatHelper.addNewMessage(buildHelpChatPromptContent()) + codeScanChatHelper.addNewMessage(buildHelpChatAnswerContent()) + } + + override suspend fun processTabRemoved(message: IncomingCodeScanMessage.TabRemoved) { + chatSessionStorage.deleteSession(message.tabId) + } + + override suspend fun processResponseBodyLinkClicked(message: IncomingCodeScanMessage.ResponseBodyLinkClicked) { + BrowserUtil.browse(message.link) + } + + override suspend fun processCodeScanCommand(message: CodeScanActionMessage) { + if (message.project != context.project) return + val isProject = message.scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT + when (message.command) { + CodeScanCommand.ScanComplete -> { + codeScanChatHelper.addNewMessage( + buildScanInProgressChatContent(currentStep = 2, isProject = isProject), + messageIdOverride = scanInProgressMessageId + ) + val result = message.scanResult + if (result != null) { + handleCodeScanResult(result, message.scope) + } else { + codeScanChatHelper.addNewMessage(buildProjectScanFailedChatContent("Cancelled")) + codeScanChatHelper.clearProgress() + } + } + } + } + + private suspend fun handleCodeScanResult(result: CodeScanResponse, scope: CodeWhispererConstants.CodeAnalysisScope) { + val isProject = scope == CodeWhispererConstants.CodeAnalysisScope.PROJECT + when (result) { + is CodeScanResponse.Success -> { + codeScanChatHelper.addNewMessage( + buildScanInProgressChatContent(currentStep = 3, isProject = isProject), + messageIdOverride = scanInProgressMessageId + ) + codeScanChatHelper.addNewMessage(buildScanCompleteChatContent(result.issues, isProject = isProject)) + codeScanChatHelper.clearProgress() + } + is CodeScanResponse.Failure -> { + codeScanChatHelper.addNewMessage(buildScanInProgressChatContent(3, isProject = isProject), messageIdOverride = scanInProgressMessageId) + codeScanChatHelper.addNewMessage(buildProjectScanFailedChatContent(result.failureReason.message)) + codeScanChatHelper.clearProgress() + } + } + } + + /** + * Return true if authenticated, else show authentication message and return false + * // TODO: Refactor this to avoid code duplication with other controllers + */ + private suspend fun checkForAuth(tabId: String): Boolean { + try { + val session = chatSessionStorage.getSession(tabId) + logger.debug { "$FEATURE_NAME: Session created with id: ${session.tabId}" } + + val credentialState = authController.getAuthNeededStates(context.project).amazonQ + if (credentialState != null) { + messenger.publish( + AuthenticationNeededExceptionMessage( + tabId = session.tabId, + authType = credentialState.authType, + message = credentialState.message + ) + ) + session.isAuthenticating = true + return false + } + } catch (err: Exception) { + messenger.publish( + CodeScanChatMessage( + tabId = tabId, + messageType = ChatMessageType.Answer, + message = message("codescan.chat.message.error_request") + ) + ) + return false + } + + return true + } + + override suspend fun processOpenIssuesPanel(message: IncomingCodeScanMessage.OpenIssuesPanel) { + if (message.tabId != codeScanChatHelper.getActiveCodeScanTabId()) return + codeScanManager.showCodeScanUI() + } + + companion object { + private val logger = getLogger() + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt new file mode 100644 index 0000000000..6a9acbbfc0 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/controller/CodeScanChatHelper.kt @@ -0,0 +1,81 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan.controller + +import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildClearPromptProgressMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.buildPromptProgressMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.ChatInputEnabledMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.CodeScanChatMessageContent +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages.UpdatePlaceholderMessage +import software.aws.toolkits.jetbrains.services.amazonqCodeScan.storage.ChatSessionStorage +import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType +import java.util.UUID + +class CodeScanChatHelper( + private val messagePublisher: MessagePublisher, + private val chatSessionStorage: ChatSessionStorage, +) { + private var activeCodeScanTabId: String? = null + + fun setActiveCodeScanTabId(tabId: String) { + activeCodeScanTabId = tabId + } + + fun getActiveCodeScanTabId(): String? = activeCodeScanTabId + + private fun isInValidSession() = activeCodeScanTabId == null || chatSessionStorage.getSession(activeCodeScanTabId as String).isAuthenticating + + suspend fun addNewMessage( + content: CodeScanChatMessageContent, + messageIdOverride: String? = null, + clearPreviousItemButtons: Boolean? = false, + ) { + if (isInValidSession()) return + + messagePublisher.publish( + CodeScanChatMessage( + tabId = activeCodeScanTabId as String, + messageId = messageIdOverride ?: UUID.randomUUID().toString(), + messageType = content.type, + message = content.message, + buttons = content.buttons, + formItems = content.formItems, + followUps = content.followUps, + canBeVoted = content.canBeVoted, + isLoading = content.type == ChatMessageType.AnswerPart, + clearPreviousItemButtons = clearPreviousItemButtons as Boolean + ) + ) + } + + suspend fun updateProgress(isProject: Boolean = false, isCanceling: Boolean = false) { + if (isInValidSession()) return + messagePublisher.publish(buildPromptProgressMessage(activeCodeScanTabId as String, isProject, isCanceling)) + sendChatInputEnabledMessage(false) + } + + suspend fun clearProgress() { + if (isInValidSession()) return + messagePublisher.publish(buildClearPromptProgressMessage(activeCodeScanTabId as String)) + sendChatInputEnabledMessage(true) + } + + suspend fun sendChatInputEnabledMessage(isEnabled: Boolean) { + if (isInValidSession()) return + messagePublisher.publish(ChatInputEnabledMessage(activeCodeScanTabId as String, enabled = isEnabled)) + } + + suspend fun updatePlaceholder(newPlaceholder: String) { + if (isInValidSession()) return + + messagePublisher.publish( + UpdatePlaceholderMessage( + tabId = activeCodeScanTabId as String, + newPlaceholder = newPlaceholder + ) + ) + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/messages/CodeScanMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/messages/CodeScanMessage.kt new file mode 100644 index 0000000000..062e974336 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/messages/CodeScanMessage.kt @@ -0,0 +1,185 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonqCodeScan.messages + +import com.fasterxml.jackson.annotation.JsonProperty +import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthFollowUpType +import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage +import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType +import software.aws.toolkits.jetbrains.services.cwc.messages.FollowUp +import java.time.Instant +import java.util.UUID + +const val CODE_SCAN_TAB_NAME = "codescan" + +enum class CodeScanButtonId(val id: String) { + StartProjectScan("codescan_start_project_scan"), + StartFileScan("codescan_start_file_scan"), + StopProjectScan("codescan_stop_project_scan"), + StopFileScan("codescan_stop_file_scan"), + OpenIssuesPanel("codescan_open_issues"), +} + +data class Button( + val id: String, + val text: String, + val description: String? = null, + val icon: String? = null, + val keepCardAfterClick: Boolean? = false, + val disabled: Boolean? = false, + val waitMandatoryFormItems: Boolean? = false, +) +data class ProgressField( + val title: String? = null, + val value: Int? = null, + val status: String? = null, + val actions: List