diff --git a/package-lock.json b/package-lock.json index 2c9578a1ba8..e44dbae0fb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "@vscode/test-electron": "^2.3.8", "@vscode/test-web": "^0.0.65", "@vscode/vsce": "^2.19.0", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-aws-toolkits": "file:plugins/eslint-plugin-aws-toolkits", "eslint-plugin-header": "^3.1.1", @@ -12254,7 +12254,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "license": "MIT", "engines": { @@ -18195,15 +18197,18 @@ } }, "node_modules/eslint": { - "version": "8.56.0", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -26729,7 +26734,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.55.0-SNAPSHOT", + "version": "1.58.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -28605,7 +28610,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.54.0-SNAPSHOT", + "version": "3.55.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/package.json b/package.json index fcc58e487cd..3ac7ea42a96 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@vscode/test-electron": "^2.3.8", "@vscode/test-web": "^0.0.65", "@vscode/vsce": "^2.19.0", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-aws-toolkits": "file:plugins/eslint-plugin-aws-toolkits", "eslint-plugin-header": "^3.1.1", diff --git a/packages/amazonq/.changes/1.55.0.json b/packages/amazonq/.changes/1.55.0.json new file mode 100644 index 00000000000..194814fc3be --- /dev/null +++ b/packages/amazonq/.changes/1.55.0.json @@ -0,0 +1,38 @@ +{ + "date": "2025-04-09", + "version": "1.55.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q Chat: Update chat history icon" + }, + { + "type": "Bug Fix", + "description": "Amazon Q Chat: chat occasionally freezes and displays gray screen" + }, + { + "type": "Bug Fix", + "description": "Amazon Q Chat: Set owner-only permissions for chat history and saved prompt files" + }, + { + "type": "Feature", + "description": "`/test` generates tests in all languages, not only Java/Python" + }, + { + "type": "Feature", + "description": "Amazon Q chat: Click export icon to save chat transcript in Markdown or HTML" + }, + { + "type": "Feature", + "description": "SageMaker: Disable the unsupported agentic commands and welcome prompt" + }, + { + "type": "Feature", + "description": "Amazon Q Chat: Add `@code` context for PHP, Ruby, Scala, Shell, and Swift projects" + }, + { + "type": "Feature", + "description": "Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.56.0.json b/packages/amazonq/.changes/1.56.0.json new file mode 100644 index 00000000000..8de6eea2db5 --- /dev/null +++ b/packages/amazonq/.changes/1.56.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-04-09", + "version": "1.56.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Improve status message while loading Amazon Q Profiles during login" + }, + { + "type": "Bug Fix", + "description": "\"failed to run command\" error" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.57.0.json b/packages/amazonq/.changes/1.57.0.json new file mode 100644 index 00000000000..3a7a8d2ab95 --- /dev/null +++ b/packages/amazonq/.changes/1.57.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-04-10", + "version": "1.57.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Fix bug where generate fix does not work" + }, + { + "type": "Bug Fix", + "description": "Fix bug where review shows 0 findings" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Bug Fix-2daaf655-9a45-4104-94f9-06b9d8c703e2.json b/packages/amazonq/.changes/next-release/Bug Fix-2daaf655-9a45-4104-94f9-06b9d8c703e2.json deleted file mode 100644 index a1775ad8258..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-2daaf655-9a45-4104-94f9-06b9d8c703e2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q Chat: Update chat history icon" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json b/packages/amazonq/.changes/next-release/Bug Fix-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json deleted file mode 100644 index b39eceeec80..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q Chat: chat occasionally freezes and displays gray screen" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json b/packages/amazonq/.changes/next-release/Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json new file mode 100644 index 00000000000..e02212b84ca --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "inline chat activates properly when using 'aws.experiments.amazonqChatLSP' feature flag" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json b/packages/amazonq/.changes/next-release/Bug Fix-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json deleted file mode 100644 index a6a20c5bdf2..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q Chat: Set owner-only permissions for chat history and saved prompt files" -} diff --git a/packages/amazonq/.changes/next-release/Feature-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json b/packages/amazonq/.changes/next-release/Feature-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json deleted file mode 100644 index f8286079401..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "`/test` generates tests in all languages, not only Java/Python" -} diff --git a/packages/amazonq/.changes/next-release/Feature-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json b/packages/amazonq/.changes/next-release/Feature-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json deleted file mode 100644 index 14c7484e001..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Amazon Q chat: Click export icon to save chat transcript in Markdown or HTML" -} diff --git a/packages/amazonq/.changes/next-release/Feature-80edcce3-4e32-4e74-a35a-af2242782181.json b/packages/amazonq/.changes/next-release/Feature-80edcce3-4e32-4e74-a35a-af2242782181.json deleted file mode 100644 index cd6456594e4..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-80edcce3-4e32-4e74-a35a-af2242782181.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "SageMaker: Disable the unsupported agentic commands and welcome prompt" -} diff --git a/packages/amazonq/.changes/next-release/Feature-846afd38-d618-4bd1-a3aa-7c74597502f1.json b/packages/amazonq/.changes/next-release/Feature-846afd38-d618-4bd1-a3aa-7c74597502f1.json deleted file mode 100644 index 19fa59c69e9..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-846afd38-d618-4bd1-a3aa-7c74597502f1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Amazon Q Chat: Add `@code` context for PHP, Ruby, Scala, Shell, and Swift projects" -} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 2d5fa9eb411..ac52aa0e297 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.57.0 2025-04-10 + +- **Bug Fix** Fix bug where generate fix does not work +- **Bug Fix** Fix bug where review shows 0 findings + +## 1.56.0 2025-04-09 + +- **Bug Fix** Improve status message while loading Amazon Q Profiles during login +- **Bug Fix** "failed to run command" error + +## 1.55.0 2025-04-09 + +- **Bug Fix** Amazon Q Chat: Update chat history icon +- **Bug Fix** Amazon Q Chat: chat occasionally freezes and displays gray screen +- **Bug Fix** Amazon Q Chat: Set owner-only permissions for chat history and saved prompt files +- **Feature** `/test` generates tests in all languages, not only Java/Python +- **Feature** Amazon Q chat: Click export icon to save chat transcript in Markdown or HTML +- **Feature** SageMaker: Disable the unsupported agentic commands and welcome prompt +- **Feature** Amazon Q Chat: Add `@code` context for PHP, Ruby, Scala, Shell, and Swift projects +- **Feature** Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions + ## 1.54.0 2025-04-03 - **Bug Fix** Amazon Q chat: `@prompts` not added to context diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 9d90694d93f..ec26ba1b56c 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.55.0-SNAPSHOT", + "version": "1.58.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -361,6 +361,11 @@ "when": "view =~ /^aws\\.amazonq/", "group": "1_amazonQ@1" }, + { + "command": "aws.amazonq.selectRegionProfile", + "when": "view == aws.amazonq.AmazonQChatView && aws.amazonq.connectedSsoIdc == true", + "group": "1_amazonQ@1" + }, { "command": "aws.amazonq.signout", "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected && !aws.isSageMakerUnifiedStudio", @@ -573,6 +578,12 @@ "category": "%AWS.amazonq.title%", "enablement": "aws.codewhisperer.connected" }, + { + "command": "aws.amazonq.selectRegionProfile", + "title": "Change Profile", + "category": "%AWS.amazonq.title%", + "enablement": "aws.codewhisperer.connected" + }, { "command": "aws.amazonq.transformationHub.reviewChanges.acceptChanges", "title": "%AWS.command.q.transform.acceptChanges%" diff --git a/packages/amazonq/src/app/amazonqScan/app.ts b/packages/amazonq/src/app/amazonqScan/app.ts index 0465048da77..21857163bd2 100644 --- a/packages/amazonq/src/app/amazonqScan/app.ts +++ b/packages/amazonq/src/app/amazonqScan/app.ts @@ -70,6 +70,9 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) Commands.register('aws.amazonq.security.scan-statusbar', async () => { if (AuthUtil.instance.isConnectionExpired()) { diff --git a/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts b/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts index 7583902dacb..72af0a200c5 100644 --- a/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts +++ b/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts @@ -108,6 +108,10 @@ export class ScanController { interactionType: data.vote, }) }) + + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + this.sessionStorage.removeActiveTab() + }) } private async tabOpened(message: any) { diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts index 49205c75c7d..10f827814aa 100644 --- a/packages/amazonq/src/app/chat/activation.ts +++ b/packages/amazonq/src/app/chat/activation.ts @@ -10,7 +10,6 @@ import { AuthUtil, CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' import { Commands, placeholder, funcUtil } from 'aws-core-vscode/shared' import * as amazonq from 'aws-core-vscode/amazonq' import { scanChatAppInit } from '../amazonqScan' -import { init as inlineChatInit } from '../../inlineChat/app' export async function activate(context: ExtensionContext) { const appInitContext = amazonq.DefaultAmazonQAppInitContext.instance @@ -72,7 +71,6 @@ function registerApps(appInitContext: amazonq.AmazonQAppInitContext, context: Ex amazonq.testChatAppInit(appInitContext) scanChatAppInit(appInitContext) amazonq.docChatAppInit(appInitContext) - inlineChatInit(context) } /** diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts index d9d36f828eb..945537b38ee 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -25,6 +25,7 @@ import { DevOptions } from 'aws-core-vscode/dev' import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth' import api from './api' import { activate as activateCWChat } from './app/chat/activation' +import { activate as activateInlineChat } from './inlineChat/activation' import { beta } from 'aws-core-vscode/dev' import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications' import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' @@ -55,6 +56,7 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { await activateCWChat(context) await activateQGumby(extContext as ExtContext) } + activateInlineChat(context) const authProvider = new CommonAuthViewProvider( context, diff --git a/packages/amazonq/src/inlineChat/app.ts b/packages/amazonq/src/inlineChat/activation.ts similarity index 54% rename from packages/amazonq/src/inlineChat/app.ts rename to packages/amazonq/src/inlineChat/activation.ts index f783ef8d84f..a42dfdb3e02 100644 --- a/packages/amazonq/src/inlineChat/app.ts +++ b/packages/amazonq/src/inlineChat/activation.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { InlineChatController } from '../inlineChat/controller/inlineChatController' -import { registerInlineCommands } from '../inlineChat/command/registerInlineCommands' +import { InlineChatController } from './controller/inlineChatController' +import { registerInlineCommands } from './command/registerInlineCommands' -export function init(context: vscode.ExtensionContext) { +export function activate(context: vscode.ExtensionContext) { const inlineChatController = new InlineChatController(context) registerInlineCommands(context, inlineChatController) } diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index 53c77349900..15314f94655 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -67,6 +67,7 @@ export class InlineChatProvider { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromPromptChatMessage(message), customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, context: [], relevantTextDocuments: [], additionalContents: [], diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts index 3febc748442..dd495d1bfbf 100644 --- a/packages/amazonq/src/lsp/chat/commands.ts +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as vscode from 'vscode' import { Commands, globals } from 'aws-core-vscode/shared' import { window } from 'vscode' import { AmazonQChatViewProvider } from './webviewProvider' @@ -74,6 +73,6 @@ function registerGenericCommand(commandName: string, genericCommand: string, pro * Instead, we just create our own as a temporary solution */ async function focusAmazonQPanel() { - await vscode.commands.executeCommand('aws.amazonq.AmazonQChatView.focus') - await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus') + await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus') + await Commands.tryExecute('aws.amazonq.AmazonCommonAuth.focus') } diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index e7943dd5418..444555e9950 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -23,12 +23,12 @@ import { insertToCursorPositionNotificationType, } from '@aws/language-server-runtimes/protocol' import { v4 as uuidv4 } from 'uuid' -import { window } from 'vscode' +import * as vscode from 'vscode' import { Disposable, LanguageClient, Position, State, TextDocumentIdentifier } from 'vscode-languageclient' import * as jose from 'jose' import { AmazonQChatViewProvider } from './webviewProvider' import { AuthUtil } from 'aws-core-vscode/codewhisperer' -import { AmazonQPromptSettings } from 'aws-core-vscode/shared' +import { AmazonQPromptSettings, messages } from 'aws-core-vscode/shared' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { languageClient.onDidChangeState(({ oldState, newState }) => { @@ -62,11 +62,15 @@ export function registerMessageListeners( switch (message.command) { case COPY_TO_CLIPBOARD: - // TODO see what we need to hook this up languageClient.info('[VSCode Client] Copy to clipboard event received') + try { + await messages.copyToClipboard(message.params.code) + } catch (e) { + languageClient.error(`[VSCode Client] Failed to copy to clipboard: ${(e as Error).message}`) + } break case INSERT_TO_CURSOR_POSITION: { - const editor = window.activeTextEditor + const editor = vscode.window.activeTextEditor let textDocument: TextDocumentIdentifier | undefined = undefined let cursorPosition: Position | undefined = undefined if (editor) { @@ -119,8 +123,8 @@ export function registerMessageListeners( ) const editor = - window.activeTextEditor || - window.visibleTextEditors.find((editor) => editor.document.languageId !== 'Log') + vscode.window.activeTextEditor || + vscode.window.visibleTextEditors.find((editor) => editor.document.languageId !== 'Log') if (editor) { message.params.cursorPosition = [editor.selection.active] message.params.textDocument = { uri: editor.document.uri.toString() } diff --git a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts index 06459cd8932..e6f51a1db34 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts @@ -9,6 +9,7 @@ import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { registerMessageListeners } from '../../../../../src/lsp/chat/messages' import { AmazonQChatViewProvider } from '../../../../../src/lsp/chat/webviewProvider' import { secondaryAuth, authConnection, AuthFollowUpType } from 'aws-core-vscode/amazonq' +import { messages } from 'aws-core-vscode/shared' describe('registerMessageListeners', () => { let languageClient: LanguageClient @@ -126,4 +127,35 @@ describe('registerMessageListeners', () => { }) }) }) + + describe('COPY_TO_CLIPBOARD', () => { + let copyToClipboardStub: sinon.SinonStub + const testCode = 'test' + const copyMessage = { + command: 'copyToClipboard', + params: { + code: testCode, + }, + } + + beforeEach(() => { + copyToClipboardStub = sandbox.stub().resolves() + sandbox.stub(messages, 'copyToClipboard').get(() => copyToClipboardStub) + }) + + it('successfully copies code to clipboard', async () => { + await messageHandler(copyMessage) + + sinon.assert.calledWith(copyToClipboardStub, testCode) + }) + + it('handles clipboard copy failure', async () => { + const errorMessage = 'Failed to copy' + copyToClipboardStub.rejects(new Error(errorMessage)) + + await messageHandler(copyMessage) + + sinon.assert.calledWith(errorStub, `[VSCode Client] Failed to copy to clipboard: ${errorMessage}`) + }) + }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts new file mode 100644 index 00000000000..af79f7dc2e5 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts @@ -0,0 +1,249 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import assert, { fail } from 'assert' +import { AuthUtil, RegionProfile, RegionProfileManager, defaultServiceConfig } from 'aws-core-vscode/codewhisperer' +import { globals } from 'aws-core-vscode/shared' +import { createTestAuth } from 'aws-core-vscode/test' +import { SsoConnection } from 'aws-core-vscode/auth' + +const enterpriseSsoStartUrl = 'https://enterprise.awsapps.com/start' + +describe('RegionProfileManager', function () { + let sut: RegionProfileManager + let auth: ReturnType + let authUtil: AuthUtil + + const profileFoo: RegionProfile = { + name: 'foo', + region: 'us-east-1', + arn: 'foo arn', + description: 'foo description', + } + + async function setupConnection(type: 'builderId' | 'idc') { + if (type === 'builderId') { + await authUtil.connectToAwsBuilderId() + const conn = authUtil.conn + assert.strictEqual(conn?.type, 'sso') + assert.strictEqual(conn.label, 'AWS Builder ID') + } else if (type === 'idc') { + await authUtil.connectToEnterpriseSso(enterpriseSsoStartUrl, 'us-east-1') + const conn = authUtil.conn + assert.strictEqual(conn?.type, 'sso') + assert.strictEqual(conn.label, 'IAM Identity Center (enterprise)') + } + } + + beforeEach(function () { + auth = createTestAuth(globals.globalState) + authUtil = new AuthUtil(auth) + sut = new RegionProfileManager(() => authUtil.conn) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('list profiles', function () { + it('should call list profiles with different region endpoints', async function () { + await setupConnection('idc') + const listProfilesStub = sinon.stub().returns({ + promise: () => + Promise.resolve({ + profiles: [ + { + arn: 'arn', + profileName: 'foo', + }, + ], + }), + }) + const mockClient = { + listAvailableProfiles: listProfilesStub, + } + const createClientStub = sinon.stub(sut, 'createQClient').resolves(mockClient) + + const r = await sut.listRegionProfile() + + assert.strictEqual(r.length, 2) + assert.deepStrictEqual(r, [ + { + name: 'foo', + arn: 'arn', + region: 'us-east-1', + description: '', + }, + { + name: 'foo', + arn: 'arn', + region: 'eu-central-1', + description: '', + }, + ]) + + assert.ok(createClientStub.calledTwice) + assert.ok(listProfilesStub.calledTwice) + }) + }) + + describe('switch and get profile', function () { + it('should switch if connection is IdC', async function () { + await setupConnection('idc') + await sut.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) + }) + + it('should do nothing and return undefined if connection is builder id', async function () { + await setupConnection('builderId') + await sut.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(sut.activeRegionProfile, undefined) + }) + }) + + describe(`client config`, function () { + it(`no valid credential should throw`, async function () { + assert.ok(authUtil.conn === undefined) + + assert.throws(() => { + sut.clientConfig + }, /trying to get client configuration without credential/) + }) + + it(`builder id should always use default profile IAD`, async function () { + await setupConnection('builderId') + await sut.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(sut.activeRegionProfile, undefined) + const conn = authUtil.conn + if (!conn) { + fail('connection should not be undefined') + } + + assert.deepStrictEqual(sut.clientConfig, defaultServiceConfig) + }) + + it(`idc should return correct endpoint corresponding to profile region`, async function () { + await setupConnection('idc') + await sut.switchRegionProfile( + { + name: 'foo', + region: 'eu-central-1', + arn: 'foo arn', + description: 'foo description', + }, + 'user' + ) + assert.ok(sut.activeRegionProfile) + assert.deepStrictEqual(sut.clientConfig, { + region: 'eu-central-1', + endpoint: 'https://q.eu-central-1.amazonaws.com/', + }) + }) + + it(`idc should throw if corresponding endpoint is not defined`, async function () { + await setupConnection('idc') + await sut.switchRegionProfile( + { + name: 'foo', + region: 'unknown region', + arn: 'foo arn', + description: 'foo description', + }, + 'user' + ) + + assert.throws(() => { + sut.clientConfig + }, /Q client configuration error, endpoint not found for region*/) + }) + }) + + describe('persistence', function () { + it('persistSelectedRegionProfile', async function () { + await setupConnection('idc') + await sut.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) + const conn = authUtil.conn + if (!conn) { + fail('connection should not be undefined') + } + + await sut.persistSelectRegionProfile() + + const state = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( + 'aws.amazonq.regionProfiles', + Object, + {} + ) + + assert.strictEqual(state[conn.id], profileFoo) + }) + + it(`restoreRegionProfile`, async function () { + sinon.stub(sut, 'listRegionProfile').resolves([profileFoo]) + await setupConnection('idc') + const conn = authUtil.conn + if (!conn) { + fail('connection should not be undefined') + } + + const state = {} as any + state[conn.id] = profileFoo + + await globals.globalState.update('aws.amazonq.regionProfiles', state) + + await sut.restoreRegionProfile(conn) + + assert.strictEqual(sut.activeRegionProfile, profileFoo) + }) + }) + + describe('invalidate', function () { + it('should reset activeProfile and global state', async function () { + // setup + await setupConnection('idc') + await sut.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) + const conn = authUtil.conn + if (!conn) { + fail('connection should not be undefined') + } + await sut.persistSelectRegionProfile() + const state = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( + 'aws.amazonq.regionProfiles', + Object, + {} + ) + assert.strictEqual(state[conn.id], profileFoo) + + // subject to test + await sut.invalidateProfile(profileFoo.arn) + + // assertion + assert.strictEqual(sut.activeRegionProfile, undefined) + const actualGlobalState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( + 'aws.amazonq.regionProfiles', + Object, + {} + ) + assert.deepStrictEqual(actualGlobalState, {}) + }) + }) + + describe('createQClient', function () { + it(`should configure the endpoint and region correspondingly`, async function () { + await setupConnection('idc') + await sut.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) + const conn = authUtil.conn as SsoConnection + + const client = await sut.createQClient('eu-central-1', 'https://amazon.com/', conn) + + assert.deepStrictEqual(client.config.region, 'eu-central-1') + assert.deepStrictEqual(client.endpoint.href, 'https://amazon.com/') + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts index 74a2c97dd75..500eaf23080 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts @@ -384,7 +384,7 @@ describe('getChatAuthState()', function () { const result = await authUtil.getChatAuthState() assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.connected, + codewhispererCore: AuthStates.pendingProfileSelection, codewhispererChat: AuthStates.expired, amazonQ: AuthStates.expired, }) @@ -399,9 +399,9 @@ describe('getChatAuthState()', function () { const result = await authUtil.getChatAuthState() assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.connected, - codewhispererChat: AuthStates.connected, - amazonQ: AuthStates.connected, + codewhispererCore: AuthStates.pendingProfileSelection, + codewhispererChat: AuthStates.pendingProfileSelection, + amazonQ: AuthStates.pendingProfileSelection, }) }) diff --git a/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts b/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts index ea0c7889426..6bcbd99bc2e 100644 --- a/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts +++ b/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts @@ -40,6 +40,7 @@ describe('triggerPayloadToChatRequest', () => { userInputContextLength: 0, focusFileContextLength: 0, }, + profile: undefined, context: [], documentReferences: [], query: undefined, diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 50f8f3ffe28..dd3bca4df0b 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -265,6 +265,7 @@ "AWS.command.codewhisperer.signout": "Sign Out", "AWS.command.codewhisperer.reconnect": "Reconnect", "AWS.command.codewhisperer.openReferencePanel": "Open Code Reference Log", + "AWS.command.q.selectRegionProfile": "Select Profile", "AWS.command.q.transform.acceptChanges": "Accept", "AWS.command.q.transform.rejectChanges": "Reject", "AWS.command.q.transform.stopJobInHub": "Stop job", diff --git a/packages/core/src/amazonq/commons/baseChatStorage.ts b/packages/core/src/amazonq/commons/baseChatStorage.ts index b0a10c8977b..ff748834576 100644 --- a/packages/core/src/amazonq/commons/baseChatStorage.ts +++ b/packages/core/src/amazonq/commons/baseChatStorage.ts @@ -35,4 +35,8 @@ export abstract class BaseChatSessionStorage= 2 + // 2. not default (fallback) which has empty arn + let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile + if (AuthUtil.instance.regionProfileManager.profiles.length === 1) { + regionProfile = undefined + } + + const regionProfileString: string = JSON.stringify(regionProfile) + const authState = (await AuthUtil.instance.getChatAuthState()).amazonQ + return ` ${cssLinks} ` diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 3486b1841a1..ab175c1da6f 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -125,6 +125,7 @@ export class Connector extends BaseConnector { padding: messageData.padding ?? undefined, fullWidth: messageData.fullWidth ?? undefined, codeBlockActions: messageData.codeBlockActions ?? undefined, + rootFolderTitle: messageData.rootFolderTitle ?? undefined, } if (messageData.relatedSuggestions !== undefined) { @@ -178,6 +179,33 @@ export class Connector extends BaseConnector { } } + private processToolMessage = async (messageData: any): Promise => { + if (this.onChatAnswerUpdated === undefined) { + return + } + const answer: CWCChatItem = { + type: messageData.messageType, + messageId: messageData.messageID ?? messageData.triggerID, + body: messageData.message, + followUp: messageData.followUps, + canBeVoted: messageData.canBeVoted ?? false, + codeReference: messageData.codeReference, + userIntent: messageData.contextList, + codeBlockLanguage: messageData.codeBlockLanguage, + contextList: messageData.contextList, + title: messageData.title, + buttons: messageData.buttons, + fileList: messageData.fileList, + header: messageData.header ?? undefined, + padding: messageData.padding ?? undefined, + fullWidth: messageData.fullWidth ?? undefined, + codeBlockActions: messageData.codeBlockActions ?? undefined, + rootFolderTitle: messageData.rootFolderTitle, + } + this.onChatAnswerUpdated(messageData.tabID, answer) + return + } + private storeChatItem(tabId: string, messageId: string, item: ChatItem): void { if (!this.chatItems.has(tabId)) { this.chatItems.set(tabId, new Map()) @@ -237,6 +265,11 @@ export class Connector extends BaseConnector { return } + if (messageData.type === 'toolMessage') { + await this.processToolMessage(messageData) + return + } + if (messageData.type === 'editorContextCommandMessage') { await this.processEditorContextCommandMessage(messageData) return @@ -257,14 +290,15 @@ export class Connector extends BaseConnector { } if (messageData.type === 'asyncEventProgressMessage') { - const enableStopAction = true + const enableStopAction = false + const isPromptInputDisabled = true this.onAsyncEventProgress( messageData.tabID, messageData.inProgress, messageData.message ?? undefined, messageData.messageId ?? undefined, enableStopAction, - false + isPromptInputDisabled ) return } @@ -361,7 +395,6 @@ export class Connector extends BaseConnector { break case 'run-shell-command': answer.header = { - icon: 'code-block' as MynahIconsType, body: 'shell', status: { icon: 'ok' as MynahIconsType, @@ -372,7 +405,6 @@ export class Connector extends BaseConnector { break case 'reject-shell-command': answer.header = { - icon: 'code-block' as MynahIconsType, body: 'shell', status: { icon: 'cancel' as MynahIconsType, diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 6bfd8f7c3f5..04a740624f1 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -66,6 +66,7 @@ export interface CWCChatItem extends ChatItem { codeBlockLanguage?: string contextList?: Context[] title?: string + rootFolderTitle?: string } export interface Context { @@ -77,7 +78,7 @@ export interface ConnectorProps { sendMessageToExtension: (message: ExtensionMessage) => void onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void onRunTestMessageReceived?: (tabID: string, showRunTestMessage: boolean) => void - onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void + onChatAnswerUpdated?: (tabID: string, message: CWCChatItem) => void onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => void onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined) => void diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 6bdb364e96a..3204ebe65b7 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -35,6 +35,7 @@ import { agentWalkthroughDataModel } from './walkthrough/agent' import { createClickTelemetry, createOpenAgentTelemetry } from './telemetry/actions' import { disclaimerAcknowledgeButtonId, disclaimerCard } from './texts/disclaimer' import { DetailedListSheetProps } from '@aws/mynah-ui/dist/components/detailed-list/detailed-list-sheet' +import { RegionProfile } from '../../../codewhisperer/models/model' /** * The number of welcome chat tabs that can be opened before the NEXT one will become @@ -48,6 +49,7 @@ export const createMynahUI = ( featureConfigsSerialized: [string, FeatureContext][], welcomeCount: number, disclaimerAcknowledged: boolean, + regionProfile: RegionProfile | undefined, disabledCommands?: string[], isSMUS?: boolean, isSM?: boolean @@ -98,6 +100,41 @@ export const createMynahUI = ( welcomeCount += 1 } + /** + * Creates a file list header from context list + * @param contextList List of file contexts + * @param rootFolderTitle Title for the root folder + * @returns Header object with file list + */ + const createFileListHeader = (contextList: any[], rootFolderTitle?: string) => { + return { + fileList: { + fileTreeTitle: '', + filePaths: contextList.map((file) => file.relativeFilePath), + rootFolderTitle: rootFolderTitle, + flatList: true, + collapsed: true, + hideFileCount: true, + details: Object.fromEntries( + contextList.map((file) => [ + file.relativeFilePath, + { + label: file.lineRanges + .map((range: { first: number; second: number }) => + range.first === -1 || range.second === -1 + ? '' + : `line ${range.first} - ${range.second}` + ) + .join(', '), + description: file.relativeFilePath, + clickable: true, + }, + ]) + ), + }, + } + } + // Adding the first tab as CWC tab tabsStorage.addTab({ id: 'tab-1', @@ -128,6 +165,7 @@ export const createMynahUI = ( isDocEnabled, disabledCommands, commandHighlight: highlightCommand, + regionProfile, }) // eslint-disable-next-line prefer-const @@ -212,6 +250,7 @@ export const createMynahUI = ( isDocEnabled, disabledCommands, commandHighlight: highlightCommand, + regionProfile, }) featureConfigs = tryNewMap(featureConfigsSerialized) @@ -342,8 +381,11 @@ export const createMynahUI = ( sendMessageToExtension: (message) => { ideApi.postMessage(message) }, - onChatAnswerUpdated: (tabID: string, item: ChatItem) => { + onChatAnswerUpdated: (tabID: string, item: CWCChatItem) => { if (item.messageId !== undefined) { + if (item.contextList !== undefined && item.contextList.length > 0) { + item.header = createFileListHeader(item.contextList, item.rootFolderTitle) + } mynahUI.updateChatAnswerWithMessageId(tabID, item.messageId, { ...(item.body !== undefined ? { body: item.body } : {}), ...(item.buttons !== undefined ? { buttons: item.buttons } : {}), @@ -405,32 +447,7 @@ export const createMynahUI = ( } if (item.contextList !== undefined && item.contextList.length > 0) { - item.header = { - fileList: { - fileTreeTitle: '', - filePaths: item.contextList.map((file) => file.relativeFilePath), - rootFolderTitle: item.title, - flatList: true, - collapsed: true, - hideFileCount: true, - details: Object.fromEntries( - item.contextList.map((file) => [ - file.relativeFilePath, - { - label: file.lineRanges - .map((range) => - range.first === -1 || range.second === -1 - ? '' - : `line ${range.first} - ${range.second}` - ) - .join(', '), - description: file.relativeFilePath, - clickable: true, - }, - ]) - ), - }, - } + item.header = createFileListHeader(item.contextList, item.rootFolderTitle) } if ( diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts index 15f00e9baeb..a551b76bcd7 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemType, MynahUIDataModel, QuickActionCommandGroup } from '@aws/mynah-ui' +import { ChatItem, ChatItemType, MynahUIDataModel, QuickActionCommandGroup } from '@aws/mynah-ui' import { TabType } from '../storages/tabsStorage' import { FollowUpGenerator } from '../followUps/generator' import { QuickActionGenerator } from '../quickActions/generator' import { qChatIntroMessageForSMUS, TabTypeDataMap } from './constants' import { agentWalkthroughDataModel } from '../walkthrough/agent' import { FeatureContext } from '../../../../shared/featureConfig' +import { RegionProfile } from '../../../../codewhisperer/models/model' export interface TabDataGeneratorProps { isFeatureDevEnabled: boolean @@ -19,12 +20,14 @@ export interface TabDataGeneratorProps { isDocEnabled: boolean disabledCommands?: string[] commandHighlight?: FeatureContext + regionProfile?: RegionProfile } export class TabDataGenerator { private followUpsGenerator: FollowUpGenerator public quickActionsGenerator: QuickActionGenerator private highlightCommand?: FeatureContext + private regionProfile?: RegionProfile constructor(props: TabDataGeneratorProps) { this.followUpsGenerator = new FollowUpGenerator() @@ -37,6 +40,7 @@ export class TabDataGenerator { disableCommands: props.disabledCommands, }) this.highlightCommand = props.commandHighlight + this.regionProfile = props.regionProfile } public getTabData( @@ -53,6 +57,16 @@ export class TabDataGenerator { return {} } + const regionProfileCard: ChatItem | undefined = + this.regionProfile === undefined + ? undefined + : { + type: ChatItemType.ANSWER, + body: `You are using the ${this.regionProfile?.name} profile for this chat period`, + status: 'info', + messageId: 'regionProfile', + } + const tabData: MynahUIDataModel = { tabTitle: taskName ?? TabTypeDataMap[tabType].title, promptInputInfo: @@ -62,6 +76,7 @@ export class TabDataGenerator { contextCommands: this.getContextCommands(tabType), chatItems: needWelcomeMessages ? [ + ...(regionProfileCard ? [regionProfileCard] : []), { type: ChatItemType.ANSWER, body: isSMUS ? qChatIntroMessageForSMUS : TabTypeDataMap[tabType].welcome, @@ -71,14 +86,15 @@ export class TabDataGenerator { followUp: this.followUpsGenerator.generateWelcomeBlockForTab(tabType), }, ] - : [], + : [...(regionProfileCard ? [regionProfileCard] : [])], promptInputOptions: tabType === 'cwc' ? [ { type: 'toggle', id: 'prompt-type', - value: 'ask', + value: 'pair-programming-on', + tooltip: 'Pair programmar on', options: [ { value: 'pair-programming-on', diff --git a/packages/core/src/amazonq/webview/webView.ts b/packages/core/src/amazonq/webview/webView.ts index a5ba7904087..70683e65bed 100644 --- a/packages/core/src/amazonq/webview/webView.ts +++ b/packages/core/src/amazonq/webview/webView.ts @@ -20,6 +20,7 @@ import { MessageListener } from '../messages/messageListener' import { MessagePublisher } from '../messages/messagePublisher' import { TabType } from './ui/storages/tabsStorage' import { amazonqMark } from '../../shared/performance/marks' +import { AuthUtil } from '../../codewhisperer/util/authUtil' export class AmazonQChatViewProvider implements WebviewViewProvider { public static readonly viewType = 'aws.amazonq.AmazonQChatView' @@ -35,6 +36,15 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { ) { registerAssetsHttpsFileSystem(extensionContext) this.webViewContentGenerator = new WebViewContentGenerator() + + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => { + if (this.webView) { + this.webView.html = await this.webViewContentGenerator.generate( + this.extensionContext.extensionUri, + this.webView + ) + } + }) } public async resolveWebviewView( @@ -63,6 +73,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { webviewView.webview ) + this.webView = webviewView.webview performance.mark(amazonqMark.open) } } diff --git a/packages/core/src/amazonqDoc/app.ts b/packages/core/src/amazonqDoc/app.ts index bf64c71c387..929cf1d45de 100644 --- a/packages/core/src/amazonqDoc/app.ts +++ b/packages/core/src/amazonqDoc/app.ts @@ -100,4 +100,7 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) } diff --git a/packages/core/src/amazonqDoc/controllers/chat/controller.ts b/packages/core/src/amazonqDoc/controllers/chat/controller.ts index 7de2b292cf7..ab6045e75ce 100644 --- a/packages/core/src/amazonqDoc/controllers/chat/controller.ts +++ b/packages/core/src/amazonqDoc/controllers/chat/controller.ts @@ -114,6 +114,9 @@ export class DocController { this.chatControllerMessageListeners.openDiff.event(async (data) => { return await this.openDiff(data) }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + this.sessionStorage.deleteAllSessions() + }) } /** Prompts user to choose a folder in current workspace for README creation/update. diff --git a/packages/core/src/amazonqFeatureDev/app.ts b/packages/core/src/amazonqFeatureDev/app.ts index 0af05cf0530..a016d2ba481 100644 --- a/packages/core/src/amazonqFeatureDev/app.ts +++ b/packages/core/src/amazonqFeatureDev/app.ts @@ -106,4 +106,7 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) } diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json index 43537b6df2d..812bbd4fd69 100644 --- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json +++ b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json @@ -110,6 +110,64 @@ ], "idempotent": true }, + "CreateUserMemoryEntry": { + "name": "CreateUserMemoryEntry", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "CreateUserMemoryEntryInput" + }, + "output": { + "shape": "CreateUserMemoryEntryOutput" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ], + "idempotent": true + }, + "CreateWorkspace": { + "name": "CreateWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "CreateWorkspaceRequest" + }, + "output": { + "shape": "CreateWorkspaceResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "ConflictException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "DeleteTaskAssistConversation": { "name": "DeleteTaskAssistConversation", "http": { @@ -140,6 +198,64 @@ } ] }, + "DeleteUserMemoryEntry": { + "name": "DeleteUserMemoryEntry", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "DeleteUserMemoryEntryInput" + }, + "output": { + "shape": "DeleteUserMemoryEntryOutput" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "ResourceNotFoundException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ], + "idempotent": true + }, + "DeleteWorkspace": { + "name": "DeleteWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "DeleteWorkspaceRequest" + }, + "output": { + "shape": "DeleteWorkspaceResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "GenerateCompletions": { "name": "GenerateCompletions", "http": { @@ -374,6 +490,33 @@ } ] }, + "ListAvailableProfiles": { + "name": "ListAvailableProfiles", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListAvailableProfilesRequest" + }, + "output": { + "shape": "ListAvailableProfilesResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "ListCodeAnalysisFindings": { "name": "ListCodeAnalysisFindings", "http": { @@ -404,6 +547,33 @@ } ] }, + "ListEvents": { + "name": "ListEvents", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListEventsRequest" + }, + "output": { + "shape": "ListEventsResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "ListFeatureEvaluations": { "name": "ListFeatureEvaluations", "http": { @@ -431,6 +601,60 @@ } ] }, + "ListUserMemoryEntries": { + "name": "ListUserMemoryEntries", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListUserMemoryEntriesInput" + }, + "output": { + "shape": "ListUserMemoryEntriesOutput" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, + "ListWorkspaceMetadata": { + "name": "ListWorkspaceMetadata", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListWorkspaceMetadataRequest" + }, + "output": { + "shape": "ListWorkspaceMetadataResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "ResumeTransformation": { "name": "ResumeTransformation", "http": { @@ -696,6 +920,56 @@ "type": "string", "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] }, + "ActiveFunctionalityList": { + "type": "list", + "member": { + "shape": "FunctionalityName" + }, + "max": 10, + "min": 0 + }, + "AdditionalContentEntry": { + "type": "structure", + "required": ["name", "description"], + "members": { + "name": { + "shape": "AdditionalContentEntryNameString" + }, + "description": { + "shape": "AdditionalContentEntryDescriptionString" + }, + "innerContext": { + "shape": "AdditionalContentEntryInnerContextString" + } + } + }, + "AdditionalContentEntryDescriptionString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryInnerContextString": { + "type": "string", + "max": 8192, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryNameString": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "[a-z]+(?:-[a-z0-9]+)*", + "sensitive": true + }, + "AdditionalContentList": { + "type": "list", + "member": { + "shape": "AdditionalContentEntry" + }, + "max": 20, + "min": 0 + }, "AppStudioState": { "type": "structure", "required": ["namespace", "propertyName", "propertyContext"], @@ -738,6 +1012,30 @@ "min": 0, "sensitive": true }, + "ApplicationProperties": { + "type": "structure", + "required": ["tenantId", "applicationArn", "tenantUrl", "applicationType"], + "members": { + "tenantId": { + "shape": "TenantId" + }, + "applicationArn": { + "shape": "ResourceArn" + }, + "tenantUrl": { + "shape": "Url" + }, + "applicationType": { + "shape": "FunctionalityName" + } + } + }, + "ApplicationPropertiesList": { + "type": "list", + "member": { + "shape": "ApplicationProperties" + } + }, "ArtifactId": { "type": "string", "max": 126, @@ -777,15 +1075,32 @@ }, "followupPrompt": { "shape": "FollowupPrompt" + }, + "toolUses": { + "shape": "ToolUses" } } }, "AssistantResponseMessageContentString": { "type": "string", - "max": 4096, + "max": 100000, "min": 0, "sensitive": true }, + "AttributesMap": { + "type": "map", + "key": { + "shape": "AttributesMapKeyString" + }, + "value": { + "shape": "StringList" + } + }, + "AttributesMapKeyString": { + "type": "string", + "max": 128, + "min": 1 + }, "Base64EncodedPaginationToken": { "type": "string", "max": 2048, @@ -796,12 +1111,24 @@ "type": "boolean", "box": true }, - "ChatAddMessageEvent": { + "ByUserAnalytics": { "type": "structure", - "required": ["conversationId", "messageId"], + "required": ["toggle"], "members": { - "conversationId": { - "shape": "ConversationId" + "s3Uri": { + "shape": "S3Uri" + }, + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, + "ChatAddMessageEvent": { + "type": "structure", + "required": ["conversationId", "messageId"], + "members": { + "conversationId": { + "shape": "ConversationId" }, "messageId": { "shape": "MessageId" @@ -849,7 +1176,7 @@ "member": { "shape": "ChatMessage" }, - "max": 10, + "max": 250, "min": 0 }, "ChatInteractWithMessageEvent": { @@ -885,6 +1212,12 @@ }, "userIntent": { "shape": "UserIntent" + }, + "addedIdeDiagnostics": { + "shape": "IdeDiagnosticList" + }, + "removedIdeDiagnostics": { + "shape": "IdeDiagnosticList" } } }, @@ -947,6 +1280,11 @@ } } }, + "ClientId": { + "type": "string", + "max": 255, + "min": 1 + }, "CodeAnalysisFindingsSchema": { "type": "string", "enum": ["codeanalysis/findings/1.0"] @@ -1012,6 +1350,21 @@ "type": "integer", "min": 0 }, + "CodeDescription": { + "type": "structure", + "required": ["href"], + "members": { + "href": { + "shape": "CodeDescriptionHrefString" + } + } + }, + "CodeDescriptionHrefString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, "CodeFixAcceptanceEvent": { "type": "structure", "required": ["jobId"], @@ -1329,7 +1682,11 @@ }, "CreateTaskAssistConversationRequest": { "type": "structure", - "members": {} + "members": { + "profileArn": { + "shape": "ProfileArn" + } + } }, "CreateTaskAssistConversationResponse": { "type": "structure", @@ -1366,6 +1723,9 @@ }, "uploadId": { "shape": "UploadId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -1404,6 +1764,71 @@ } } }, + "CreateUserMemoryEntryInput": { + "type": "structure", + "required": ["memoryEntryString", "origin"], + "members": { + "memoryEntryString": { + "shape": "CreateUserMemoryEntryInputMemoryEntryStringString" + }, + "origin": { + "shape": "Origin" + }, + "profileArn": { + "shape": "CreateUserMemoryEntryInputProfileArnString" + }, + "clientToken": { + "shape": "String", + "idempotencyToken": true + } + } + }, + "CreateUserMemoryEntryInputMemoryEntryStringString": { + "type": "string", + "min": 1, + "sensitive": true + }, + "CreateUserMemoryEntryInputProfileArnString": { + "type": "string", + "min": 1, + "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + }, + "CreateUserMemoryEntryOutput": { + "type": "structure", + "required": ["memoryEntry"], + "members": { + "memoryEntry": { + "shape": "MemoryEntry" + } + } + }, + "CreateWorkspaceRequest": { + "type": "structure", + "required": ["workspaceRoot"], + "members": { + "workspaceRoot": { + "shape": "CreateWorkspaceRequestWorkspaceRootString" + }, + "profileArn": { + "shape": "ProfileArn" + } + } + }, + "CreateWorkspaceRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "CreateWorkspaceResponse": { + "type": "structure", + "required": ["workspace"], + "members": { + "workspace": { + "shape": "WorkspaceMetadata" + } + } + }, "CursorState": { "type": "structure", "members": { @@ -1449,12 +1874,24 @@ "shape": "Customization" } }, + "DashboardAnalytics": { + "type": "structure", + "required": ["toggle"], + "members": { + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, "DeleteTaskAssistConversationRequest": { "type": "structure", "required": ["conversationId"], "members": { "conversationId": { "shape": "ConversationId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -1467,6 +1904,49 @@ } } }, + "DeleteUserMemoryEntryInput": { + "type": "structure", + "required": ["id"], + "members": { + "id": { + "shape": "DeleteUserMemoryEntryInputIdString" + }, + "profileArn": { + "shape": "DeleteUserMemoryEntryInputProfileArnString" + } + } + }, + "DeleteUserMemoryEntryInputIdString": { + "type": "string", + "max": 36, + "min": 36, + "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + }, + "DeleteUserMemoryEntryInputProfileArnString": { + "type": "string", + "min": 1, + "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + }, + "DeleteUserMemoryEntryOutput": { + "type": "structure", + "members": {} + }, + "DeleteWorkspaceRequest": { + "type": "structure", + "required": ["workspaceId"], + "members": { + "workspaceId": { + "shape": "UUID" + }, + "profileArn": { + "shape": "ProfileArn" + } + } + }, + "DeleteWorkspaceResponse": { + "type": "structure", + "members": {} + }, "Description": { "type": "string", "max": 256, @@ -1485,10 +1965,66 @@ }, "union": true }, + "DiagnosticLocation": { + "type": "structure", + "required": ["uri", "range"], + "members": { + "uri": { + "shape": "DiagnosticLocationUriString" + }, + "range": { + "shape": "Range" + } + } + }, + "DiagnosticLocationUriString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "DiagnosticRelatedInformation": { + "type": "structure", + "required": ["location", "message"], + "members": { + "location": { + "shape": "DiagnosticLocation" + }, + "message": { + "shape": "DiagnosticRelatedInformationMessageString" + } + } + }, + "DiagnosticRelatedInformationList": { + "type": "list", + "member": { + "shape": "DiagnosticRelatedInformation" + }, + "max": 1024, + "min": 0 + }, + "DiagnosticRelatedInformationMessageString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, "DiagnosticSeverity": { "type": "string", "enum": ["ERROR", "WARNING", "INFORMATION", "HINT"] }, + "DiagnosticTag": { + "type": "string", + "enum": ["UNNECESSARY", "DEPRECATED"] + }, + "DiagnosticTagList": { + "type": "list", + "member": { + "shape": "DiagnosticTag" + }, + "max": 1024, + "min": 0 + }, "Dimension": { "type": "structure", "members": { @@ -1741,6 +2277,9 @@ }, "useRelevantDocuments": { "shape": "Boolean" + }, + "workspaceFolders": { + "shape": "WorkspaceFolderList" } } }, @@ -1810,6 +2349,65 @@ "max": 100, "min": 0 }, + "ErrorDetails": { + "type": "string", + "max": 2048, + "min": 0 + }, + "Event": { + "type": "structure", + "required": ["eventId", "generationId", "eventTimestamp", "eventType", "eventBlob"], + "members": { + "eventId": { + "shape": "UUID" + }, + "generationId": { + "shape": "UUID" + }, + "eventTimestamp": { + "shape": "SyntheticTimestamp_date_time" + }, + "eventType": { + "shape": "EventType" + }, + "eventBlob": { + "shape": "EventBlob" + } + } + }, + "EventBlob": { + "type": "blob", + "max": 400000, + "min": 1, + "sensitive": true + }, + "EventList": { + "type": "list", + "member": { + "shape": "Event" + }, + "max": 10, + "min": 1 + }, + "EventType": { + "type": "string", + "max": 100, + "min": 1 + }, + "ExternalIdentityDetails": { + "type": "structure", + "members": { + "issuerUrl": { + "shape": "IssuerUrl" + }, + "clientId": { + "shape": "ClientId" + }, + "scimEndpoint": { + "shape": "String" + } + } + }, "FeatureDevCodeAcceptanceEvent": { "type": "structure", "required": ["conversationId", "linesOfCodeAccepted", "charactersOfCodeAccepted"], @@ -1983,6 +2581,21 @@ "min": 0, "sensitive": true }, + "FunctionalityName": { + "type": "string", + "enum": [ + "COMPLETIONS", + "ANALYSIS", + "CONVERSATIONS", + "TASK_ASSIST", + "TRANSFORMATIONS", + "CHAT_CUSTOMIZATION", + "TRANSFORMATIONS_WEBAPP", + "FEATURE_DEVELOPMENT" + ], + "max": 64, + "min": 1 + }, "GenerateCompletionsRequest": { "type": "structure", "required": ["fileContext"], @@ -2013,6 +2626,9 @@ }, "profileArn": { "shape": "ProfileArn" + }, + "workspaceId": { + "shape": "UUID" } } }, @@ -2046,6 +2662,9 @@ "members": { "jobId": { "shape": "GetCodeAnalysisRequestJobIdString" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2072,6 +2691,9 @@ "members": { "jobId": { "shape": "GetCodeFixJobRequestJobIdString" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2101,6 +2723,9 @@ }, "codeGenerationId": { "shape": "CodeGenerationId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2134,6 +2759,9 @@ }, "testGenerationJobId": { "shape": "UUID" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2151,6 +2779,9 @@ "members": { "transformationJobId": { "shape": "TransformationJobId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2169,6 +2800,9 @@ "members": { "transformationJobId": { "shape": "TransformationJobId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2201,44 +2835,131 @@ "max": 64, "min": 1 }, - "IdempotencyToken": { - "type": "string", - "max": 256, - "min": 1 - }, - "Import": { + "IdeDiagnostic": { "type": "structure", + "required": ["ideDiagnosticType"], "members": { - "statement": { - "shape": "ImportStatementString" + "range": { + "shape": "Range" + }, + "source": { + "shape": "IdeDiagnosticSourceString" + }, + "severity": { + "shape": "DiagnosticSeverity" + }, + "ideDiagnosticType": { + "shape": "IdeDiagnosticType" } } }, - "ImportStatementString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "Imports": { + "IdeDiagnosticList": { "type": "list", "member": { - "shape": "Import" + "shape": "IdeDiagnostic" }, - "max": 10, + "max": 1024, "min": 0 }, - "InlineChatEvent": { - "type": "structure", - "required": ["requestId", "timestamp"], - "members": { - "requestId": { - "shape": "UUID" - }, - "timestamp": { - "shape": "Timestamp" - }, - "inputLength": { + "IdeDiagnosticSourceString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "IdeDiagnosticType": { + "type": "string", + "enum": ["SYNTAX_ERROR", "TYPE_ERROR", "REFERENCE_ERROR", "BEST_PRACTICE", "SECURITY", "OTHER"] + }, + "IdempotencyToken": { + "type": "string", + "max": 256, + "min": 1 + }, + "IdentityDetails": { + "type": "structure", + "members": { + "ssoIdentityDetails": { + "shape": "SSOIdentityDetails" + }, + "externalIdentityDetails": { + "shape": "ExternalIdentityDetails" + } + }, + "union": true + }, + "ImageBlock": { + "type": "structure", + "required": ["format", "source"], + "members": { + "format": { + "shape": "ImageFormat" + }, + "source": { + "shape": "ImageSource" + } + } + }, + "ImageBlocks": { + "type": "list", + "member": { + "shape": "ImageBlock" + }, + "max": 10, + "min": 0 + }, + "ImageFormat": { + "type": "string", + "enum": ["png", "jpeg", "gif", "webp"] + }, + "ImageSource": { + "type": "structure", + "members": { + "bytes": { + "shape": "ImageSourceBytesBlob" + } + }, + "sensitive": true, + "union": true + }, + "ImageSourceBytesBlob": { + "type": "blob", + "max": 1500000, + "min": 1 + }, + "Import": { + "type": "structure", + "members": { + "statement": { + "shape": "ImportStatementString" + } + } + }, + "ImportStatementString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "Imports": { + "type": "list", + "member": { + "shape": "Import" + }, + "max": 10, + "min": 0 + }, + "InlineChatEvent": { + "type": "structure", + "required": ["requestId", "timestamp"], + "members": { + "requestId": { + "shape": "UUID" + }, + "timestamp": { + "shape": "Timestamp" + }, + "inputLength": { "shape": "PrimitiveInteger" }, "numSelectedLines": { @@ -2308,6 +3029,11 @@ "throttling": false } }, + "IssuerUrl": { + "type": "string", + "max": 255, + "min": 1 + }, "LineRangeList": { "type": "list", "member": { @@ -2322,6 +3048,9 @@ }, "nextToken": { "shape": "Base64EncodedPaginationToken" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2343,6 +3072,35 @@ } } }, + "ListAvailableProfilesRequest": { + "type": "structure", + "members": { + "maxResults": { + "shape": "ListAvailableProfilesRequestMaxResultsInteger" + }, + "nextToken": { + "shape": "Base64EncodedPaginationToken" + } + } + }, + "ListAvailableProfilesRequestMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 10, + "min": 1 + }, + "ListAvailableProfilesResponse": { + "type": "structure", + "required": ["profiles"], + "members": { + "profiles": { + "shape": "ProfileList" + }, + "nextToken": { + "shape": "Base64EncodedPaginationToken" + } + } + }, "ListCodeAnalysisFindingsRequest": { "type": "structure", "required": ["jobId", "codeAnalysisFindingsSchema"], @@ -2355,6 +3113,9 @@ }, "codeAnalysisFindingsSchema": { "shape": "CodeAnalysisFindingsSchema" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2375,12 +3136,51 @@ } } }, + "ListEventsRequest": { + "type": "structure", + "required": ["conversationId"], + "members": { + "conversationId": { + "shape": "UUID" + }, + "maxResults": { + "shape": "ListEventsRequestMaxResultsInteger" + }, + "nextToken": { + "shape": "NextToken" + } + } + }, + "ListEventsRequestMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 50, + "min": 1 + }, + "ListEventsResponse": { + "type": "structure", + "required": ["conversationId", "events"], + "members": { + "conversationId": { + "shape": "UUID" + }, + "events": { + "shape": "EventList" + }, + "nextToken": { + "shape": "NextToken" + } + } + }, "ListFeatureEvaluationsRequest": { "type": "structure", "required": ["userContext"], "members": { "userContext": { "shape": "UserContext" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2393,10 +3193,141 @@ } } }, + "ListUserMemoryEntriesInput": { + "type": "structure", + "members": { + "maxResults": { + "shape": "ListUserMemoryEntriesInputMaxResultsInteger" + }, + "profileArn": { + "shape": "ListUserMemoryEntriesInputProfileArnString" + }, + "nextToken": { + "shape": "ListUserMemoryEntriesInputNextTokenString" + } + } + }, + "ListUserMemoryEntriesInputMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 1 + }, + "ListUserMemoryEntriesInputNextTokenString": { + "type": "string", + "min": 1 + }, + "ListUserMemoryEntriesInputProfileArnString": { + "type": "string", + "min": 1, + "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + }, + "ListUserMemoryEntriesOutput": { + "type": "structure", + "required": ["memoryEntries"], + "members": { + "memoryEntries": { + "shape": "MemoryEntryList" + }, + "nextToken": { + "shape": "ListUserMemoryEntriesOutputNextTokenString" + } + } + }, + "ListUserMemoryEntriesOutputNextTokenString": { + "type": "string", + "min": 1 + }, + "ListWorkspaceMetadataRequest": { + "type": "structure", + "members": { + "workspaceRoot": { + "shape": "ListWorkspaceMetadataRequestWorkspaceRootString" + }, + "nextToken": { + "shape": "String" + }, + "maxResults": { + "shape": "Integer" + }, + "profileArn": { + "shape": "ProfileArn" + } + } + }, + "ListWorkspaceMetadataRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "ListWorkspaceMetadataResponse": { + "type": "structure", + "required": ["workspaces"], + "members": { + "workspaces": { + "shape": "WorkspaceList" + }, + "nextToken": { + "shape": "String" + } + } + }, "Long": { "type": "long", "box": true }, + "MemoryEntry": { + "type": "structure", + "required": ["id", "memoryEntryString", "metadata"], + "members": { + "id": { + "shape": "MemoryEntryIdString" + }, + "memoryEntryString": { + "shape": "MemoryEntryMemoryEntryStringString" + }, + "metadata": { + "shape": "MemoryEntryMetadata" + } + } + }, + "MemoryEntryIdString": { + "type": "string", + "max": 36, + "min": 36, + "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + }, + "MemoryEntryList": { + "type": "list", + "member": { + "shape": "MemoryEntry" + } + }, + "MemoryEntryMemoryEntryStringString": { + "type": "string", + "max": 500, + "min": 1, + "sensitive": true + }, + "MemoryEntryMetadata": { + "type": "structure", + "required": ["origin", "createdAt", "updatedAt"], + "members": { + "origin": { + "shape": "Origin" + }, + "attributes": { + "shape": "AttributesMap" + }, + "createdAt": { + "shape": "Timestamp" + }, + "updatedAt": { + "shape": "Timestamp" + } + } + }, "MessageId": { "type": "string", "max": 128, @@ -2435,16 +3366,134 @@ "min": 1, "pattern": "[-a-zA-Z0-9._]*" }, + "NextToken": { + "type": "string", + "max": 1000, + "min": 0 + }, + "Notifications": { + "type": "list", + "member": { + "shape": "NotificationsFeature" + }, + "max": 10, + "min": 0 + }, + "NotificationsFeature": { + "type": "structure", + "required": ["feature", "toggle"], + "members": { + "feature": { + "shape": "FeatureName" + }, + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, "OperatingSystem": { "type": "string", "enum": ["MAC", "WINDOWS", "LINUX"], "max": 64, "min": 1 }, + "OptInFeatureToggle": { + "type": "string", + "enum": ["ON", "OFF"] + }, + "OptInFeatures": { + "type": "structure", + "members": { + "promptLogging": { + "shape": "PromptLogging" + }, + "byUserAnalytics": { + "shape": "ByUserAnalytics" + }, + "dashboardAnalytics": { + "shape": "DashboardAnalytics" + }, + "notifications": { + "shape": "Notifications" + }, + "workspaceContext": { + "shape": "WorkspaceContext" + } + } + }, "OptOutPreference": { "type": "string", "enum": ["OPTIN", "OPTOUT"] }, + "Origin": { + "type": "string", + "enum": [ + "CHATBOT", + "CONSOLE", + "DOCUMENTATION", + "MARKETING", + "MOBILE", + "SERVICE_INTERNAL", + "UNIFIED_SEARCH", + "UNKNOWN", + "MD", + "IDE", + "SAGE_MAKER", + "CLI", + "AI_EDITOR", + "OPENSEARCH_DASHBOARD", + "GITLAB" + ] + }, + "PackageInfo": { + "type": "structure", + "members": { + "executionCommand": { + "shape": "SensitiveString" + }, + "buildCommand": { + "shape": "SensitiveString" + }, + "buildOrder": { + "shape": "PackageInfoBuildOrderInteger" + }, + "testFramework": { + "shape": "String" + }, + "packageSummary": { + "shape": "PackageInfoPackageSummaryString" + }, + "packagePlan": { + "shape": "PackageInfoPackagePlanString" + }, + "targetFileInfoList": { + "shape": "TargetFileInfoList" + } + } + }, + "PackageInfoBuildOrderInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "PackageInfoList": { + "type": "list", + "member": { + "shape": "PackageInfo" + } + }, + "PackageInfoPackagePlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "PackageInfoPackageSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, "PaginationToken": { "type": "string", "max": 2048, @@ -2472,12 +3521,86 @@ "PrimitiveInteger": { "type": "integer" }, + "Profile": { + "type": "structure", + "required": ["arn", "profileName"], + "members": { + "arn": { + "shape": "ProfileArn" + }, + "identityDetails": { + "shape": "IdentityDetails" + }, + "profileName": { + "shape": "ProfileName" + }, + "description": { + "shape": "ProfileDescription" + }, + "referenceTrackerConfiguration": { + "shape": "ReferenceTrackerConfiguration" + }, + "kmsKeyArn": { + "shape": "ResourceArn" + }, + "activeFunctionalities": { + "shape": "ActiveFunctionalityList" + }, + "status": { + "shape": "ProfileStatus" + }, + "errorDetails": { + "shape": "ErrorDetails" + }, + "resourcePolicy": { + "shape": "ResourcePolicy" + }, + "profileType": { + "shape": "ProfileType" + }, + "optInFeatures": { + "shape": "OptInFeatures" + }, + "permissionUpdateRequired": { + "shape": "Boolean" + }, + "applicationProperties": { + "shape": "ApplicationPropertiesList" + } + } + }, "ProfileArn": { "type": "string", "max": 950, "min": 0, "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, + "ProfileDescription": { + "type": "string", + "max": 256, + "min": 1, + "pattern": "[\\sa-zA-Z0-9_-]*" + }, + "ProfileList": { + "type": "list", + "member": { + "shape": "Profile" + } + }, + "ProfileName": { + "type": "string", + "max": 100, + "min": 1, + "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" + }, + "ProfileStatus": { + "type": "string", + "enum": ["ACTIVE", "CREATING", "CREATE_FAILED", "UPDATING", "UPDATE_FAILED", "DELETING", "DELETE_FAILED"] + }, + "ProfileType": { + "type": "string", + "enum": ["Q_DEVELOPER", "CODEWHISPERER"] + }, "ProgrammingLanguage": { "type": "structure", "required": ["languageName"], @@ -2499,6 +3622,18 @@ "shape": "TransformationProgressUpdate" } }, + "PromptLogging": { + "type": "structure", + "required": ["s3Uri", "toggle"], + "members": { + "s3Uri": { + "shape": "S3Uri" + }, + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, "Range": { "type": "structure", "required": ["start", "end"], @@ -2569,7 +3704,7 @@ "member": { "shape": "RelevantTextDocument" }, - "max": 5, + "max": 30, "min": 0 }, "RelevantTextDocument": { @@ -2587,12 +3722,6 @@ }, "documentSymbols": { "shape": "DocumentSymbols" - }, - "startLine": { - "shape": "Integer" - }, - "endLine": { - "shape": "Integer" } } }, @@ -2604,7 +3733,7 @@ }, "RelevantTextDocumentTextString": { "type": "string", - "max": 10240, + "max": 40960, "min": 0, "sensitive": true }, @@ -2646,6 +3775,19 @@ }, "exception": true }, + "ResourcePolicy": { + "type": "structure", + "required": ["effect"], + "members": { + "effect": { + "shape": "ResourcePolicyEffect" + } + } + }, + "ResourcePolicyEffect": { + "type": "string", + "enum": ["ALLOW", "DENY"] + }, "ResumeTransformationRequest": { "type": "structure", "required": ["transformationJobId"], @@ -2655,6 +3797,9 @@ }, "userActionStatus": { "shape": "TransformationUserActionStatus" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2682,17 +3827,44 @@ } } }, - "RuntimeDiagnosticMessageString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "RuntimeDiagnosticSourceString": { + "RuntimeDiagnosticMessageString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "RuntimeDiagnosticSourceString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "S3Uri": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "s3://((?!xn--)[a-z0-9](?![^/]*[.]{2})[a-z0-9-.]{1,61}[a-z0-9](? { return await this.storeCodeResultMessageId(data) }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + this.sessionStorage.deleteAllSessions() + }) } private async processChatItemVotedMessage(tabId: string, vote: string) { @@ -576,7 +579,7 @@ export class FeatureDevController { open ) if (resp === open) { - await vscode.commands.executeCommand('aws.amazonq.AmazonQChatView.focus') + await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus') // TODO add focusing on the specific tab once that's implemented } } diff --git a/packages/core/src/amazonqGumby/app.ts b/packages/core/src/amazonqGumby/app.ts index 1638231ef24..21182b38155 100644 --- a/packages/core/src/amazonqGumby/app.ts +++ b/packages/core/src/amazonqGumby/app.ts @@ -67,6 +67,9 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) showTransformationHub.register() diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index 5f45cc66d12..c1e3bd34ea3 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -145,6 +145,10 @@ export class GumbyController { this.chatControllerMessageListeners.errorThrown.event((data) => { return this.handleError(data) }) + + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + this.sessionStorage.removeActiveTab() + }) } private async tabOpened(message: any) { diff --git a/packages/core/src/amazonqTest/app.ts b/packages/core/src/amazonqTest/app.ts index 62ed24aee0d..6c638c13b71 100644 --- a/packages/core/src/amazonqTest/app.ts +++ b/packages/core/src/amazonqTest/app.ts @@ -68,6 +68,9 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) testGenState.setChatControllers(testChatControllerEventEmitters) // TODO: Add testGen provider for creating new files after test generation if they does not exist } diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts index 07a087977d9..747cca57e8e 100644 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ b/packages/core/src/amazonqTest/chat/controller/controller.ts @@ -202,6 +202,10 @@ export class TestController { return this.openDiff(data) } }) + + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + this.sessionStorage.removeActiveTab() + }) } /** @@ -928,6 +932,7 @@ export class TestController { codeQuery: undefined, userIntent: UserIntent.GENERATE_UNIT_TESTS, customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, context: [], relevantTextDocuments: [], additionalContents: [], diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 73f65ca4ada..70a7417f263 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -53,6 +53,7 @@ import { focusIssue, showExploreAgentsView, showCodeIssueGroupingQuickPick, + selectRegionProfileCommand, } from './commands/basicCommands' import { ReferenceLogViewProvider } from './service/referenceLogViewProvider' import { ReferenceHoverProvider } from './service/referenceHoverProvider' @@ -296,6 +297,7 @@ export async function activate(context: ExtContext): Promise { selectCustomizationPrompt.register(), // notify new customizations notifyNewCustomizationsCmd.register(), + selectRegionProfileCommand.register(), /** * On recommendation acceptance */ diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index b2f9808a849..b338eaf65e1 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -23,24 +23,17 @@ import { indent } from '../../shared/utilities/textUtilities' import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util' import { extensionVersion, getServiceEnvVarConfig } from '../../shared/vscode/env' import { DevSettings } from '../../shared/settings' +import { CodeWhispererConfig } from '../models/model' const keepAliveHeader = 'keep-alive-codewhisperer' -export interface CodeWhispererConfig { - readonly region: string - readonly endpoint: string -} - -export const defaultServiceConfig: CodeWhispererConfig = { - region: 'us-east-1', - endpoint: 'https://codewhisperer.us-east-1.amazonaws.com/', -} export function getCodewhispererConfig(): CodeWhispererConfig { + const clientConfig = AuthUtil.instance.regionProfileManager.clientConfig return { - ...DevSettings.instance.getServiceConfig('codewhispererService', defaultServiceConfig), + ...DevSettings.instance.getServiceConfig('codewhispererService', clientConfig), // Environment variable overrides - ...getServiceEnvVarConfig('codewhisperer', Object.keys(defaultServiceConfig)), + ...getServiceEnvVarConfig('codewhisperer', Object.keys(clientConfig)), } } @@ -209,13 +202,15 @@ export class DefaultCodeWhispererClient { } public async listCodeScanFindings( - request: ListCodeScanFindingsRequest + request: ListCodeScanFindingsRequest, + profileArn: string | undefined ): Promise> { if (this.isBearerTokenAuth()) { const req = { jobId: request.jobId, nextToken: request.nextToken, codeAnalysisFindingsSchema: 'codeanalysis/findings/1.0', + profileArn: profileArn, } as CodeWhispererUserClient.ListCodeAnalysisFindingsRequest return (await this.createUserSdkClient()).listCodeAnalysisFindings(req).promise() } @@ -226,9 +221,10 @@ export class DefaultCodeWhispererClient { public async listAvailableCustomizations(): Promise { const client = await this.createUserSdkClient() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const requester = async (request: CodeWhispererUserClient.ListAvailableCustomizationsRequest) => client.listAvailableCustomizations(request).promise() - return pageableToCollection(requester, {}, 'nextToken') + return pageableToCollection(requester, { profileArn: profile?.arn }, 'nextToken') .promise() .then((resps) => { let logStr = 'amazonq: listAvailableCustomizations API request:' diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index d7831be9584..c2978b56568 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -55,7 +55,6 @@ import { isRemoteWorkspace } from '../../shared/vscode/env' import { isBuilderIdConnection } from '../../auth/connection' import globals from '../../shared/extensionGlobals' import { getVscodeCliPath } from '../../shared/utilities/pathFind' -import { setContext } from '../../shared/vscode/setContext' import { tryRun } from '../../shared/utilities/pathFind' import { IssueItem, SecurityIssueTreeViewProvider } from '../service/securityIssueTreeViewProvider' import { SecurityIssueProvider } from '../service/securityIssueProvider' @@ -69,6 +68,8 @@ import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' import { parsePatch } from 'diff' import { createCodeIssueGroupingStrategyPrompter } from '../ui/prompters' import { cancel, confirm } from '../../shared/localizedText' +import { showQuickPick } from '../../shared/ui/pickerPrompter' +import { i18n } from '../../shared/i18n-helper' const MessageTimeOut = 5_000 @@ -104,8 +105,6 @@ export const enableCodeSuggestions = Commands.declare( (context: ExtContext) => async (isAuto: boolean = true) => { await CodeSuggestionsState.instance.setSuggestionsEnabled(isAuto) - await setContext('aws.codewhisperer.connected', true) - await setContext('aws.codewhisperer.connectionExpired', false) vsCodeState.isFreeTierLimitReached = false await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') } @@ -248,6 +247,22 @@ export const selectCustomizationPrompt = Commands.declare( } ) +export const selectRegionProfileCommand = Commands.declare( + { id: 'aws.amazonq.selectRegionProfile', compositeKey: { 1: 'source' } }, + () => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { + const quickPickItems = AuthUtil.instance.regionProfileManager.generateQuickPickItem() + + await showQuickPick(quickPickItems, { + title: localize('AWS.amazonq.profile.quickPick.title', 'Select a Profile'), + placeholder: localize( + 'AWS.amazonq.profile.quickPick.placeholder', + 'You can choose from the following profiles:' + ), + recentlyUsed: i18n('AWS.codewhisperer.customization.selected'), + }) + } +) + export const reconnect = Commands.declare( { id: 'aws.amazonq.reconnect', compositeKey: { 1: 'source' } }, () => async (_: VsCodeCommandArg, source: CodeWhispererSource) => await AuthUtil.instance.reauthenticate() diff --git a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts index 8e9a5240812..9dc2fd5f68f 100644 --- a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts +++ b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts @@ -19,6 +19,7 @@ import path from 'path' import { TelemetryHelper } from '../util/telemetryHelper' import { tempDirPath } from '../../shared/filesystemUtilities' import { CodeWhispererSettings } from '../util/codewhispererSettings' +import { AuthUtil } from '../util/authUtil' export async function startCodeFixGeneration( client: DefaultCodeWhispererClient, @@ -26,6 +27,7 @@ export async function startCodeFixGeneration( filePath: string, codeFixName: string ) { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile /** * Step 0: Initial code fix telemetry */ @@ -53,7 +55,7 @@ export async function startCodeFixGeneration( */ let artifactMap: ArtifactMap = {} try { - artifactMap = await getPresignedUrlAndUpload(client, zipFilePath, codeFixName) + artifactMap = await getPresignedUrlAndUpload(client, zipFilePath, codeFixName, profile) } finally { await fs.delete(zipFilePath) } @@ -76,7 +78,8 @@ export async function startCodeFixGeneration( : 'BLOCK', }, codeFixName, - issue.ruleId + issue.ruleId, + profile ) if (codeFixJob.status === 'Failed') { throw new CreateCodeFixError() @@ -89,7 +92,7 @@ export async function startCodeFixGeneration( * Step 4: Polling mechanism on code fix job status */ throwIfCancelled() - const jobStatus = await pollCodeFixJobStatus(client, String(codeFixJob.jobId)) + const jobStatus = await pollCodeFixJobStatus(client, String(codeFixJob.jobId), profile) if (jobStatus === 'Failed') { getLogger().verbose(`Code fix generation failed.`) throw new CreateCodeFixError() @@ -101,7 +104,7 @@ export async function startCodeFixGeneration( throwIfCancelled() getLogger().verbose(`Code fix job succeeded and start processing result.`) - const { suggestedFix } = await getCodeFixJob(client, String(codeFixJob.jobId)) + const { suggestedFix } = await getCodeFixJob(client, String(codeFixJob.jobId), profile) // eslint-disable-next-line aws-toolkits/no-json-stringify-in-log getLogger().verbose(`Suggested fix: ${JSON.stringify(suggestedFix)}`) return { suggestedFix, jobId } diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index cc9b733f56d..d04fe6effc3 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -108,6 +108,7 @@ export async function startSecurityScan( zipUtil: ZipUtil = new ZipUtil(), scanUuid?: string ) { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const logger = getLoggerForScope(scope) /** * Step 0: Initial Code Scan telemetry @@ -187,7 +188,7 @@ export async function startSecurityScan( const uploadStartTime = performance.now() const scanName = randomUUID() try { - artifactMap = await getPresignedUrlAndUpload(client, zipMetadata, scope, scanName) + artifactMap = await getPresignedUrlAndUpload(client, zipMetadata, scope, scanName, profile) } finally { await zipUtil.removeTmpFiles(zipMetadata, scope) codeScanTelemetryEntry.artifactsUploadDuration = performance.now() - uploadStartTime @@ -212,7 +213,8 @@ export async function startSecurityScan( artifactMap, codeScanTelemetryEntry.codewhispererLanguage, scope, - scanName + scanName, + profile ) if (scanJob.status === 'Failed') { logger.verbose(`${scanJob.errorMessage}`) @@ -235,7 +237,8 @@ export async function startSecurityScan( scanUuid, }) } - const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope, codeScanStartTime) + // pass profile + const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope, codeScanStartTime, profile) if (jobStatus === 'Failed') { logger.verbose(`Security scan failed.`) throw new CodeScanJobFailedError() @@ -261,7 +264,8 @@ export async function startSecurityScan( CodeWhispererConstants.codeScanFindingsSchema, projectPaths, scope, - editor + editor, + profile ) for (const issue of securityRecommendationCollection .flatMap(({ issues }) => issues) diff --git a/packages/core/src/codewhisperer/commands/startTestGeneration.ts b/packages/core/src/codewhisperer/commands/startTestGeneration.ts index 29e7148a17b..003790937c0 100644 --- a/packages/core/src/codewhisperer/commands/startTestGeneration.ts +++ b/packages/core/src/codewhisperer/commands/startTestGeneration.ts @@ -21,6 +21,7 @@ import { ChildProcess, spawn } from 'child_process' // eslint-disable-line no-re import { BuildStatus } from '../../amazonqTest/chat/session/session' import { fs } from '../../shared/fs/fs' import { Range } from '../client/codewhispereruserclient' +import { AuthUtil } from '../indexNode' // eslint-disable-next-line unicorn/no-null let spawnResult: ChildProcess | null = null @@ -34,6 +35,7 @@ export async function startTestGenerationProcess( ) { const logger = getLogger() const session = ChatSessionManager.Instance.getSession() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile // TODO: Step 0: Initial Test Gen telemetry try { logger.verbose(`Starting Test Generation `) @@ -96,7 +98,9 @@ export async function startTestGenerationProcess( targetLineRangeList: selectionRange ? [selectionRange] : [], }, ], - userInputPrompt + userInputPrompt, + undefined, + profile ) if (!testJob.testGenerationJob) { throw Error('Test job not found') @@ -114,7 +118,8 @@ export async function startTestGenerationProcess( testJob.testGenerationJob.testGenerationJobId, testJob.testGenerationJob.testGenerationJobGroupName, filePath, - initialExecution + initialExecution, + profile ) // TODO: Send status to test summary throwIfCancelled() diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index a7ea1365cca..cad260e7012 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -19,6 +19,7 @@ import { TransformByQStatus, TransformationType, TransformationCandidateProject, + RegionProfile, } from '../models/model' import { createZipManifest, @@ -80,6 +81,7 @@ import globals from '../../shared/extensionGlobals' import { convertDateToTimestamp } from '../../shared/datetime' import { findStringInDirectory } from '../../shared/utilities/workspaceUtils' import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' +import { AuthUtil } from '../util/authUtil' export function getFeedbackCommentData() { const jobId = transformByQState.getJobId() @@ -172,6 +174,7 @@ export async function startTransformByQ() { await setTransformationToRunningState() try { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile // Set webview UI to poll for progress startInterval() @@ -179,13 +182,13 @@ export async function startTransformByQ() { const uploadId = await preTransformationUploadCode() // step 2: StartJob and store the returned jobId in TransformByQState - const jobId = await startTransformationJob(uploadId, transformStartTime) + const jobId = await startTransformationJob(uploadId, transformStartTime, profile) // step 3 (intermediate step): show transformation-plan.md file await pollTransformationStatusUntilPlanReady(jobId) // step 4: poll until artifacts are ready to download - await humanInTheLoopRetryLogic(jobId) + await humanInTheLoopRetryLogic(jobId, profile) } catch (error: any) { await transformationJobErrorHandler(error) } finally { @@ -201,16 +204,16 @@ export async function startTransformByQ() { * We only don't want to continue calling pollTransformationStatusUntilComplete if there is no HIL * state ever engaged or we have reached our max amount of HIL retries. */ -export async function humanInTheLoopRetryLogic(jobId: string) { +export async function humanInTheLoopRetryLogic(jobId: string, profile: RegionProfile | undefined) { let status = '' try { - status = await pollTransformationStatusUntilComplete(jobId) + status = await pollTransformationStatusUntilComplete(jobId, profile) if (status === 'PAUSED') { const hilStatusFailure = await initiateHumanInTheLoopPrompt(jobId) if (hilStatusFailure) { // We rejected the changes and resumed the job and should // try to resume normal polling asynchronously - void humanInTheLoopRetryLogic(jobId) + void humanInTheLoopRetryLogic(jobId, profile) } } else { await finalizeTransformByQ(status) @@ -294,9 +297,10 @@ export async function preTransformationUploadCode() { export async function initiateHumanInTheLoopPrompt(jobId: string) { try { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const humanInTheLoopManager = HumanInTheLoopManager.instance // 1) We need to call GetTransformationPlan to get artifactId - const transformationSteps = await getTransformationSteps(jobId, false) + const transformationSteps = await getTransformationSteps(jobId, false, profile) const { transformationStep, progressUpdate } = findDownloadArtifactStep(transformationSteps) if (!transformationStep || !progressUpdate) { @@ -418,6 +422,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { let successfulFeedbackLoop = true const jobId = transformByQState.getJobId() let hilResult: MetadataResult = MetadataResult.Pass + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile try { if (!selectedDependency) { throw new Error('No dependency selected') @@ -467,7 +472,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { // 8) Once code has been uploaded we will restart the job await resumeTransformationJob(jobId, 'COMPLETED') - void humanInTheLoopRetryLogic(jobId) + void humanInTheLoopRetryLogic(jobId, profile) } catch (err: any) { successfulFeedbackLoop = false CodeTransformTelemetryState.instance.setCodeTransformMetaDataField({ @@ -478,7 +483,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { // If anything went wrong in HIL state, we should restart the job // with the rejected state await terminateHILEarly(jobId) - void humanInTheLoopRetryLogic(jobId) + void humanInTheLoopRetryLogic(jobId, profile) } finally { // Always delete the dependency directories telemetry.codeTransform_humanInTheLoop.emit({ @@ -495,13 +500,17 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { return successfulFeedbackLoop } -export async function startTransformationJob(uploadId: string, transformStartTime: number) { +export async function startTransformationJob( + uploadId: string, + transformStartTime: number, + profile: RegionProfile | undefined +) { let jobId = '' try { await telemetry.codeTransform_jobStart.run(async () => { telemetry.record({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId() }) - jobId = await startJob(uploadId) + jobId = await startJob(uploadId, profile) getLogger().info(`CodeTransformation: jobId: ${jobId}`) telemetry.record({ @@ -537,9 +546,9 @@ export async function startTransformationJob(uploadId: string, transformStartTim return jobId } -export async function pollTransformationStatusUntilPlanReady(jobId: string) { +export async function pollTransformationStatusUntilPlanReady(jobId: string, profile?: RegionProfile) { try { - await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForPlanGenerated) + await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForPlanGenerated, profile) } catch (error) { getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) @@ -582,7 +591,7 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string) { } let plan = undefined try { - plan = await getTransformationPlan(jobId) + plan = await getTransformationPlan(jobId, profile) } catch (error) { // means API call failed getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) @@ -606,10 +615,10 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string) { throwIfCancelled() } -export async function pollTransformationStatusUntilComplete(jobId: string) { +export async function pollTransformationStatusUntilComplete(jobId: string, profile: RegionProfile | undefined) { let status = '' try { - status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl) + status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl, profile) } catch (error) { getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) if (!transformByQState.getJobFailureErrorNotification()) { diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index affe4b8457e..930b168beec 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -104,3 +104,4 @@ export { Container } from './service/serviceContainer' export * from './util/gitUtil' export * from './ui/prompters' export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker' +export { RegionProfileManager, defaultServiceConfig } from './region/regionProfileManager' diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index f7a150cb1f0..b887d2ade17 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -48,6 +48,18 @@ export const vsCodeState: VsCodeState = { isFreeTierLimitReached: false, } +export interface CodeWhispererConfig { + readonly region: string + readonly endpoint: string +} + +export interface RegionProfile { + name: string + region: string + arn: string + description: string +} + export type UtgStrategy = 'byName' | 'byContent' export type CrossFileStrategy = 'opentabs' | 'codemap' | 'bm25' | 'default' diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts new file mode 100644 index 00000000000..71f6338d82a --- /dev/null +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -0,0 +1,371 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getIcon } from '../../shared/icons' +import { DataQuickPickItem } from '../../shared/ui/pickerPrompter' +import { CodeWhispererConfig, RegionProfile } from '../models/model' +import { showConfirmationMessage } from '../../shared/utilities/messages' +import { + Connection, + isBuilderIdConnection, + isIdcSsoConnection, + isSsoConnection, + SsoConnection, +} from '../../auth/connection' +import globals from '../../shared/extensionGlobals' +import { once } from '../../shared/utilities/functionUtils' +import CodeWhispererUserClient from '../client/codewhispereruserclient' +import { Credentials, Service } from 'aws-sdk' +import { ServiceOptions } from '../../shared/awsClientBuilder' +import userApiConfig = require('../client/user-service-2.json') +import { createConstantMap } from '../../shared/utilities/tsUtils' +import { getLogger } from '../../shared/logger/logger' +import { pageableToCollection } from '../../shared/utilities/collectionUtils' +import { parse } from '@aws-sdk/util-arn-parser' +import { isAwsError, ToolkitError } from '../../shared/errors' +import { telemetry } from '../../shared/telemetry/telemetry' +import { localize } from '../../shared/utilities/vsCodeUtils' + +// TODO: is there a better way to manage all endpoint strings in one place? +export const defaultServiceConfig: CodeWhispererConfig = { + region: 'us-east-1', + endpoint: 'https://codewhisperer.us-east-1.amazonaws.com/', +} + +// Hack until we have a single discovery endpoint. We will call each endpoint one by one to fetch profile before then. +// TODO: update correct endpoint and region +const endpoints = createConstantMap({ + 'us-east-1': 'https://q.us-east-1.amazonaws.com/', + 'eu-central-1': 'https://q.eu-central-1.amazonaws.com/', +}) + +/** + * 'user' -> users change the profile through Q menu + * 'auth' -> users change the profile through webview profile selector page + * 'update' -> plugin auto select the profile on users' behalf as there is only 1 profile + * 'reload' -> on plugin restart, plugin will try to reload previous selected profile + */ +export type ProfileSwitchIntent = 'user' | 'auth' | 'update' | 'reload' + +export class RegionProfileManager { + private static logger = getLogger() + private _activeRegionProfile: RegionProfile | undefined + private _onDidChangeRegionProfile = new vscode.EventEmitter() + public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event + + // Store the last API results (for UI propuse) so we don't need to call service again if doesn't require "latest" result + private _profiles: RegionProfile[] = [] + + get activeRegionProfile() { + const conn = this.connectionProvider() + if (isBuilderIdConnection(conn)) { + return undefined + } + return this._activeRegionProfile + } + + get clientConfig(): CodeWhispererConfig { + const conn = this.connectionProvider() + if (!conn) { + throw new ToolkitError('trying to get client configuration without credential') + } + + // builder id should simply use default IAD + if (isBuilderIdConnection(conn)) { + return defaultServiceConfig + } + + // idc + const p = this.activeRegionProfile + if (p) { + const region = p.region + const endpoint = endpoints.get(p.region) + if (endpoint === undefined) { + RegionProfileManager.logger.error( + `Not found endpoint for region ${region}, not able to initialize a codewhisperer client` + ) + throw new ToolkitError(`Q client configuration error, endpoint not found for region ${region}`) + } + return { + region: region, + endpoint: endpoint, + } + } + + return defaultServiceConfig + } + + get profiles(): RegionProfile[] { + return this._profiles + } + + constructor(private readonly connectionProvider: () => Connection | undefined) {} + + async listRegionProfile(): Promise { + this._profiles = [] + + const conn = this.connectionProvider() + if (conn === undefined || !isSsoConnection(conn)) { + return [] + } + const availableProfiles: RegionProfile[] = [] + for (const [region, endpoint] of endpoints.entries()) { + const client = await this.createQClient(region, endpoint, conn as SsoConnection) + const requester = async (request: CodeWhispererUserClient.ListAvailableProfilesRequest) => + client.listAvailableProfiles(request).promise() + const request: CodeWhispererUserClient.ListAvailableProfilesRequest = {} + try { + const profiles = await pageableToCollection(requester, request, 'nextToken', 'profiles') + .flatten() + .promise() + const mappedPfs = profiles.map((it) => { + let accntId = '' + try { + accntId = parse(it.arn).accountId + } catch (e) {} + + return { + name: it.profileName, + region: region, + arn: it.arn, + description: accntId, + } + }) + + availableProfiles.push(...mappedPfs) + } catch (e) { + const logMsg = isAwsError(e) ? `requestId=${e.requestId}; message=${e.message}` : (e as Error).message + RegionProfileManager.logger.error(`failed to listRegionProfile: ${logMsg}`) + throw e + } + + RegionProfileManager.logger.info(`available amazonq profiles: ${availableProfiles.length}`) + } + + this._profiles = availableProfiles + return availableProfiles + } + + async switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) { + const conn = this.connectionProvider() + if (conn === undefined || !isIdcSsoConnection(conn)) { + return + } + + if (regionProfile && this.activeRegionProfile && regionProfile.arn === this.activeRegionProfile.arn) { + return + } + + // TODO: make it typesafe + const ssoConn = this.connectionProvider() as SsoConnection + + // only prompt to users when users switch from A profile to B profile + if (this.activeRegionProfile !== undefined && regionProfile !== undefined) { + const response = await showConfirmationMessage({ + prompt: localize( + 'AWS.amazonq.profile.confirmation', + "Do you want to change your Q Developer profile to '{0}'?\n When you change profiles, you will no longer have access to your current customizations, chats, code reviews, or any other code or content being generated by Amazon Q", + regionProfile?.name + ), + confirm: 'Switch profiles', + cancel: 'Cancel', + type: 'info', + }) + + if (!response) { + telemetry.amazonq_didSelectProfile.emit({ + source: source, + amazonQProfileRegion: this.activeRegionProfile?.region ?? 'not-set', + ssoRegion: ssoConn.ssoRegion, + result: 'Cancelled', + credentialStartUrl: ssoConn.startUrl, + profileCount: this.profiles.length, + }) + return + } + } + + if (source === 'reload' || source === 'update') { + telemetry.amazonq_profileState.emit({ + source: source, + amazonQProfileRegion: regionProfile?.region ?? 'not-set', + result: 'Succeeded', + }) + } else { + telemetry.amazonq_didSelectProfile.emit({ + source: source, + amazonQProfileRegion: regionProfile?.region ?? 'not-set', + ssoRegion: ssoConn.ssoRegion, + result: 'Succeeded', + credentialStartUrl: ssoConn.startUrl, + profileCount: this.profiles.length, + }) + } + + await this._switchRegionProfile(regionProfile) + } + + private async _switchRegionProfile(regionProfile: RegionProfile | undefined) { + this._activeRegionProfile = regionProfile + + this._onDidChangeRegionProfile.fire(regionProfile) + // dont show if it's a default (fallback) + if (regionProfile && this.profiles.length > 1) { + void vscode.window.showInformationMessage(`You are using the ${regionProfile.name} profile for Q.`).then() + } + + // persist to state + await this.persistSelectRegionProfile() + } + + restoreProfileSelection = once(async () => { + const conn = this.connectionProvider() + if (conn) { + await this.restoreRegionProfile(conn) + } + }) + + // Note: should be called after [AuthUtil.instance.conn] returns non null + async restoreRegionProfile(conn: Connection) { + const previousSelected = this.loadPersistedRegionProfle()[conn.id] || undefined + if (!previousSelected) { + return + } + // cross-validation + this.listRegionProfile() + .then(async (profiles) => { + const r = profiles.find((it) => it.arn === previousSelected.arn) + if (!r) { + telemetry.amazonq_profileState.emit({ + source: 'reload', + amazonQProfileRegion: 'not-set', + reason: 'profile could not be selected', + result: 'Failed', + }) + + await this.invalidateProfile(previousSelected.arn) + RegionProfileManager.logger.warn( + `invlaidating ${previousSelected.name} profile, arn=${previousSelected.arn}` + ) + } + }) + .catch((e) => { + telemetry.amazonq_profileState.emit({ + source: 'reload', + amazonQProfileRegion: 'not-set', + reason: (e as Error).message, + result: 'Failed', + }) + }) + + await this.switchRegionProfile(previousSelected, 'reload') + } + + private loadPersistedRegionProfle(): { [label: string]: RegionProfile } { + const previousPersistedState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( + 'aws.amazonq.regionProfiles', + Object, + {} + ) + + return previousPersistedState + } + + async persistSelectRegionProfile() { + const conn = this.connectionProvider() + + // default has empty arn and shouldn't be persisted because it's just a fallback + if (!conn || this.activeRegionProfile === undefined) { + return + } + + // persist connectionId to profileArn + const previousPersistedState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( + 'aws.amazonq.regionProfiles', + Object, + {} + ) + + previousPersistedState[conn.id] = this.activeRegionProfile + await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState) + } + + async generateQuickPickItem(): Promise[]> { + const selected = this.activeRegionProfile + let profiles: RegionProfile[] = [] + try { + profiles = await this.listRegionProfile() + } catch (e) { + return [ + { + label: '[Failed to list available profiles]', + detail: `${(e as Error).message}`, + data: '', + }, + ] + } + const icon = getIcon('vscode-account') + const quickPickItems: DataQuickPickItem[] = profiles.map((it) => { + const label = it.name + const onClick = async () => { + await this.switchRegionProfile(it, 'user') + } + const data = it.arn + const description = it.region + const isRecentlyUsed = selected ? selected.arn === it.arn : false + + return { + label: `${icon} ${label}`, + onClick: onClick, + data: data, + description: description, + recentlyUsed: isRecentlyUsed, + detail: it.description, + } + }) + + return quickPickItems + } + + async invalidateProfile(arn: string | undefined) { + if (arn) { + if (this.activeRegionProfile && this.activeRegionProfile.arn === arn) { + this._activeRegionProfile = undefined + } + + const profiles = this.loadPersistedRegionProfle() + const updatedProfiles = Object.fromEntries( + Object.entries(profiles).filter(([connId, profile]) => profile.arn !== arn) + ) + await globals.globalState.update('aws.amazonq.regionProfiles', updatedProfiles) + } + } + + async createQClient(region: string, endpoint: string, conn: SsoConnection): Promise { + const token = (await conn.getToken()).accessToken + const serviceOption: ServiceOptions = { + apiConfig: userApiConfig, + region: region, + endpoint: endpoint, + credentials: new Credentials({ accessKeyId: 'xxx', secretAccessKey: 'xxx' }), + onRequestSetup: [ + (req) => { + req.on('build', ({ httpRequest }) => { + httpRequest.headers['Authorization'] = `Bearer ${token}` + }) + }, + ], + } as ServiceOptions + + const c = (await globals.sdkClientBuilder.createAwsService( + Service, + serviceOption, + undefined + )) as CodeWhispererUserClient + + return c + } +} diff --git a/packages/core/src/codewhisperer/service/codeFixHandler.ts b/packages/core/src/codewhisperer/service/codeFixHandler.ts index 4aa74a91ac7..68dc25e7cf3 100644 --- a/packages/core/src/codewhisperer/service/codeFixHandler.ts +++ b/packages/core/src/codewhisperer/service/codeFixHandler.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CodeWhispererUserClient } from '../indexNode' +import { CodeWhispererUserClient, RegionProfile } from '../indexNode' import * as CodeWhispererConstants from '../models/constants' import { codeFixState } from '../models/model' import { ArtifactMap, CreateUploadUrlRequest, DefaultCodeWhispererClient } from '../client/codewhisperer' @@ -22,12 +22,14 @@ import { sleep } from '../../shared/utilities/timeoutUtils' export async function getPresignedUrlAndUpload( client: DefaultCodeWhispererClient, zipFilePath: string, - codeFixName: string + codeFixName: string, + profile: RegionProfile | undefined ) { const srcReq: CreateUploadUrlRequest = { artifactType: 'SourceCode', uploadIntent: CodeWhispererConstants.codeFixUploadIntent, uploadContext: { codeFixUploadContext: { codeFixName } }, + profileArn: profile?.arn, } getLogger().verbose(`Prepare for uploading src context...`) const srcResp = await client.createUploadUrl(srcReq).catch((err) => { @@ -52,7 +54,8 @@ export async function createCodeFixJob( description: string, referenceTrackerConfiguration: CodeWhispererUserClient.ReferenceTrackerConfiguration, codeFixName?: string, - ruleId?: string + ruleId?: string, + profile?: RegionProfile ) { getLogger().verbose(`Creating code fix job...`) const req: CodeWhispererUserClient.StartCodeFixJobRequest = { @@ -62,6 +65,7 @@ export async function createCodeFixJob( ruleId, description, referenceTrackerConfiguration, + profileArn: profile?.arn, } const resp = await client.startCodeFixJob(req).catch((err) => { @@ -75,7 +79,7 @@ export async function createCodeFixJob( return resp } -export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, jobId: string) { +export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, jobId: string, profile?: RegionProfile) { const pollingStartTime = performance.now() await sleep(CodeWhispererConstants.codeFixJobPollingDelayMs) @@ -85,6 +89,7 @@ export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, j throwIfCancelled() const req: CodeWhispererUserClient.GetCodeFixJobRequest = { jobId, + profileArn: profile?.arn, } const resp = await client.getCodeFixJob(req) getLogger().verbose(`GetCodeFixJobRequest requestId: ${resp.$response.requestId}`) @@ -106,9 +111,10 @@ export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, j return status } -export async function getCodeFixJob(client: DefaultCodeWhispererClient, jobId: string) { +export async function getCodeFixJob(client: DefaultCodeWhispererClient, jobId: string, profile?: RegionProfile) { const req: CodeWhispererUserClient.GetCodeFixJobRequest = { jobId, + profileArn: profile?.arn, } const resp = await client.getCodeFixJob(req) return resp diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts index 1da76995781..de78b435913 100644 --- a/packages/core/src/codewhisperer/service/recommendationService.ts +++ b/packages/core/src/codewhisperer/service/recommendationService.ts @@ -15,6 +15,7 @@ import { ClassifierTrigger } from './classifierTrigger' import { DefaultCodeWhispererClient } from '../client/codewhisperer' import { randomUUID } from '../../shared/crypto' import { TelemetryHelper } from '../util/telemetryHelper' +import { AuthUtil } from '../util/authUtil' export interface SuggestionActionEvent { readonly editor: vscode.TextEditor | undefined @@ -66,6 +67,11 @@ export class RecommendationService { autoTriggerType?: CodewhispererAutomatedTriggerType, event?: vscode.TextDocumentChangeEvent ) { + // TODO: should move all downstream auth check(inlineCompletionService, recommendationHandler etc) to here(upstream) instead of spreading everywhere + if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { + return + } + if (this._isRunning) { return } diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index 7f7a1cbf326..b83fdbebb1a 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -13,6 +13,7 @@ import { codeScanState, CodeScanStoppedError, onDemandFileScanState, + RegionProfile, } from '../models/model' import { sleep } from '../../shared/utilities/timeoutUtils' import * as codewhispererClient from '../client/codewhisperer' @@ -50,13 +51,19 @@ export async function listScanResults( codeScanFindingsSchema: string, projectPaths: string[], scope: CodeWhispererConstants.CodeAnalysisScope, - editor: vscode.TextEditor | undefined + editor: vscode.TextEditor | undefined, + profile?: RegionProfile ) { const logger = getLoggerForScope(scope) const codeScanIssueMap: Map = new Map() const aggregatedCodeScanIssueList: AggregatedCodeScanIssue[] = [] - const requester = (request: codewhispererClient.ListCodeScanFindingsRequest) => client.listCodeScanFindings(request) - const request: codewhispererClient.ListCodeScanFindingsRequest = { jobId, codeScanFindingsSchema } + const requester = (request: codewhispererClient.ListCodeScanFindingsRequest) => + client.listCodeScanFindings(request, profile?.arn) + const request: codewhispererClient.ListCodeScanFindingsRequest = { + jobId, + codeAnalysisFindingsSchema: codeScanFindingsSchema, + profileArn: profile?.arn, + } const collection = pageableToCollection(requester, request, 'nextToken') const issues = await collection .flatten() @@ -203,7 +210,8 @@ export async function pollScanJobStatus( client: DefaultCodeWhispererClient, jobId: string, scope: CodeWhispererConstants.CodeAnalysisScope, - codeScanStartTime: number + codeScanStartTime: number, + profile?: RegionProfile ) { const pollingStartTime = performance.now() // We don't expect to get results immediately, so sleep for some time initially to not make unnecessary calls @@ -216,6 +224,7 @@ export async function pollScanJobStatus( throwIfCancelled(scope, codeScanStartTime) const req: codewhispererClient.GetCodeScanRequest = { jobId: jobId, + profileArn: profile?.arn, } const resp = await client.getCodeScan(req) logger.verbose(`GetCodeScanRequest requestId: ${resp.$response.requestId}`) @@ -242,7 +251,8 @@ export async function createScanJob( artifactMap: codewhispererClient.ArtifactMap, languageId: string, scope: CodeWhispererConstants.CodeAnalysisScope, - scanName: string + scanName: string, + profile?: RegionProfile ) { const logger = getLoggerForScope(scope) logger.verbose(`Creating scan job...`) @@ -254,6 +264,7 @@ export async function createScanJob( }, scope: codeAnalysisScope, codeScanName: scanName, + profileArn: profile?.arn, } const resp = await client.createCodeScan(req).catch((err) => { getLogger().error(`Failed creating scan job. Request id: ${err.requestId}`) @@ -276,7 +287,8 @@ export async function getPresignedUrlAndUpload( client: DefaultCodeWhispererClient, zipMetadata: ZipMetadata, scope: CodeWhispererConstants.CodeAnalysisScope, - scanName: string + scanName: string, + profile?: RegionProfile ) { const artifactMap = await telemetry.amazonq_createUpload.run(async (span) => { const logger = getLoggerForScope(scope) @@ -299,6 +311,7 @@ export async function getPresignedUrlAndUpload( codeScanName: scanName, }, }, + profileArn: profile?.arn, } logger.verbose(`Prepare for uploading src context...`) const srcResp = await client.createUploadUrl(srcReq).catch((err) => { diff --git a/packages/core/src/codewhisperer/service/testGenHandler.ts b/packages/core/src/codewhisperer/service/testGenHandler.ts index 3f4825d37ca..8573dd0dcc6 100644 --- a/packages/core/src/codewhisperer/service/testGenHandler.ts +++ b/packages/core/src/codewhisperer/service/testGenHandler.ts @@ -23,7 +23,7 @@ import { TestGenTimedOutError, } from '../../amazonqTest/error' import { getMd5, uploadArtifactToS3 } from './securityScanHandler' -import { testGenState, Reference } from '../models/model' +import { testGenState, Reference, RegionProfile } from '../models/model' import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient' import { downloadExportResultArchive } from '../../shared/utilities/download' @@ -76,7 +76,8 @@ export async function createTestJob( artifactMap: codewhispererClient.ArtifactMap, relativeTargetPath: TargetCode[], userInputPrompt: string, - clientToken?: string + clientToken?: string, + profile?: RegionProfile ) { const logger = getLogger() logger.verbose(`Creating test job and starting startTestGeneration...`) @@ -96,6 +97,7 @@ export async function createTestJob( userInput: userInputPrompt, testGenerationJobGroupName: ChatSessionManager.Instance.getSession().testGenerationJobGroupName ?? randomUUID(), // TODO: remove fallback clientToken, + profileArn: profile?.arn, } logger.debug('Unit test generation request body: %O', req) logger.debug('target code list: %O', req.targetCodeList[0]) @@ -130,7 +132,8 @@ export async function pollTestJobStatus( jobId: string, jobGroupName: string, filePath: string, - initialExecution: boolean + initialExecution: boolean, + profile?: RegionProfile ) { const session = ChatSessionManager.Instance.getSession() const pollingStartTime = performance.now() @@ -145,6 +148,7 @@ export async function pollTestJobStatus( const req: CodeWhispererUserClient.GetTestGenerationRequest = { testGenerationJobId: jobId, testGenerationJobGroupName: jobGroupName, + profileArn: profile?.arn, } const resp = await codewhispererClient.codeWhispererClient.getTestGeneration(req) logger.verbose('pollTestJobStatus request id: %s', resp.$response.requestId) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index 37643263861..4965637a416 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -14,6 +14,7 @@ import { HilZipManifest, IHilZipManifestParams, jobPlanProgress, + RegionProfile, sessionJobHistory, StepProgress, TransformationType, @@ -427,7 +428,7 @@ export async function zipCode( return { dependenciesCopied: dependenciesCopied, tempFilePath: tempFilePath, fileSize: zipSize } as ZipCodeResult } -export async function startJob(uploadId: string) { +export async function startJob(uploadId: string, profile: RegionProfile | undefined) { const sourceLanguageVersion = `JAVA_${transformByQState.getSourceJDKVersion()}` const targetLanguageVersion = `JAVA_${transformByQState.getTargetJDKVersion()}` try { @@ -441,6 +442,7 @@ export async function startJob(uploadId: string) { source: { language: sourceLanguageVersion }, // dummy value of JDK8 used for SQL conversions just so that this API can be called target: { language: targetLanguageVersion }, // JAVA_17 or JAVA_21 }, + profileArn: profile?.arn, }) getLogger().info('CodeTransformation: called startJob API successfully') return response.transformationJobId @@ -564,11 +566,12 @@ export function getJobStatisticsHtml(jobStatistics: any) { return htmlString } -export async function getTransformationPlan(jobId: string) { +export async function getTransformationPlan(jobId: string, profile: RegionProfile | undefined) { let response = undefined try { response = await codeWhisperer.codeWhispererClient.codeModernizerGetCodeTransformationPlan({ transformationJobId: jobId, + profileArn: profile?.arn, }) const stepZeroProgressUpdates = response.transformationPlan.transformationSteps[0].progressUpdates @@ -624,7 +627,11 @@ export async function getTransformationPlan(jobId: string) { } } -export async function getTransformationSteps(jobId: string, handleThrottleFlag: boolean) { +export async function getTransformationSteps( + jobId: string, + handleThrottleFlag: boolean, + profile: RegionProfile | undefined +) { try { // prevent ThrottlingException if (handleThrottleFlag) { @@ -632,6 +639,7 @@ export async function getTransformationSteps(jobId: string, handleThrottleFlag: } const response = await codeWhisperer.codeWhispererClient.codeModernizerGetCodeTransformationPlan({ transformationJobId: jobId, + profileArn: profile?.arn, }) return response.transformationPlan.transformationSteps.slice(1) // skip step 0 (contains supplemental info) } catch (e: any) { @@ -641,13 +649,14 @@ export async function getTransformationSteps(jobId: string, handleThrottleFlag: } } -export async function pollTransformationJob(jobId: string, validStates: string[]) { +export async function pollTransformationJob(jobId: string, validStates: string[], profile: RegionProfile | undefined) { let status: string = '' while (true) { throwIfCancelled() try { const response = await codeWhisperer.codeWhispererClient.codeModernizerGetCodeTransformation({ transformationJobId: jobId, + profileArn: profile?.arn, }) status = response.transformationJob.status! if (CodeWhispererConstants.validStatesForBuildSucceeded.includes(status)) { diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 63eae8606bb..2d0585085a9 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -23,6 +23,7 @@ import { import { startInterval } from '../../commands/startTransformByQ' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { convertToTimeString } from '../../../shared/datetime' +import { AuthUtil } from '../../util/authUtil' export class TransformationHubViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.amazonq.transformationHub' @@ -325,7 +326,8 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider jobPlanProgress['generatePlan'] === StepProgress.Succeeded && transformByQState.isRunning() ) { - planSteps = await getTransformationSteps(transformByQState.getJobId(), false) + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile + planSteps = await getTransformationSteps(transformByQState.getJobId(), false, profile) transformByQState.setPlanSteps(planSteps) } let progressHtml diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts index 39416eafe70..0989f022245 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts @@ -145,6 +145,7 @@ export class CodeWhispererCodeCoverageTracker { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { diff --git a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts index dbcfb3ab134..ca19c87505f 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts @@ -161,6 +161,7 @@ export class CodeWhispererTracker { ), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts index 5d8382cbec9..d804cbedd46 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -18,6 +18,7 @@ import { signoutCodeWhisperer, showIntroduction, toggleCodeScans, + selectRegionProfileCommand, } from '../commands/basicCommands' import { CodeWhispererCommandDeclarations } from '../commands/gettingStartedPageCommands' import { CodeScansState, codeScanState } from '../models/model' @@ -137,6 +138,23 @@ export function createSelectCustomization(): DataQuickPickItem<'selectCustomizat } as DataQuickPickItem<'selectCustomization'> } +export function createSelectRegionProfileNode(): DataQuickPickItem<'selectRegionProfile'> { + const selectedRegionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile + + const label = 'Change Profile' + const icon = getIcon('vscode-arrow-swap') + const description = selectedRegionProfile ? `Current profile: ${selectedRegionProfile.name}` : '' + + return { + data: 'selectRegionProfile', + label: codicon`${icon} ${label}`, + onClick: async () => { + await selectRegionProfileCommand.execute(placeholder, cwQuickPickSource) + }, + description: description, + } +} + /* Opens the Learn CodeWhisperer Page */ export function createGettingStarted(): DataQuickPickItem<'gettingStarted'> { const label = localize('AWS.codewhisperer.gettingStartedNode.label', 'Try inline suggestion examples') diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index 821e5281790..9c7ea8c43ec 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -21,6 +21,7 @@ import { createSignIn, switchToAmazonQNode, createSecurityScan, + createSelectRegionProfileNode, } from './codeWhispererNodes' import { hasVendedIamCredentials, hasVendedCredentialsFromMetadata } from '../../auth/auth' import { AuthUtil } from '../util/authUtil' @@ -42,6 +43,10 @@ function getAmazonQCodeWhispererNodes() { return [createSignIn(), createLearnMore()] } + if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { + return [] + } + if (vsCodeState.isFreeTierLimitReached) { if (hasVendedIamCredentials()) { return [createFreeTierLimitMet(), createOpenReferenceLog()] @@ -92,6 +97,7 @@ export function getQuickPickItems(): DataQuickPickItem[] { // Add settings and signout createSeparator(), createSettingsNode(), + ...(AuthUtil.instance.isValidEnterpriseSsoInUse() ? [createSelectRegionProfileNode()] : []), ...(AuthUtil.instance.isConnected() && !hasVendedIamCredentials() && !hasVendedCredentialsFromMetadata() ? [createSignout()] : []), diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index 1e350384e73..0898493b6db 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -45,7 +45,7 @@ import { asStringifiedStack } from '../../shared/telemetry/spans' import { withTelemetryContext } from '../../shared/telemetry/util' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { throttle } from 'lodash' - +import { RegionProfileManager } from '../region/regionProfileManager' /** Backwards compatibility for connections w pre-chat scopes */ export const codeWhispererCoreScopes = [...scopesCodeWhispererCore] export const codeWhispererChatScopes = [...codeWhispererCoreScopes, ...scopesCodeWhispererChat] @@ -105,7 +105,10 @@ export class AuthUtil { ) public readonly restore = () => this.secondaryAuth.restoreConnection() - public constructor(public readonly auth = Auth.instance) {} + public constructor( + public readonly auth = Auth.instance, + public readonly regionProfileManager = new RegionProfileManager(() => this.conn) + ) {} public initCodeWhispererHooks = once(() => { this.auth.onDidChangeConnectionState(async (e) => { @@ -121,6 +124,7 @@ export class AuthUtil { getLogger().info(`codewhisperer: active connection changed`) if (this.isValidEnterpriseSsoInUse()) { void vscode.commands.executeCommand('aws.amazonq.notifyNewCustomizations') + await this.regionProfileManager.restoreProfileSelection() } vsCodeState.isFreeTierLimitReached = false await Promise.all([ @@ -135,14 +139,26 @@ export class AuthUtil { if (this.isValidEnterpriseSsoInUse() || (this.isBuilderIdInUse() && !this.isConnectionExpired())) { await showAmazonQWalkthroughOnce() } + + if (!this.isConnected()) { + await this.regionProfileManager.invalidateProfile(this.regionProfileManager.activeRegionProfile?.arn) + } + }) + + this.regionProfileManager.onDidChangeRegionProfile(async () => { + await this.setVscodeContextProps() }) }) public async setVscodeContextProps() { - await setContext('aws.codewhisperer.connected', this.isConnected()) - const doShowAmazonQLoginView = !this.isConnected() || this.isConnectionExpired() + // if users are "pending profile selection", they're not fully connected and require profile selection for Q usage + // requireProfileSelection() always returns false for builderID users + await setContext('aws.codewhisperer.connected', this.isConnected() && !this.requireProfileSelection()) + const doShowAmazonQLoginView = + !this.isConnected() || this.isConnectionExpired() || this.requireProfileSelection() await setContext('aws.amazonq.showLoginView', doShowAmazonQLoginView) await setContext('aws.codewhisperer.connectionExpired', this.isConnectionExpired()) + await setContext('aws.amazonq.connectedSsoIdc', isIdcSsoConnection(this.conn)) } public reformatStartUrl(startUrl: string | undefined) { @@ -293,6 +309,13 @@ export class AuthUtil { return connectionExpired } + requireProfileSelection(): boolean { + if (isBuilderIdConnection(this.conn)) { + return false + } + return isIdcSsoConnection(this.conn) && this.regionProfileManager.activeRegionProfile === undefined + } + private logConnection() { const logStr = indent( `codewhisperer: connection states @@ -458,12 +481,23 @@ export class AuthUtil { } if (isBuilderIdConnection(conn) || isIdcSsoConnection(conn) || isSageMaker()) { + // TODO: refactor if (isValidCodeWhispererCoreConnection(conn)) { - state[Features.codewhispererCore] = AuthStates.connected + if (this.requireProfileSelection()) { + state[Features.codewhispererCore] = AuthStates.pendingProfileSelection + } else { + state[Features.codewhispererCore] = AuthStates.connected + } } if (isValidAmazonQConnection(conn)) { - for (const v of Object.values(Features)) { - state[v as Feature] = AuthStates.connected + if (this.requireProfileSelection()) { + for (const v of Object.values(Features)) { + state[v as Feature] = AuthStates.pendingProfileSelection + } + } else { + for (const v of Object.values(Features)) { + state[v as Feature] = AuthStates.connected + } } } } @@ -532,6 +566,7 @@ export const AuthStates = { * but fetching/refreshing the token resulted in a network error. */ connectedWithNetworkError: 'connectedWithNetworkError', + pendingProfileSelection: 'pendingProfileSelection', } as const const Features = { codewhispererCore: 'codewhispererCore', diff --git a/packages/core/src/codewhisperer/util/editorContext.ts b/packages/core/src/codewhisperer/util/editorContext.ts index 11598cfe20c..4e2173a043e 100644 --- a/packages/core/src/codewhisperer/util/editorContext.ts +++ b/packages/core/src/codewhisperer/util/editorContext.ts @@ -18,6 +18,7 @@ import { checkLeftContextKeywordsForJson } from './commonUtil' import { CodeWhispererSupplementalContext } from '../models/model' import { getOptOutPreference } from '../../shared/telemetry/util' import { indent } from '../../shared/utilities/textUtilities' +import { AuthUtil } from './authUtil' let tabSize: number = getTabSizeSetting() @@ -108,6 +109,8 @@ export async function buildListRecommendationRequest( }) : [] + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile + return { request: { fileContext: fileContext, @@ -118,6 +121,7 @@ export async function buildListRecommendationRequest( supplementalContexts: supplementalContext, customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, optOutPreference: getOptOutPreference(), + profileArn: profile?.arn, }, supplementalMetadata: supplementalContexts, } diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index ced228dba51..4bb3b92dc33 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -160,6 +160,7 @@ export class TelemetryHelper { supplementalContextMetadata?: CodeWhispererSupplementalContext | undefined ) { const selectedCustomization = getSelectedCustomization() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile telemetry.codewhisperer_userTriggerDecision.emit({ codewhispererAutomatedTriggerType: session.autoTriggerType, @@ -218,6 +219,7 @@ export class TelemetryHelper { acceptedCharacterCount: 0, }, }, + profileArn: profile?.arn, }) .then() .catch((error) => { @@ -364,6 +366,7 @@ export class TelemetryHelper { const aggregatedCompletionType = this.sessionDecisions[0].codewhispererCompletionType const aggregatedSuggestionState = this.getAggregatedSuggestionState(this.sessionDecisions) const selectedCustomization = getSelectedCustomization() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const generatedLines = acceptedRecommendationContent.trim() === '' ? 0 : acceptedRecommendationContent.split('\n').length const suggestionCount = this.sessionDecisions @@ -443,6 +446,7 @@ export class TelemetryHelper { acceptedCharacterCount: acceptedRecommendationContent.length, }, }, + profileArn: profile?.arn, }) .then() .catch((error) => { @@ -658,6 +662,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -693,6 +698,7 @@ export class TelemetryHelper { codeAnalysisScope: scope === CodeAnalysisScopeClientSide.FILE_AUTO ? 'FILE' : 'PROJECT', }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -722,6 +728,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -759,6 +766,7 @@ export class TelemetryHelper { charsOfCodeGenerated, }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -796,6 +804,7 @@ export class TelemetryHelper { charsOfCodeAccepted, }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -841,6 +850,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -889,6 +899,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index 53551b254f5..10409be4ada 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -14,7 +14,7 @@ import { ToolkitError } from '../../../../shared/errors' import { createCodeWhispererChatStreamingClient } from '../../../../shared/clients/codewhispererChatClient' import { createQDeveloperStreamingClient } from '../../../../shared/clients/qDeveloperChatClient' import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWrittenCodeTracker' -import { PromptMessage } from '../../../controllers/chat/model' +import { DocumentReference, PromptMessage } from '../../../controllers/chat/model' import { FsWriteBackup } from '../../../../codewhispererChat/tools/fsWrite' export type ToolUseWithError = { @@ -30,8 +30,10 @@ export class ChatSession { * _readFiles = list of files read from the project to gather context before generating response. * _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user * _context = Additional context to be passed to the LLM for generating the response + * _messageIdToUpdate = messageId of a chat message to be updated, used for reducing consecutive tool messages */ - private _readFiles: string[] = [] + private _readFiles: DocumentReference[] = [] + private _readFolders: DocumentReference[] = [] private _toolUseWithError: ToolUseWithError | undefined private _showDiffOnFileWrite: boolean = false private _context: PromptMessage['context'] @@ -41,6 +43,8 @@ export class ChatSession { * True if messages from local history have been sent to session. */ localHistoryHydrated: boolean = false + private _messageIdToUpdate: string | undefined + private _messageIdToUpdateListDirectory: string | undefined contexts: Map = new Map() // TODO: doesn't handle the edge case when two files share the same relativePath string but from different root @@ -49,6 +53,21 @@ export class ChatSession { public get sessionIdentifier(): string | undefined { return this.sessionId } + public get messageIdToUpdate(): string | undefined { + return this._messageIdToUpdate + } + + public setMessageIdToUpdate(messageId: string | undefined) { + this._messageIdToUpdate = messageId + } + + public get messageIdToUpdateListDirectory(): string | undefined { + return this._messageIdToUpdateListDirectory + } + + public setMessageIdToUpdateListDirectory(messageId: string | undefined) { + this._messageIdToUpdateListDirectory = messageId + } public get pairProgrammingModeOn(): boolean { return this._pairProgrammingModeOn @@ -95,21 +114,30 @@ export class ChatSession { public setSessionID(id?: string) { this.sessionId = id } - public get readFiles(): string[] { + public get readFiles(): DocumentReference[] { return this._readFiles } + public get readFolders(): DocumentReference[] { + return this._readFolders + } public get showDiffOnFileWrite(): boolean { return this._showDiffOnFileWrite } public setShowDiffOnFileWrite(value: boolean) { this._showDiffOnFileWrite = value } - public addToReadFiles(filePath: string) { + public addToReadFiles(filePath: DocumentReference) { this._readFiles.push(filePath) } public clearListOfReadFiles() { this._readFiles = [] } + public setReadFolders(folder: DocumentReference) { + this._readFolders.push(folder) + } + public clearListOfReadFolders() { + this._readFolders = [] + } async chatIam(chatRequest: SendMessageRequest): Promise { const client = await createQDeveloperStreamingClient() diff --git a/packages/core/src/codewhispererChat/commands/registerCommands.ts b/packages/core/src/codewhispererChat/commands/registerCommands.ts index dd9f50d7a48..39d8383c867 100644 --- a/packages/core/src/codewhispererChat/commands/registerCommands.ts +++ b/packages/core/src/codewhispererChat/commands/registerCommands.ts @@ -7,7 +7,6 @@ import { commandPalette } from '../../codewhisperer/commands/types' import { CodeScanIssue } from '../../codewhisperer/models/model' import { Commands, VsCodeCommandArg, placeholder } from '../../shared/vscode/commands2' import { ChatControllerMessagePublishers } from '../controllers/chat/controller' -import vscode from 'vscode' /** * Opens the Amazon Q panel, showing the correct View that should @@ -25,8 +24,8 @@ export const focusAmazonQPanel = Commands.declare( * So when we try to focus the following Views, only one will show depending * on the context. */ - await vscode.commands.executeCommand('aws.amazonq.AmazonQChatView.focus') - await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus') + await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus') + await Commands.tryExecute('aws.amazonq.AmazonCommonAuth.focus') } ) diff --git a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts index 5c676d3f340..cbf7a5228f2 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts @@ -33,7 +33,10 @@ export const supportedLanguagesList = [ export const filePathSizeLimit = 4_000 -export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { conversationState: ConversationState } { +export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { + conversationState: ConversationState + profileArn?: string +} { // Flexible truncation logic const remainingPayloadSize = 100_000 @@ -182,6 +185,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c customizationArn: customizationArn, history: history || undefined, }, + profileArn: triggerPayload.profile?.arn, } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 1f75f33c5ed..379aed50b7e 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -97,7 +97,6 @@ import { OutputKind } from '../../tools/toolShared' import { ToolUtils, Tool, ToolType } from '../../tools/toolUtils' import { ChatStream } from '../../tools/chatStream' import { ChatHistoryStorage } from '../../storages/chatHistoryStorage' -import { FsWriteParams } from '../../tools/fsWrite' import { tempDirPath } from '../../../shared/filesystemUtilities' import { Database } from '../../../shared/db/chatDb/chatDb' import { TabBarController } from './tabBarController' @@ -177,6 +176,7 @@ export class ChatController { private userPromptsWatcher: vscode.FileSystemWatcher | undefined private readonly chatHistoryStorage: ChatHistoryStorage private chatHistoryDb = Database.getInstance() + private cancelTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource() public constructor( private readonly chatControllerMessageListeners: ChatControllerMessageListeners, @@ -205,6 +205,10 @@ export class ChatController { } }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + this.cancelTokenSource.cancel() + }) + this.chatControllerMessageListeners.processPromptChatMessage.onMessage((data) => { const uiEvents = uiEventRecorder.get(data.tabID) if (uiEvents) { @@ -318,6 +322,9 @@ export class ChatController { this.chatControllerMessageListeners.processDetailedListItemSelectMessage.onMessage((data) => { return this.tabBarController.processItemSelectMessage(data) }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + this.sessionStorage.deleteAllSessions() + }) } private registerUserPromptsWatcher() { @@ -716,9 +723,18 @@ export class ChatController { try { await ToolUtils.validate(tool) - const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, { - requiresAcceptance: false, - }) + const chatStream = new ChatStream( + this.messenger, + tabID, + triggerID, + toolUse, + session, + undefined, + false, + { + requiresAcceptance: false, + } + ) if (tool.type === ToolType.FsWrite && toolUse.toolUseId) { const backup = await tool.tool.getBackup() session.setFsWriteBackup(toolUse.toolUseId, backup) @@ -746,13 +762,6 @@ export class ChatController { const toolResult: ToolResult = result toolResults.push(toolResult) } - - if (toolUse.name === ToolType.FsWrite) { - await vscode.commands.executeCommand( - 'vscode.open', - vscode.Uri.file((toolUse.input as unknown as FsWriteParams).path) - ) - } } await this.generateResponse( @@ -769,6 +778,7 @@ export class ChatController { userIntent: undefined, customization: getSelectedCustomization(), toolResults: toolResults, + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, origin: Origin.IDE, context: session.context ?? [], relevantTextDocuments: [], @@ -787,11 +797,26 @@ export class ChatController { }) } - private async closeDiffView() { - // Close the diff view if User reject the generated code changes. + private async closeDiffView(message: CustomFormActionMessage) { + // Close the diff view if User rejected or accepted the generated code changes. if (vscode.window.tabGroups.activeTabGroup.activeTab?.label.includes(amazonQTabSuffix)) { await vscode.commands.executeCommand('workbench.action.closeActiveEditor') } + // clean up temp file + const tabID = message.tabID + const toolUseId = message.action.formItemValues?.toolUseId + if (!tabID || !toolUseId) { + return + } + + const session = this.sessionStorage.getSession(tabID) + const { filePath } = session.fsWriteBackups.get(toolUseId) ?? {} + if (filePath) { + const tempFilePath = await this.getTempFilePath(filePath) + if (await fs.existsFile(tempFilePath)) { + await fs.delete(tempFilePath) + } + } } private async rejectShellCommand(message: CustomFormActionMessage) { @@ -812,10 +837,7 @@ export class ChatController { getLogger().error( `toolUse name: ${currentToolUse!.name} of toolUseWithError in the stored session doesn't match when click shell command reject button.` ) - return } - - await this.generateStaticTextResponse('reject-shell-command', triggerId) } private async processCustomFormAction(message: CustomFormActionMessage) { @@ -828,11 +850,11 @@ export class ChatController { await this.processToolUseMessage(message) break case 'accept-code-diff': - await this.closeDiffView() + await this.closeDiffView(message) break case 'reject-code-diff': await this.restoreBackup(message) - await this.closeDiffView() + await this.closeDiffView(message) break case 'reject-shell-command': await this.rejectShellCommand(message) @@ -876,6 +898,21 @@ export class ChatController { } } + private async getTempFilePath(filePath: string) { + // Create a temporary file path to show the diff view + const pathToArchiveDir = path.join(tempDirPath, 'q-chat') + const archivePathExists = await fs.existsDir(pathToArchiveDir) + if (!archivePathExists) { + await fs.mkdir(pathToArchiveDir) + } + const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts') + const resultArtifactsDirExists = await fs.existsDir(resultArtifactsDir) + if (!resultArtifactsDirExists) { + await fs.mkdir(resultArtifactsDir) + } + return path.join(resultArtifactsDir, `temp-${path.basename(filePath)}`) + } + private async processFileClickMessage(message: FileClick) { const session = this.sessionStorage.getSession(message.tabID) // Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly. @@ -887,18 +924,7 @@ export class ChatController { } try { - // Create a temporary file path to show the diff view - // TODO: Use amazonQDiffScheme for temp file - const pathToArchiveDir = path.join(tempDirPath, 'q-chat') - const archivePathExists = await fs.existsDir(pathToArchiveDir) - if (archivePathExists) { - await fs.delete(pathToArchiveDir, { recursive: true }) - } - await fs.mkdir(pathToArchiveDir) - const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts') - await fs.mkdir(resultArtifactsDir) - - const tempFilePath = path.join(resultArtifactsDir, `temp-${path.basename(filePath)}`) + const tempFilePath = await this.getTempFilePath(filePath) await fs.writeFile(tempFilePath, content) const leftUri = vscode.Uri.file(tempFilePath) @@ -1066,6 +1092,7 @@ export class ChatController { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromContextMenuCommand(command), customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, additionalContents: [], relevantTextDocuments: [], documentReferences: [], @@ -1153,6 +1180,7 @@ export class ChatController { codeQuery: lastTriggerEvent.context?.focusAreaContext?.names, userIntent: message.userIntent, customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, contextLengths: { ...defaultContextLengths, }, @@ -1172,6 +1200,7 @@ export class ChatController { private async processPromptMessageAsNewThread(message: PromptMessage) { const session = this.sessionStorage.getSession(message.tabID) session.clearListOfReadFiles() + session.clearListOfReadFolders() session.setShowDiffOnFileWrite(false) this.editorContextExtractor .extractContextForTrigger('ChatMessage') @@ -1201,6 +1230,7 @@ export class ChatController { userIntent: undefined, customization: getSelectedCustomization(), origin: Origin.IDE, + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, context: message.context ?? [], relevantTextDocuments: [], additionalContents: [], @@ -1486,8 +1516,9 @@ export class ChatController { ) let response: MessengerResponseType | undefined = undefined session.createNewTokenSource() + // TODO: onProfileChanged, abort previous response? try { - if (!session.context) { + if (!session.context && triggerPayload.context.length) { // Only show context for the first message in the loop this.messenger.sendContextMessage(tabID, triggerID, triggerPayload.documentReferences) session.setContext(triggerPayload.context) @@ -1526,7 +1557,17 @@ export class ChatController { response.$metadata.requestId } metadata: ${inspect(response.$metadata, { depth: 12 })}` ) - await this.messenger.sendAIResponse(response, session, tabID, triggerID, triggerPayload, chatHistory) + + this.cancelTokenSource = new vscode.CancellationTokenSource() + await this.messenger.sendAIResponse( + response, + session, + tabID, + triggerID, + triggerPayload, + chatHistory, + this.cancelTokenSource.token + ) // Turn off AgentLoop flag after sending the AI response this.sessionStorage.setAgentLoopInProgress(tabID, false) diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index ff079e422a6..930ae133c17 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as vscode from 'vscode' import { waitUntil } from '../../../../shared/utilities/timeoutUtils' import { AppToWebViewMessageDispatcher, @@ -21,6 +22,7 @@ import { CloseDetailedListMessage, SelectTabMessage, ChatItemHeader, + ToolMessage, } from '../../../view/connector/connector' import { EditorContextCommandType } from '../../../commands/registerCommands' import { ChatResponseStream as qdevChatResponseStream } from '@amzn/amazon-q-developer-streaming-client' @@ -68,13 +70,10 @@ import { FsWriteParams } from '../../../tools/fsWrite' import { AsyncEventProgressMessage } from '../../../../amazonq/commons/connector/connectorMessages' import { localize } from '../../../../shared/utilities/vsCodeUtils' import { getDiffLinesFromChanges } from '../../../../shared/utilities/diffUtils' +import { FsReadParams } from '../../../tools/fsRead' +import { ListDirectoryParams } from '../../../tools/listDirectory' -export type StaticTextResponseType = - | 'quick-action-help' - | 'onboarding-help' - | 'transform' - | 'help' - | 'reject-shell-command' +export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' export type MessengerResponseType = { $metadata: { requestId?: string; httpStatusCode?: number } @@ -135,7 +134,7 @@ export class Messenger { new ChatMessage( { message: '', - messageType: 'answer', + messageType: 'answer-stream', followUps: undefined, followUpsHeader: undefined, relatedSuggestions: undefined, @@ -144,7 +143,8 @@ export class Messenger { userIntent: undefined, codeBlockLanguage: undefined, contextList: mergedRelevantDocuments, - title: 'Context', + title: '', + rootFolderTitle: 'Context', buttons: undefined, fileList: undefined, canBeVoted: false, @@ -188,7 +188,8 @@ export class Messenger { tabID: string, triggerID: string, triggerPayload: TriggerPayload, - chatHistoryManager: ChatHistoryManager + chatHistoryManager: ChatHistoryManager, + cancelToken: vscode.CancellationToken ) { let message = '' const messageID = response.$metadata.requestId ?? '' @@ -228,6 +229,9 @@ export class Messenger { await waitUntil( async () => { for await (const chatEvent of response.message!) { + if (cancelToken.isCancellationRequested) { + return + } for (const key of keys(chatEvent)) { if ((chatEvent[key] as any) !== undefined) { eventCounts.set(key, (eventCounts.get(key) ?? 0) + 1) @@ -265,12 +269,11 @@ export class Messenger { } if (cwChatEvent.toolUseEvent?.stop) { - toolUse.input = JSON.parse(toolUseInput) - toolUse.toolUseId = cwChatEvent.toolUseEvent.toolUseId ?? '' - toolUse.name = cwChatEvent.toolUseEvent.name ?? '' - let toolError = undefined try { + toolUse.toolUseId = cwChatEvent.toolUseEvent.toolUseId ?? '' + toolUse.name = cwChatEvent.toolUseEvent.name ?? '' + toolUse.input = JSON.parse(toolUseInput) const availableToolsNames = (session.pairProgrammingModeOn ? tools : noWriteTools).map( (item) => item.toolSpecification?.name ) @@ -284,16 +287,51 @@ export class Messenger { session.setShowDiffOnFileWrite(true) changeList = await tool.tool.getDiffChanges() } + if (tool.type === ToolType.FsRead) { + const input = toolUse.input as unknown as FsReadParams + // Check if this file path is already in the readFiles list + const isFileAlreadyRead = session.readFiles.some( + (file) => file.relativeFilePath === input.path + ) + if (!isFileAlreadyRead) { + session.addToReadFiles({ + relativeFilePath: input?.path, + lineRanges: [{ first: -1, second: -1 }], + }) + } + } else if (tool.type === ToolType.ListDirectory) { + const input = toolUse.input as unknown as ListDirectoryParams + session.setReadFolders({ + relativeFilePath: input?.path, + lineRanges: [{ first: -1, second: -1 }], + }) + } const validation = ToolUtils.requiresAcceptance(tool) const chatStream = new ChatStream( this, tabID, triggerID, toolUse, + session, + tool.type === ToolType.FsRead + ? session.messageIdToUpdate + : session.messageIdToUpdateListDirectory, + true, validation, changeList ) await ToolUtils.queueDescription(tool, chatStream) + if (session.messageIdToUpdate === undefined && tool.type === ToolType.FsRead) { + // Store the first messageId in a chain of tool uses + session.setMessageIdToUpdate(toolUse.toolUseId) + } + + if ( + session.messageIdToUpdateListDirectory === undefined && + tool.type === ToolType.ListDirectory + ) { + session.setMessageIdToUpdateListDirectory(toolUse.toolUseId) + } if (!validation.requiresAcceptance) { // Need separate id for read tool and safe bash command execution as 'run-shell-command' id is required to state in cwChatConnector.ts which will impact generic tool execution. @@ -457,11 +495,16 @@ export class Messenger { ) } + const agenticLoopEnded = !eventCounts.has('toolUseEvent') + if (agenticLoopEnded) { + // Reset context for the next request + session.setContext(undefined) + } this.dispatcher.sendChatMessage( new ChatMessage( { message: undefined, - messageType: 'answer', + messageType: agenticLoopEnded ? 'answer' : 'answer-stream', followUps: followUps, followUpsHeader: undefined, relatedSuggestions: undefined, @@ -485,11 +528,6 @@ export class Messenger { toolUse.input !== '' && { toolUses: [{ ...toolUse }] }), }, }) - const agenticLoopEnded = !eventCounts.has('toolUseEvent') - if (agenticLoopEnded) { - // Reset context for the next request - session.setContext(undefined) - } getLogger().info( `All events received. requestId=%s counts=%s`, @@ -513,6 +551,26 @@ export class Messenger { }) } + public sendInitialToolMessage(tabID: string, triggerID: string, toolUseId: string | undefined) { + this.dispatcher.sendChatMessage( + new ChatMessage( + { + message: '', + messageType: 'answer', + followUps: undefined, + followUpsHeader: undefined, + relatedSuggestions: undefined, + triggerID, + messageID: toolUseId ?? 'toolUse', + userIntent: undefined, + codeBlockLanguage: undefined, + contextList: undefined, + }, + tabID + ) + ) + } + public sendErrorMessage(errorMessage: string | undefined, tabID: string, requestID: string | undefined) { this.showChatExceptionMessage( { @@ -525,37 +583,85 @@ export class Messenger { ) } + private sendReadAndListDirToolMessage( + toolUse: ToolUse, + session: ChatSession, + tabID: string, + triggerID: string, + messageIdToUpdate?: string + ) { + const contextList = toolUse.name === ToolType.ListDirectory ? session.readFolders : session.readFiles + const isFileRead = toolUse.name === ToolType.FsRead + const items = isFileRead ? session.readFiles : session.readFolders + const itemCount = items.length + + const title = + itemCount < 1 + ? 'Gathering context' + : isFileRead + ? `${itemCount} file${itemCount > 1 ? 's' : ''} read` + : `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` + + this.dispatcher.sendToolMessage( + new ToolMessage( + { + message: '', + messageType: 'answer-part', + followUps: undefined, + followUpsHeader: undefined, + relatedSuggestions: undefined, + triggerID, + messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '', + userIntent: undefined, + codeBlockLanguage: undefined, + contextList, + canBeVoted: false, + buttons: undefined, + fullWidth: false, + padding: false, + codeBlockActions: undefined, + rootFolderTitle: title, + }, + tabID + ) + ) + } + public sendPartialToolLog( message: string, tabID: string, triggerID: string, toolUse: ToolUse | undefined, + session: ChatSession, + messageIdToUpdate: string | undefined, validation: CommandValidation, changeList?: Change[] ) { + // Handle read tool and list directory messages + if (toolUse?.name === ToolType.FsRead || toolUse?.name === ToolType.ListDirectory) { + return this.sendReadAndListDirToolMessage(toolUse, session, tabID, triggerID, messageIdToUpdate) + } + + // Handle file write tool, execute bash tool and bash command output log messages const buttons: ChatItemButton[] = [] let header: ChatItemHeader | undefined = undefined - let fullWidth: boolean | undefined = undefined - let padding: boolean | undefined = undefined - let codeBlockActions: ChatItemContent['codeBlockActions'] = undefined if (toolUse?.name === ToolType.ExecuteBash && message.startsWith('```shell')) { if (validation.requiresAcceptance) { const buttons: ChatItemButton[] = [ - { - id: 'run-shell-command', - text: localize('AWS.amazonq.executeBash.run', 'Run'), - status: 'main', - icon: 'play' as MynahIconsType, - }, { id: 'reject-shell-command', text: localize('AWS.amazonq.executeBash.reject', 'Reject'), status: 'clear', icon: 'cancel' as MynahIconsType, }, + { + id: 'run-shell-command', + text: localize('AWS.amazonq.executeBash.run', 'Run'), + status: 'clear', + icon: 'play' as MynahIconsType, + }, ] header = { - icon: 'code-block' as MynahIconsType, body: 'shell', buttons, } @@ -563,10 +669,6 @@ export class Messenger { if (validation.warning) { message = validation.warning + message } - fullWidth = true - padding = false - // eslint-disable-next-line unicorn/no-null - codeBlockActions = { 'insert-to-cursor': null, copy: null } } else if (toolUse?.name === ToolType.FsWrite) { const input = toolUse.input as unknown as FsWriteParams const fileName = path.basename(input.path) @@ -596,14 +698,9 @@ export class Messenger { }, ] header = { - icon: 'code-block' as MynahIconsType, buttons, fileList, } - fullWidth = true - padding = false - // eslint-disable-next-line unicorn/no-null - codeBlockActions = { 'insert-to-cursor': null, copy: null } } this.dispatcher.sendChatMessage( @@ -619,12 +716,14 @@ export class Messenger { userIntent: undefined, codeBlockLanguage: undefined, contextList: undefined, + title: undefined, canBeVoted: false, buttons, - fullWidth, - padding, + fullWidth: true, + padding: false, header, - codeBlockActions, + // eslint-disable-next-line unicorn/no-null + codeBlockActions: { 'insert-to-cursor': null, copy: null }, }, tabID ) @@ -676,10 +775,6 @@ export class Messenger { ] followUpsHeader = 'Try Examples:' break - case 'reject-shell-command': - // need to update the string later - message = 'The shell command execution rejected. Abort.' - break } this.dispatcher.sendChatMessage( @@ -754,6 +849,20 @@ export class Messenger { } private showChatExceptionMessage(e: ChatException, tabID: string, requestID: string | undefined) { + const title = 'An error occurred while processing your request.' + // TODO: once the server sends the correct exception back, fix this + if (e.statusCode && e.statusCode === '500') { + // Send throttling message + this.dispatcher.sendErrorMessage( + new ErrorMessage( + title, + 'We are experiencing heavy traffic, please try again shortly.'.trimEnd().trimStart(), + tabID + ) + ) + return + } + let message = 'This error is reported to the team automatically. We will attempt to fix it as soon as possible.' if (e.errorMessage !== undefined) { message += `\n\nDetails: ${e.errorMessage}` @@ -769,9 +878,7 @@ export class Messenger { message += `\n\nRequest ID: ${requestID}` } - this.dispatcher.sendErrorMessage( - new ErrorMessage('An error occurred while processing your request.', message.trimEnd().trimStart(), tabID) - ) + this.dispatcher.sendErrorMessage(new ErrorMessage(title, message.trimEnd().trimStart(), tabID)) } public sendOpenSettingsMessage(triggerId: string, tabID: string) { diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 2bd2ec9cfe8..e4320c70de4 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -18,6 +18,7 @@ import { CodeReference } from '../../view/connector/connector' import { Customization } from '../../../codewhisperer/client/codewhispereruserclient' import { QuickActionCommand } from '@aws/mynah-ui' import { Message } from '../../../shared/db/chatDb/util' +import { RegionProfile } from '../../../codewhisperer/models/model' export interface TriggerTabIDReceived { tabID: string @@ -210,7 +211,8 @@ export interface TriggerPayload { readonly codeQuery: CodeQuery | undefined readonly userIntent: UserIntent | undefined readonly customization: Customization - context: string[] | QuickActionCommand[] + readonly profile: RegionProfile | undefined + readonly context: string[] | QuickActionCommand[] relevantTextDocuments: RelevantTextDocumentAddition[] additionalContents: AdditionalContentEntryAddition[] // a reference to all documents used in chat payload diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 5d5cc09056d..f2c447500da 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -375,6 +375,7 @@ export class CWCTelemetryHelper { customizationArn: undefinedIfEmpty(getSelectedCustomization().arn), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch(logSendTelemetryEventFailure) @@ -577,6 +578,7 @@ export class CWCTelemetryHelper { customizationArn: undefinedIfEmpty(getSelectedCustomization().arn), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch(logSendTelemetryEventFailure) diff --git a/packages/core/src/codewhispererChat/storages/chatSession.ts b/packages/core/src/codewhispererChat/storages/chatSession.ts index c1781ea7860..d99f6e340e9 100644 --- a/packages/core/src/codewhispererChat/storages/chatSession.ts +++ b/packages/core/src/codewhispererChat/storages/chatSession.ts @@ -43,4 +43,8 @@ export class ChatSessionStorage { public setAgentLoopInProgress(tabID: string, inProgress: boolean): void { this.agentLoopInProgress.set(tabID, inProgress) } + + public deleteAllSessions() { + this.sessions.clear() + } } diff --git a/packages/core/src/codewhispererChat/tools/chatStream.ts b/packages/core/src/codewhispererChat/tools/chatStream.ts index 7e6e4d3ae3a..8f38e789f15 100644 --- a/packages/core/src/codewhispererChat/tools/chatStream.ts +++ b/packages/core/src/codewhispererChat/tools/chatStream.ts @@ -9,6 +9,7 @@ import { Messenger } from '../controllers/chat/messenger/messenger' import { ToolUse } from '@amzn/codewhisperer-streaming' import { CommandValidation } from './executeBash' import { Change } from 'diff' +import { ChatSession } from '../clients/chat/v0/chat' /** * A writable stream that feeds each chunk/line to the chat UI. @@ -22,24 +23,38 @@ export class ChatStream extends Writable { private readonly tabID: string, private readonly triggerID: string, private readonly toolUse: ToolUse | undefined, + private readonly session: ChatSession, + private readonly messageIdToUpdate: string | undefined, + // emitEvent decides to show the streaming message or read/list directory tool message to the user. + private readonly emitEvent: boolean, private readonly validation: CommandValidation, private readonly changeList?: Change[], private readonly logger = getLogger('chatStream') ) { super() - this.logger.debug(`ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}`) - this.messenger.sendInitalStream(tabID, triggerID) + this.logger.debug( + `ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}, session: ${session.readFiles}, emitEvent to mynahUI: ${emitEvent}` + ) + if (!emitEvent) { + return + } + // If messageIdToUpdate is undefined, we need to first create an empty message with messageId so it can be updated later + messageIdToUpdate + ? this.messenger.sendInitalStream(tabID, triggerID) + : this.messenger.sendInitialToolMessage(tabID, triggerID, toolUse?.toolUseId) } override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void { const text = chunk.toString() this.accumulatedLogs += text - this.logger.debug(`ChatStream received chunk: ${text}`) + this.logger.debug(`ChatStream received chunk: ${text}, emitEvent to mynahUI: ${this.emitEvent}`) this.messenger.sendPartialToolLog( this.accumulatedLogs, this.tabID, this.triggerID, this.toolUse, + this.session, + this.messageIdToUpdate, this.validation, this.changeList ) diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index 84308390673..616ea49ed47 100644 --- a/packages/core/src/codewhispererChat/tools/executeBash.ts +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -12,11 +12,10 @@ import { split } from 'shlex' export enum CommandCategory { ReadOnly, - HighRisk, + Mutate, Destructive, } -export const dangerousPatterns = new Set(['<(', '$(', '`']) export const splitOperators = new Set(['|', '&&', '||', '>']) export const splitOperatorsArray = Array.from(splitOperators) export const commandCategories = new Map([ @@ -47,45 +46,37 @@ export const commandCategories = new Map([ ['netstat', CommandCategory.ReadOnly], ['ss', CommandCategory.ReadOnly], ['dig', CommandCategory.ReadOnly], - ['grep', CommandCategory.ReadOnly], ['wc', CommandCategory.ReadOnly], ['sort', CommandCategory.ReadOnly], ['diff', CommandCategory.ReadOnly], ['head', CommandCategory.ReadOnly], ['tail', CommandCategory.ReadOnly], - // HighRisk commands - ['chmod', CommandCategory.HighRisk], - ['chown', CommandCategory.HighRisk], - ['mv', CommandCategory.HighRisk], - ['cp', CommandCategory.HighRisk], - ['ln', CommandCategory.HighRisk], - ['mount', CommandCategory.HighRisk], - ['umount', CommandCategory.HighRisk], - ['kill', CommandCategory.HighRisk], - ['killall', CommandCategory.HighRisk], - ['pkill', CommandCategory.HighRisk], - ['iptables', CommandCategory.HighRisk], - ['route', CommandCategory.HighRisk], - ['systemctl', CommandCategory.HighRisk], - ['service', CommandCategory.HighRisk], - ['crontab', CommandCategory.HighRisk], - ['at', CommandCategory.HighRisk], - ['tar', CommandCategory.HighRisk], - ['awk', CommandCategory.HighRisk], - ['sed', CommandCategory.HighRisk], - ['wget', CommandCategory.HighRisk], - ['curl', CommandCategory.HighRisk], - ['nc', CommandCategory.HighRisk], - ['ssh', CommandCategory.HighRisk], - ['scp', CommandCategory.HighRisk], - ['ftp', CommandCategory.HighRisk], - ['sftp', CommandCategory.HighRisk], - ['rsync', CommandCategory.HighRisk], - ['chroot', CommandCategory.HighRisk], - ['lsof', CommandCategory.HighRisk], - ['strace', CommandCategory.HighRisk], - ['gdb', CommandCategory.HighRisk], + // Mutable commands + ['chmod', CommandCategory.Mutate], + ['curl', CommandCategory.Mutate], + ['mount', CommandCategory.Mutate], + ['umount', CommandCategory.Mutate], + ['systemctl', CommandCategory.Mutate], + ['service', CommandCategory.Mutate], + ['crontab', CommandCategory.Mutate], + ['at', CommandCategory.Mutate], + ['nc', CommandCategory.Mutate], + ['ssh', CommandCategory.Mutate], + ['scp', CommandCategory.Mutate], + ['ftp', CommandCategory.Mutate], + ['sftp', CommandCategory.Mutate], + ['rsync', CommandCategory.Mutate], + ['chroot', CommandCategory.Mutate], + ['strace', CommandCategory.Mutate], + ['gdb', CommandCategory.Mutate], + ['apt', CommandCategory.Mutate], + ['yum', CommandCategory.Mutate], + ['dnf', CommandCategory.Mutate], + ['pacman', CommandCategory.Mutate], + ['exec', CommandCategory.Mutate], + ['eval', CommandCategory.Mutate], + ['xargs', CommandCategory.Mutate], // Destructive commands ['rm', CommandCategory.Destructive], @@ -104,22 +95,17 @@ export const commandCategories = new Map([ ['insmod', CommandCategory.Destructive], ['rmmod', CommandCategory.Destructive], ['modprobe', CommandCategory.Destructive], - ['apt', CommandCategory.Destructive], - ['yum', CommandCategory.Destructive], - ['dnf', CommandCategory.Destructive], - ['pacman', CommandCategory.Destructive], - ['perl', CommandCategory.Destructive], - ['python', CommandCategory.Destructive], - ['bash', CommandCategory.Destructive], - ['sh', CommandCategory.Destructive], - ['exec', CommandCategory.Destructive], - ['eval', CommandCategory.Destructive], - ['xargs', CommandCategory.Destructive], + ['kill', CommandCategory.Destructive], + ['killall', CommandCategory.Destructive], + ['pkill', CommandCategory.Destructive], + ['iptables', CommandCategory.Destructive], + ['route', CommandCategory.Destructive], + ['chown', CommandCategory.Destructive], ]) export const maxBashToolResponseSize: number = 1024 * 1024 // 1MB export const lineCount: number = 1024 export const destructiveCommandWarningMessage = '⚠️ WARNING: Destructive command detected:\n\n' -export const highRiskCommandWarningMessage = '⚠️ WARNING: High risk command detected:\n\n' +export const mutateCommandWarningMessage = 'Mutation command:\n\n' export interface ExecuteBashParams { command: string @@ -197,22 +183,12 @@ export class ExecuteBash { switch (category) { case CommandCategory.Destructive: return { requiresAcceptance: true, warning: destructiveCommandWarningMessage } - case CommandCategory.HighRisk: - return { - requiresAcceptance: true, - warning: highRiskCommandWarningMessage, - } + case CommandCategory.Mutate: + return { requiresAcceptance: true, warning: mutateCommandWarningMessage } case CommandCategory.ReadOnly: - if ( - cmdArgs.some((arg) => - Array.from(dangerousPatterns).some((pattern) => arg.includes(pattern)) - ) - ) { - return { requiresAcceptance: true, warning: highRiskCommandWarningMessage } - } continue default: - return { requiresAcceptance: true, warning: highRiskCommandWarningMessage } + return { requiresAcceptance: true } } } return { requiresAcceptance: false } diff --git a/packages/core/src/codewhispererChat/tools/fsRead.ts b/packages/core/src/codewhispererChat/tools/fsRead.ts index 05519641bd0..ecf90ea6dd3 100644 --- a/packages/core/src/codewhispererChat/tools/fsRead.ts +++ b/packages/core/src/codewhispererChat/tools/fsRead.ts @@ -7,7 +7,6 @@ import { getLogger } from '../../shared/logger/logger' import fs from '../../shared/fs/fs' import { InvokeOutput, OutputKind, sanitizePath } from './toolShared' import { Writable } from 'stream' -import path from 'path' export interface FsReadParams { path: string @@ -48,23 +47,7 @@ export class FsRead { } public queueDescription(updates: Writable): void { - const fileName = path.basename(this.fsPath) - const fileUri = vscode.Uri.file(this.fsPath) - updates.write(`Reading file: [${fileName}](${fileUri}), `) - - const [start, end] = this.readRange ?? [] - - if (start && end) { - updates.write(`from line ${start} to ${end}`) - } else if (start) { - if (start > 0) { - updates.write(`from line ${start} to end of file`) - } else { - updates.write(`${start} line from the end of file to end of file`) - } - } else { - updates.write('all lines') - } + updates.write('') updates.end() } diff --git a/packages/core/src/codewhispererChat/tools/listDirectory.ts b/packages/core/src/codewhispererChat/tools/listDirectory.ts index 96ac6972bdc..a7379169a4d 100644 --- a/packages/core/src/codewhispererChat/tools/listDirectory.ts +++ b/packages/core/src/codewhispererChat/tools/listDirectory.ts @@ -51,12 +51,12 @@ export class ListDirectory { public queueDescription(updates: Writable): void { const fileName = path.basename(this.fsPath) if (this.maxDepth === undefined) { - updates.write(`Listing directory recursively: ${fileName}`) + updates.write(`Analyzing directories recursively: ${fileName}`) } else if (this.maxDepth === 0) { - updates.write(`Listing directory: ${fileName}`) + updates.write(`Analyzing directory: ${fileName}`) } else { const level = this.maxDepth > 1 ? 'levels' : 'level' - updates.write(`Listing directory: ${fileName} limited to ${this.maxDepth} subfolder ${level}`) + updates.write(`Analyzing directory: ${fileName} limited to ${this.maxDepth} subfolder ${level}`) } updates.end() } diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index 51b96486fde..6e2aa92fe63 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -352,6 +352,7 @@ export interface ChatMessageProps { readonly fullWidth?: boolean readonly padding?: boolean readonly codeBlockActions?: CodeBlockActions | null + readonly rootFolderTitle?: string } export class ChatMessage extends UiMessage { @@ -375,6 +376,7 @@ export class ChatMessage extends UiMessage { readonly padding?: boolean readonly codeBlockActions?: CodeBlockActions | null readonly canBeVoted?: boolean = false + readonly rootFolderTitle?: string override type = 'chatMessage' constructor(props: ChatMessageProps, tabID: string) { @@ -398,9 +400,14 @@ export class ChatMessage extends UiMessage { this.fullWidth = props.fullWidth this.padding = props.padding this.codeBlockActions = props.codeBlockActions + this.rootFolderTitle = props.rootFolderTitle } } +export class ToolMessage extends ChatMessage { + override type = 'toolMessage' +} + export interface FollowUp { readonly type: string readonly pillText: string @@ -455,6 +462,10 @@ export class AppToWebViewMessageDispatcher { this.appsToWebViewMessagePublisher.publish(message) } + public sendToolMessage(message: ToolMessage) { + this.appsToWebViewMessagePublisher.publish(message) + } + public sendEditorContextCommandMessage(message: EditorContextCommandMessage) { this.appsToWebViewMessagePublisher.publish(message) } diff --git a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts index 7132e91afec..0853f80e952 100644 --- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts +++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts @@ -22,6 +22,11 @@ import { AuthError, AuthFlowState, userCancelled } from '../types' import { ToolkitError } from '../../../../shared/errors' import { withTelemetryContext } from '../../../../shared/telemetry/util' import { builderIdStartUrl } from '../../../../auth/sso/constants' +import { RegionProfile } from '../../../../codewhisperer/models/model' +import { randomUUID } from '../../../../shared/crypto' +import globals from '../../../../shared/extensionGlobals' +import { telemetry } from '../../../../shared/telemetry/telemetry' +import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager' const className = 'AmazonQLoginWebview' export class AmazonQLoginWebview extends CommonAuthWebview { @@ -157,6 +162,16 @@ export class AmazonQLoginWebview extends CommonAuthWebview { if (featureAuthStates.amazonQ === 'expired') { this.authState = this.isReauthenticating ? 'REAUTHENTICATING' : 'REAUTHNEEDED' return + } else if (featureAuthStates.amazonQ === 'pendingProfileSelection') { + this.authState = 'PENDING_PROFILE_SELECTION' + // possible that user starts with "profile selection" state therefore the timeout for auth flow should be disposed otherwise will emit failure + this.loadMetadata?.loadTimeout?.dispose() + this.loadMetadata = { + traceId: randomUUID(), + loadTimeout: undefined, + start: globals.clock.Date.now(), + } + return } this.authState = 'LOGIN' } @@ -202,10 +217,37 @@ export class AmazonQLoginWebview extends CommonAuthWebview { /** If users are unauthenticated in Q/CW, we should always display the auth screen. */ async quitLoginScreen() {} + /** + * The purpose of returning Error.message is to notify vue frontend that API call fails and to render corresponding error message to users + * @returns ProfileList when API call succeeds, otherwise Error.message + */ + override async listRegionProfiles(): Promise { + try { + return await AuthUtil.instance.regionProfileManager.listRegionProfile() + } catch (e) { + const conn = AuthUtil.instance.conn as SsoConnection | undefined + telemetry.amazonq_didSelectProfile.emit({ + source: 'auth', + amazonQProfileRegion: AuthUtil.instance.regionProfileManager.activeRegionProfile?.region ?? 'not-set', + ssoRegion: conn?.ssoRegion, + result: 'Failed', + credentialStartUrl: conn?.startUrl, + reason: (e as Error).message, + }) + + return (e as Error).message + } + } + + override selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent) { + return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile, source) + } + private setupConnectionEventEmitter(): void { // allows the frontend to listen to Amazon Q auth events from the backend const codeWhispererConnectionChanged = createThrottle(() => this.onActiveConnectionModified.fire()) AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(codeWhispererConnectionChanged) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(codeWhispererConnectionChanged) /** * Multiple events can be received in rapid succession and if diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts index 54dbcd8d05c..9fc91589e91 100644 --- a/packages/core/src/login/webview/vue/backend.ts +++ b/packages/core/src/login/webview/vue/backend.ts @@ -32,6 +32,8 @@ import { DevSettings } from '../../../shared/settings' import { AuthSSOServer } from '../../../auth/sso/server' import { getLogger } from '../../../shared/logger/logger' import { isValidUrl } from '../../../shared/utilities/uriUtils' +import { RegionProfile } from '../../../codewhisperer/models/model' +import { ProfileSwitchIntent } from '../../../codewhisperer/region/regionProfileManager' export abstract class CommonAuthWebview extends VueWebview { private readonly className = 'CommonAuthWebview' @@ -67,7 +69,7 @@ export abstract class CommonAuthWebview extends VueWebview { * @param errorMessage IF an error is caught on the frontend, this is the message. It will result in a failure metric. * Otherwise we assume success. */ - public setUiReady(state: 'login' | 'reauth', errorMessage?: string) { + public setUiReady(state: 'login' | 'reauth' | 'selectProfile', errorMessage?: string) { if (errorMessage) { this.setLoadFailure(state, errorMessage) } else { @@ -206,6 +208,10 @@ export abstract class CommonAuthWebview extends VueWebview { /** List current connections known by the extension for the purpose of preventing duplicates. */ abstract listSsoConnections(): Promise + abstract listRegionProfiles(): Promise + + abstract selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent): Promise + /** * Emit stored metric metadata. Does not reset the stored metric metadata, because it * may be used for additional emits (e.g. user cancels multiple times, user cancels then logs in) diff --git a/packages/core/src/login/webview/vue/login.vue b/packages/core/src/login/webview/vue/login.vue index 831a1dfcb22..50b1e244043 100644 --- a/packages/core/src/login/webview/vue/login.vue +++ b/packages/core/src/login/webview/vue/login.vue @@ -603,7 +603,7 @@ export function getReadyElementId() { } - diff --git a/packages/core/src/login/webview/vue/root.vue b/packages/core/src/login/webview/vue/root.vue index d50634048c9..a4afc07f150 100644 --- a/packages/core/src/login/webview/vue/root.vue +++ b/packages/core/src/login/webview/vue/root.vue @@ -12,12 +12,18 @@ configure app to AMAZONQ if for Amazon Q login :state="authFlowState" :key="refreshKey" > + diff --git a/packages/core/src/login/webview/vue/selectableItem.vue b/packages/core/src/login/webview/vue/selectableItem.vue index 2ac95876f28..4e545cb5cea 100644 --- a/packages/core/src/login/webview/vue/selectableItem.vue +++ b/packages/core/src/login/webview/vue/selectableItem.vue @@ -59,7 +59,12 @@
-
{{ itemTitle }}
+
+ {{ itemTitle }} + +
{{ itemText }}
@@ -75,6 +80,7 @@ export default defineComponent({ itemId: Number, itemText: String, itemTitle: String, + itemSubTitle: String, itemType: Number, isSelected: Boolean, isHovering: Boolean, @@ -83,6 +89,7 @@ export default defineComponent({ return { itemId: this.itemId, itemTitle: this.itemTitle, + itemSubTitle: this.itemSubTitle, itemText: this.itemText, isSelected: this.isSelected, isHovering: false, @@ -158,6 +165,13 @@ body.vscode-high-contrast-light .item-container-base.focussed:before { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + margin-bottom: 0.2rem; +} + +.p { + font-weight: var(--font-size-base); + margin-top: 0.2rem; + text-align: justify; } .text { diff --git a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts index 6c147f5d313..3c2a9e2e8ec 100644 --- a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts +++ b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts @@ -21,6 +21,8 @@ import { CodeCatalystAuthenticationProvider } from '../../../../codecatalyst/aut import { AuthError, AuthFlowState } from '../types' import { setContext } from '../../../../shared/vscode/setContext' import { builderIdStartUrl } from '../../../../auth/sso/constants' +import { RegionProfile } from '../../../../codewhisperer/models/model' +import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager' export class ToolkitLoginWebview extends CommonAuthWebview { public override id: string = 'aws.toolkit.AmazonCommonAuth' @@ -176,4 +178,12 @@ export class ToolkitLoginWebview extends CommonAuthWebview { await setContext('aws.explorer.showAuthView', false) await this.showResourceExplorer() } + + override listRegionProfiles(): Promise { + throw new Error('Method not implemented') + } + + override selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent): Promise { + throw new Error('Method not implemented') + } } diff --git a/packages/core/src/login/webview/vue/types.ts b/packages/core/src/login/webview/vue/types.ts index 1b92af8ab72..c4203dcbe18 100644 --- a/packages/core/src/login/webview/vue/types.ts +++ b/packages/core/src/login/webview/vue/types.ts @@ -29,6 +29,7 @@ export const AuthFlowStates = { REAUTHNEEDED: 'REAUTHNEEDED', /** Reauthentication is currently in progress */ REAUTHENTICATING: 'REAUTHENTICATING', + PENDING_PROFILE_SELECTION: 'PENDING_PROFILE_SELECTION', } as const export type AuthFlowState = (typeof AuthFlowStates)[keyof typeof AuthFlowStates] diff --git a/packages/core/src/shared/featureConfig.ts b/packages/core/src/shared/featureConfig.ts index 5ad84b1ded4..6d59fe0782a 100644 --- a/packages/core/src/shared/featureConfig.ts +++ b/packages/core/src/shared/featureConfig.ts @@ -105,6 +105,7 @@ export class FeatureConfigProvider { } public async listFeatureEvaluations(): Promise { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const request: ListFeatureEvaluationsRequest = { userContext: { ideCategory: 'VSCODE', @@ -113,6 +114,7 @@ export class FeatureConfigProvider { clientId: getClientId(globals.globalState), ideVersion: extensionVersion, }, + profileArn: profile?.arn, } return (await client.createUserSdkClient()).listFeatureEvaluations(request).promise() } diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts index fdfd1bc3106..44d848ec69d 100644 --- a/packages/core/src/shared/globalState.ts +++ b/packages/core/src/shared/globalState.ts @@ -48,6 +48,7 @@ export type globalKey = | 'aws.toolkit.lsp.versions' | 'aws.toolkit.lsp.manifest' | 'aws.amazonq.customization.overrideV2' + | 'aws.amazonq.regionProfiles' // Deprecated/legacy names. New keys should start with "aws.". | '#sessionCreationDates' // Legacy name from `ssoAccessTokenProvider.ts`. | 'CODECATALYST_RECONNECT' diff --git a/packages/core/src/shared/settings.ts b/packages/core/src/shared/settings.ts index 1110b630776..71a2f83a77c 100644 --- a/packages/core/src/shared/settings.ts +++ b/packages/core/src/shared/settings.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode' import * as codecatalyst from './clients/codecatalystClient' -import * as codewhisperer from '../codewhisperer/client/codewhisperer' import { getLogger } from './logger/logger' import { cast, @@ -23,6 +22,7 @@ import { telemetry } from './telemetry/telemetry' import globals from './extensionGlobals' import toolkitSettings from './settings-toolkit.gen' import amazonQSettings from './settings-amazonq.gen' +import { CodeWhispererConfig } from '../codewhisperer/models/model' type Workspace = Pick @@ -785,9 +785,9 @@ type AwsDevSetting = keyof ResolvedDevSettings type ServiceClients = keyof ServiceTypeMap interface ServiceTypeMap { codecatalystService: codecatalyst.CodeCatalystConfig - codewhispererService: codewhisperer.CodeWhispererConfig amazonqLsp: object // type is provided inside of amazon q amazonqWorkspaceLsp: object // type is provided inside of amazon q + codewhispererService: CodeWhispererConfig } /** diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index 4bc7052e865..ffd715cbed8 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -1,5 +1,20 @@ { "types": [ + { + "name": "amazonQProfileRegion", + "type": "string", + "description": "Region of the Q Profile associated with a metric\n- \"n/a\" if metric is not associated with a profile or region.\n- \"not-set\" if metric is associated with a profile, but profile is unknown." + }, + { + "name": "ssoRegion", + "type": "string", + "description": "Region of the current SSO connection. Typically associated with credentialStartUrl\n- \"n/a\" if metric is not associated with a region.\n- \"not-set\" if metric is associated with a region, but region is unknown." + }, + { + "name": "profileCount", + "type": "int", + "description": "The number of profiles that were available to choose from" + }, { "name": "amazonGenerateApproachLatency", "type": "double", @@ -231,6 +246,31 @@ } ], "metrics": [ + { + "name": "amazonq_didSelectProfile", + "description": "Emitted after the user's Q Profile has been set, whether the user was prompted with a dialog, or a profile was automatically assigned after signing in.", + "metadata": [ + { "type": "source" }, + { "type": "amazonQProfileRegion" }, + { "type": "result" }, + { "type": "ssoRegion", "required": false }, + { "type": "credentialStartUrl", "required": false }, + { "type": "profileCount", "required": false } + ], + "passive": true + }, + { + "name": "amazonq_profileState", + "description": "Indicates a change in the user's Q Profile state", + "metadata": [ + { "type": "source" }, + { "type": "amazonQProfileRegion" }, + { "type": "result" }, + { "type": "ssoRegion", "required": false }, + { "type": "credentialStartUrl", "required": false } + ], + "passive": true + }, { "name": "ide_fileSystem", "description": "File System event on execution", diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts index 1196ea8cc1d..b630a708d15 100644 --- a/packages/core/src/shared/vscode/setContext.ts +++ b/packages/core/src/shared/vscode/setContext.ts @@ -19,6 +19,7 @@ export type contextKey = | 'aws.amazonq.showLoginView' | 'aws.amazonq.security.noMatches' | 'aws.amazonq.notifications.show' + | 'aws.amazonq.connectedSsoIdc' | 'aws.codecatalyst.connected' | 'aws.codewhisperer.connected' | 'aws.codewhisperer.connectionExpired' diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index ce00b0c52c3..3ec69e70b04 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -233,7 +233,11 @@ describe('transformByQ', function () { } sinon.stub(codeWhisperer.codeWhispererClient, 'codeModernizerGetCodeTransformation').resolves(mockJobResponse) transformByQState.setToSucceeded() - const status = await pollTransformationJob('dummyId', CodeWhispererConstants.validStatesForCheckingDownloadUrl) + const status = await pollTransformationJob( + 'dummyId', + CodeWhispererConstants.validStatesForCheckingDownloadUrl, + undefined + ) assert.strictEqual(status, 'COMPLETED') }) diff --git a/packages/toolkit/.changes/3.54.0.json b/packages/toolkit/.changes/3.54.0.json new file mode 100644 index 00000000000..4fd39ef1882 --- /dev/null +++ b/packages/toolkit/.changes/3.54.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-04-09", + "version": "3.54.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index ef72984cca3..b89201f8d4f 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.54.0 2025-04-09 + +- Miscellaneous non-user-facing changes + ## 3.53.0 2025-04-03 - **Feature** Step Functions: Use WorkflowStudio to render StateMachine Graph in CDK applications diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index ac20f9e163a..b84e3d2ee47 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.54.0-SNAPSHOT", + "version": "3.55.0-SNAPSHOT", "extensionKind": [ "workspace" ],