From bd3bf40c9e9d89d67d114882e58cfbd4187bce31 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 6 May 2025 14:19:43 -0400 Subject: [PATCH 1/4] feat(amazonq): paid tier --- packages/amazonq/package.json | 22 ++++++++++++---- packages/amazonq/src/lsp/auth.ts | 2 ++ packages/amazonq/src/lsp/chat/activation.ts | 14 +++++++++- packages/amazonq/src/lsp/chat/commands.ts | 2 +- packages/amazonq/src/lsp/chat/messages.ts | 26 ++++++++++++++++++- packages/core/package.nls.json | 1 + .../core/src/auth/credentials/validation.ts | 8 ++++++ packages/core/src/auth/index.ts | 1 + .../codewhisperer/ui/codeWhispererNodes.ts | 12 +++++++++ .../src/codewhisperer/ui/statusBarMenu.ts | 3 ++- packages/core/src/login/webview/vue/login.vue | 8 +++--- .../commands/basicCommands.test.ts | 13 +++++++++- 12 files changed, 98 insertions(+), 14 deletions(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 477b548e3f5..b87b09f8132 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -388,16 +388,21 @@ "when": "view == aws.amazonq.AmazonQChatView", "group": "0_topAmazonQ@1" }, - { - "command": "aws.amazonq.learnMore", - "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.manageSubscription", + "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected", + "group": "1_amazonQ@2" + }, + { + "command": "aws.amazonq.learnMore", + "when": "view =~ /^aws\\.amazonq/", + "group": "1_amazonQ@3" + }, { "command": "aws.amazonq.signout", "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected && !aws.isSageMakerUnifiedStudio", @@ -679,6 +684,13 @@ "category": "%AWS.amazonq.title%", "icon": "$(question)" }, + { + "command": "aws.amazonq.manageSubscription", + "title": "%AWS.command.manageSubscription%", + "category": "%AWS.amazonq.title%", + "icon": "$(gear)", + "enablement": "aws.codewhisperer.connected && !aws.amazonq.connectedSsoIdc" + }, { "command": "aws.amazonq.signout", "title": "%AWS.command.codewhisperer.signout%", diff --git a/packages/amazonq/src/lsp/auth.ts b/packages/amazonq/src/lsp/auth.ts index d81f464d6a3..0bfee98f2e2 100644 --- a/packages/amazonq/src/lsp/auth.ts +++ b/packages/amazonq/src/lsp/auth.ts @@ -96,6 +96,8 @@ export class AmazonQLspAuth { token, }) + // "aws/credentials/token/update" + // https://github.com/aws/language-servers/blob/44d81f0b5754747d77bda60b40cc70950413a737/core/aws-lsp-core/src/credentials/credentialsProvider.ts#L27 await this.client.sendRequest(bearerCredentialsUpdateRequestType.method, request) this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`) diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index f8e3ee16251..e10a7d2d438 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -6,7 +6,7 @@ import { window } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' -import { registerCommands } from './commands' +import { focusAmazonQPanel, registerCommands } from './commands' import { registerLanguageServerEventListener, registerMessageListeners } from './messages' import { Commands, getLogger, globals, undefinedIfEmpty } from 'aws-core-vscode/shared' import { activate as registerLegacyChatListeners } from '../../app/chat/activation' @@ -73,6 +73,18 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu customization: undefinedIfEmpty(getSelectedCustomization().arn), }) }), + Commands.register('aws.amazonq.manageSubscription', () => { + focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) + + languageClient + .sendRequest('workspace/executeCommand', { + command: 'aws/chat/manageSubscription', + // arguments: [], + }) + .catch((e) => { + getLogger('amazonqLsp').error('failed request: aws/chat/manageSubscription: %O', e) + }) + }), globals.logOutputChannel.onDidChangeLogLevel((logLevel) => { getLogger('amazonqLsp').info(`Local log level changed to ${logLevel}, notifying LSP`) void pushConfigUpdate(languageClient, { diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts index 74c63592a4f..115118a4ad2 100644 --- a/packages/amazonq/src/lsp/chat/commands.ts +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -125,7 +125,7 @@ function registerGenericCommand(commandName: string, genericCommand: string, pro * * Instead, we just create our own as a temporary solution */ -async function focusAmazonQPanel() { +export async function focusAmazonQPanel() { 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 f0dcbd9e608..9a36707ba38 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -63,6 +63,7 @@ import * as jose from 'jose' import { AmazonQChatViewProvider } from './webviewProvider' import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl } from 'aws-core-vscode/shared' +import { credentialsValidation } from 'aws-core-vscode/auth' import { DefaultAmazonQAppInitContext, messageDispatcher, @@ -72,6 +73,7 @@ import { } from 'aws-core-vscode/amazonq' import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' +import { focusAmazonQPanel } from './commands' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { languageClient.info( @@ -328,13 +330,33 @@ export function registerMessageListeners( } break case buttonClickRequestType.method: { + if (message.params.buttonId === 'paidtier-upgrade-q') { + focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) + + const accountId = await vscode.window.showInputBox({ + title: 'Upgrade Amazon Q', + prompt: 'Enter your 12-digit AWS account ID', + placeHolder: '111111111111', + validateInput: credentialsValidation.validateAwsAccount, + }) + + if (accountId) { + languageClient.sendRequest('workspace/executeCommand', { + command: 'aws/chat/manageSubscription', + arguments: [accountId], + }) + } else { + languageClient.error('[VSCode Client] user canceled or did not input AWS account id') + } + } + const buttonResult = await languageClient.sendRequest( buttonClickRequestType.method, message.params ) if (!buttonResult.success) { languageClient.error( - `[VSCode Client] Failed to execute action associated with button with reason: ${buttonResult.failureReason}` + `[VSCode Client] Failed to execute button action: ${buttonResult.failureReason}` ) } break @@ -433,6 +455,8 @@ export function registerMessageListeners( languageClient.onRequest( ShowDocumentRequest.method, async (params: ShowDocumentParams): Promise> => { + focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) + try { const uri = vscode.Uri.parse(params.uri) diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 609eeb5cd08..aa1ac167917 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -137,6 +137,7 @@ "AWS.command.codecatalyst.login": "Connect to CodeCatalyst", "AWS.command.codecatalyst.logout": "Sign out of CodeCatalyst", "AWS.command.codecatalyst.signout": "Sign Out", + "AWS.command.manageSubscription": "Manage Q Developer Pro Subscription", "AWS.command.amazonq.explainCode": "Explain", "AWS.command.amazonq.refactorCode": "Refactor", "AWS.command.amazonq.fixCode": "Fix", diff --git a/packages/core/src/auth/credentials/validation.ts b/packages/core/src/auth/credentials/validation.ts index 70229e3786c..5272c4e625d 100644 --- a/packages/core/src/auth/credentials/validation.ts +++ b/packages/core/src/auth/credentials/validation.ts @@ -127,6 +127,14 @@ async function validateProfileName(profileName: SectionName) { } } +export function validateAwsAccount(s: string): string | undefined { + // AWS account IDs are exactly 12 digits + if (!/^\d{12}$/.test(s)) { + return 'Enter a valid 12-digit AWS account ID' + } + return undefined +} + // All shared credentials keys const sharedCredentialsKeysSet = new Set(Object.values(SharedCredentialsKeys)) diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 02a0067be45..c180d603c67 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -24,3 +24,4 @@ export { Auth } from './auth' export { CredentialsStore } from './credentials/store' export { LoginManager } from './deprecated/loginManager' export * as AuthUtils from './utils' +export * as credentialsValidation from './credentials/validation' diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts index f317a20a573..28ed3952494 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -176,6 +176,18 @@ export function createGettingStarted(): DataQuickPickItem<'gettingStarted'> { } as DataQuickPickItem<'gettingStarted'> } +export function createManageSubscription(): DataQuickPickItem<'manageSubscription'> { + const label = localize('AWS.command.manageSubscription', 'Manage Q Developer Pro Subscription') + // const kind = AuthUtil.instance.isBuilderIdInUse() ? 'AWS Builder ID' : 'IAM Identity Center' + + return { + data: 'manageSubscription', + label: label, + iconPath: getIcon('vscode-link-external'), + onClick: () => Commands.tryExecute('aws.amazonq.manageSubscription'), + } as DataQuickPickItem<'manageSubscription'> +} + export function createSignout(): DataQuickPickItem<'signout'> { const label = localize('AWS.codewhisperer.signoutNode.label', 'Sign Out') const icon = getIcon('vscode-export') diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index 2ad14a81df0..19cfbeab80a 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -11,6 +11,7 @@ import { createSelectCustomization, createReconnect, createGettingStarted, + createManageSubscription, createSignout, createSeparator, createSettingsNode, @@ -106,7 +107,7 @@ export function getQuickPickItems(): DataQuickPickItem[] { createSettingsNode(), ...(isUsingEnterpriseSso && regionProfile ? [createSelectRegionProfileNode(regionProfile)] : []), ...(AuthUtil.instance.isConnected() && !hasVendedIamCredentials() && !hasVendedCredentialsFromMetadata() - ? [createSignout()] + ? [createManageSubscription(), createSignout()] : []), ] diff --git a/packages/core/src/login/webview/vue/login.vue b/packages/core/src/login/webview/vue/login.vue index ddcd1d91c28..312aa18029b 100644 --- a/packages/core/src/login/webview/vue/login.vue +++ b/packages/core/src/login/webview/vue/login.vue @@ -108,8 +108,8 @@ @toggle="toggleItemSelection" :isSelected="selectedLoginOption === LoginOption.BUILDER_ID" :itemId="LoginOption.BUILDER_ID" - :itemText="'with Builder ID, a personal profile from AWS'" - :itemTitle="'Use for Free'" + :itemText="'Free to start with a Builder ID.'" + :itemTitle="'Personal account'" :itemType="LoginOption.BUILDER_ID" class="selectable-item bottomMargin" > @@ -118,8 +118,8 @@ @toggle="toggleItemSelection" :isSelected="selectedLoginOption === LoginOption.ENTERPRISE_SSO" :itemId="LoginOption.ENTERPRISE_SSO" - :itemText="''" - :itemTitle="'Use with Pro license'" + :itemText="'Best for individual teams or organizations.'" + :itemTitle="'Company account'" :itemType="LoginOption.ENTERPRISE_SSO" class="selectable-item bottomMargin" > diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 01c7c43c947..cf8c1195f69 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -42,6 +42,7 @@ import { createGettingStarted, createGitHubNode, createLearnMore, + createManageSubscription, createOpenReferenceLog, createReconnect, createSecurityScan, @@ -445,7 +446,13 @@ describe('CodeWhisperer-basicCommands', function () { sinon.stub(AuthUtil.instance, 'isConnected').returns(true) getTestWindow().onDidShowQuickPick((e) => { - e.assertContainsItems(createReconnect(), createLearnMore(), ...genericItems(), createSignout()) + e.assertContainsItems( + createReconnect(), + createLearnMore(), + ...genericItems(), + createManageSubscription(), + createSignout() + ) e.dispose() // skip needing to select an item to continue }) @@ -465,6 +472,7 @@ describe('CodeWhisperer-basicCommands', function () { switchToAmazonQNode(), ...genericItems(), createSettingsNode(), + createManageSubscription(), createSignout() ) e.dispose() // skip needing to select an item to continue @@ -489,6 +497,7 @@ describe('CodeWhisperer-basicCommands', function () { switchToAmazonQNode(), ...genericItems(), createSettingsNode(), + createManageSubscription(), createSignout() ) e.dispose() // skip needing to select an item to continue @@ -515,6 +524,7 @@ describe('CodeWhisperer-basicCommands', function () { ...genericItems(), createSeparator(), createSettingsNode(), + createManageSubscription(), createSignout(), ]) e.dispose() // skip needing to select an item to continue @@ -537,6 +547,7 @@ describe('CodeWhisperer-basicCommands', function () { switchToAmazonQNode(), ...genericItems(), createSettingsNode(), + createManageSubscription(), createSignout() ) e.dispose() From 50a418318aa4908f3eba9e71aebcdba78a64cea7 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Fri, 23 May 2025 15:52:38 -0400 Subject: [PATCH 2/4] remove account-id input UX/product decision: aws account-id will be collected by the aws console instead of the client (IDE extension). --- packages/amazonq/src/lsp/chat/messages.ts | 21 ------------------- .../core/src/auth/credentials/validation.ts | 8 ------- 2 files changed, 29 deletions(-) diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 9a36707ba38..bbac828e3df 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -63,7 +63,6 @@ import * as jose from 'jose' import { AmazonQChatViewProvider } from './webviewProvider' import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl } from 'aws-core-vscode/shared' -import { credentialsValidation } from 'aws-core-vscode/auth' import { DefaultAmazonQAppInitContext, messageDispatcher, @@ -330,26 +329,6 @@ export function registerMessageListeners( } break case buttonClickRequestType.method: { - if (message.params.buttonId === 'paidtier-upgrade-q') { - focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) - - const accountId = await vscode.window.showInputBox({ - title: 'Upgrade Amazon Q', - prompt: 'Enter your 12-digit AWS account ID', - placeHolder: '111111111111', - validateInput: credentialsValidation.validateAwsAccount, - }) - - if (accountId) { - languageClient.sendRequest('workspace/executeCommand', { - command: 'aws/chat/manageSubscription', - arguments: [accountId], - }) - } else { - languageClient.error('[VSCode Client] user canceled or did not input AWS account id') - } - } - const buttonResult = await languageClient.sendRequest( buttonClickRequestType.method, message.params diff --git a/packages/core/src/auth/credentials/validation.ts b/packages/core/src/auth/credentials/validation.ts index 5272c4e625d..70229e3786c 100644 --- a/packages/core/src/auth/credentials/validation.ts +++ b/packages/core/src/auth/credentials/validation.ts @@ -127,14 +127,6 @@ async function validateProfileName(profileName: SectionName) { } } -export function validateAwsAccount(s: string): string | undefined { - // AWS account IDs are exactly 12 digits - if (!/^\d{12}$/.test(s)) { - return 'Enter a valid 12-digit AWS account ID' - } - return undefined -} - // All shared credentials keys const sharedCredentialsKeysSet = new Set(Object.values(SharedCredentialsKeys)) From 045832533f52ad684ff427b396737cdc6de10772 Mon Sep 17 00:00:00 2001 From: chungjac Date: Mon, 9 Jun 2025 14:15:41 -0700 Subject: [PATCH 3/4] feat(amazonq): add MCP server support (#7451) ## Problem - we are missing the `mcp: true` flag ## Solution - add `mcp: true` flag to enable mcp server support --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../Feature-57661731-6180-4157-a04b-d3a8b50aa1a8.json | 4 ++++ packages/amazonq/src/lsp/client.ts | 1 + 2 files changed, 5 insertions(+) create mode 100644 packages/amazonq/.changes/next-release/Feature-57661731-6180-4157-a04b-d3a8b50aa1a8.json diff --git a/packages/amazonq/.changes/next-release/Feature-57661731-6180-4157-a04b-d3a8b50aa1a8.json b/packages/amazonq/.changes/next-release/Feature-57661731-6180-4157-a04b-d3a8b50aa1a8.json new file mode 100644 index 00000000000..c2e164f773f --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-57661731-6180-4157-a04b-d3a8b50aa1a8.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Add MCP Server Support" +} diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 549b0ac7dad..01dac742902 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -123,6 +123,7 @@ export async function startLanguageServer( awsClientCapabilities: { q: { developerProfiles: true, + mcp: true, }, window: { notifications: true, From 3e87ce27df78666c8334b439137631b18bcc72d9 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Mon, 9 Jun 2025 17:00:25 -0700 Subject: [PATCH 4/4] remove unused code --- packages/amazonq/src/lsp/chat/messages.ts | 38 ----------------------- 1 file changed, 38 deletions(-) diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index a386f4b5861..c55086fd3fc 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -40,9 +40,6 @@ import { tabBarActionRequestType, contextCommandsNotificationType, ContextCommandParams, - ShowDocumentParams, - ShowDocumentResult, - ShowDocumentRequest, openFileDiffNotificationType, OpenFileDiffParams, LINK_CLICK_NOTIFICATION_METHOD, @@ -73,7 +70,6 @@ import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' import { decryptResponse, encryptRequest } from '../encryption' import { getCursorState } from '../utils' -import { focusAmazonQPanel } from './commands' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { languageClient.info( @@ -416,40 +412,6 @@ export function registerMessageListeners( } }) - languageClient.onRequest( - ShowDocumentRequest.method, - async (params: ShowDocumentParams): Promise> => { - focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) - - try { - const uri = vscode.Uri.parse(params.uri) - - if (params.external) { - // Note: Not using openUrl() because we probably don't want telemetry for these URLs. - // Also it doesn't yet support the required HACK below. - - // HACK: workaround vscode bug: https://github.com/microsoft/vscode/issues/85930 - vscode.env.openExternal(params.uri as any).then(undefined, (e) => { - // TODO: getLogger('?').error('failed vscode.env.openExternal: %O', e) - vscode.env.openExternal(uri).then(undefined, (e) => { - // TODO: getLogger('?').error('failed vscode.env.openExternal: %O', e) - }) - }) - return params - } - - const doc = await vscode.workspace.openTextDocument(uri) - await vscode.window.showTextDocument(doc, { preview: false }) - return params - } catch (e) { - return new ResponseError( - LSPErrorCodes.RequestFailed, - `Failed to open document: ${(e as Error).message}` - ) - } - } - ) - languageClient.onNotification(contextCommandsNotificationType.method, (params: ContextCommandParams) => { void provider.webview?.postMessage({ command: contextCommandsNotificationType.method,