From 2953454375e04beb09633a7ad9bcf35287e23f05 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:27:53 -0400 Subject: [PATCH 01/10] fix(amazonq): add missing lsp manifest messages from amazonq/package.json (#6826) ## Problem - manifest suppression messages were missing from package.json and directly written to settings gen, causing them to be overridden on every `npm install` ## Solution - add manifest suppression to package.json - make this field available in the config --- - 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. --- packages/amazonq/package.json | 8 ++++++++ packages/amazonq/src/lsp/config.ts | 3 ++- packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts | 2 +- packages/amazonq/test/unit/amazonq/lsp/config.test.ts | 4 +++- packages/core/src/amazonq/lsp/config.ts | 4 +++- packages/core/src/shared/lsp/baseLspInstaller.ts | 4 ++-- packages/core/src/shared/lsp/manifestResolver.ts | 7 ++++--- packages/core/src/shared/settings-amazonq.gen.ts | 4 ++-- .../core/src/test/shared/lsp/manifestResolver.test.ts | 6 +++--- 9 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index c9844eb6eb8..087602a00bf 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -119,6 +119,14 @@ "ssoCacheError": { "type": "boolean", "default": false + }, + "amazonQLspManifestMessage": { + "type": "boolean", + "default": false + }, + "amazonQWorkspaceLspManifestMessage": { + "type": "boolean", + "default": false } }, "additionalProperties": false diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts index 634cc43aab2..7ea12fdd4d7 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -9,7 +9,8 @@ import { LspConfig } from 'aws-core-vscode/amazonq' export const defaultAmazonQLspConfig: LspConfig = { manifestUrl: 'https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json', supportedVersions: '^3.1.1', - id: 'AmazonQ', // used for identification in global storage/local disk location. Do not change. + id: 'AmazonQ', // used across IDEs for identifying global storage/local disk locations. Do not change. + suppressPromptPrefix: 'amazonQ', path: undefined, } diff --git a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts index 0031e627d8c..4c88cba491b 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts @@ -249,7 +249,7 @@ export function createLspInstallerTests({ }) it('resolves release candidiates', async () => { - const original = new ManifestResolver(lspConfig.manifestUrl, lspConfig.id).resolve() + const original = new ManifestResolver(lspConfig.manifestUrl, lspConfig.id, '').resolve() sandbox.stub(ManifestResolver.prototype, 'resolve').callsFake(async () => { const originalManifest = await original diff --git a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts index 9a9ba1ef348..0d31536a720 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -52,6 +52,7 @@ for (const [name, config, defaultConfig, setEnv, resetEnv] of [ manifestUrl: 'https://custom.url/manifest.json', supportedVersions: '4.0.0', id: 'AmazonQSetting', + suppressPromptPrefix: 'amazonQSetting', path: '/custom/path', } @@ -94,7 +95,8 @@ for (const [name, config, defaultConfig, setEnv, resetEnv] of [ const envConfig: LspConfig = { manifestUrl: 'https://another-custom.url/manifest.json', supportedVersions: '5.1.1', - id: 'AmazonQEnv', + id: 'AmazonQSetting', + suppressPromptPrefix: 'amazonQSetting', path: '/some/new/custom/path', } diff --git a/packages/core/src/amazonq/lsp/config.ts b/packages/core/src/amazonq/lsp/config.ts index 6731c8a4bb4..613a60dc729 100644 --- a/packages/core/src/amazonq/lsp/config.ts +++ b/packages/core/src/amazonq/lsp/config.ts @@ -10,13 +10,15 @@ export interface LspConfig { manifestUrl: string supportedVersions: string id: string + suppressPromptPrefix: string path?: string } export const defaultAmazonQWorkspaceLspConfig: LspConfig = { manifestUrl: 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json', supportedVersions: '0.1.46', - id: 'AmazonQ-Workspace', // used for identification in global storage/local disk location. Do not change. + id: 'AmazonQ-Workspace', // used across IDEs for identifying global storage/local disk locations. Do not change. + suppressPromptPrefix: 'amazonQWorkspace', path: undefined, } diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index 4f3bcdf57e7..e2371769fab 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -25,7 +25,7 @@ export abstract class BaseLspInstaller } async resolve(): Promise> { - const { id, manifestUrl, supportedVersions, path } = this.config + const { id, manifestUrl, supportedVersions, path, suppressPromptPrefix } = this.config if (path) { const overrideMsg = `Using language server override location: ${path}` this.logger.info(overrideMsg) @@ -38,7 +38,7 @@ export abstract class BaseLspInstaller } } - const manifest = await new ManifestResolver(manifestUrl, id).resolve() + const manifest = await new ManifestResolver(manifestUrl, id, suppressPromptPrefix).resolve() const installationResult = await new LanguageServerResolver( manifest, id, diff --git a/packages/core/src/shared/lsp/manifestResolver.ts b/packages/core/src/shared/lsp/manifestResolver.ts index e2b89a0120b..289fc63268d 100644 --- a/packages/core/src/shared/lsp/manifestResolver.ts +++ b/packages/core/src/shared/lsp/manifestResolver.ts @@ -29,7 +29,8 @@ const manifestTimeoutMs = 15000 export class ManifestResolver { constructor( private readonly manifestURL: string, - private readonly lsName: string + private readonly lsName: string, + private readonly supressPrefix: string ) {} /** @@ -109,9 +110,9 @@ export class ManifestResolver { */ private async checkDeprecation(manifest: Manifest): Promise { const prompts = AmazonQPromptSettings.instance - const lspId = `${this.lsName}LspManifestMessage` as keyof typeof amazonQPrompts + const lspId = `${this.supressPrefix}LspManifestMessage` as keyof typeof amazonQPrompts - // Sanity check, if the lsName is changed then we also need to update the prompt keys in settings-amazonq.gen + // Sanity check, if the lsName is changed then we also need to update the prompt keys in core/package.json if (!(lspId in amazonQPrompts)) { logger.error(`LSP ID "${lspId}" not found in amazonQPrompts.`) return diff --git a/packages/core/src/shared/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts index 9447f43d12b..1af71eb4ccc 100644 --- a/packages/core/src/shared/settings-amazonq.gen.ts +++ b/packages/core/src/shared/settings-amazonq.gen.ts @@ -19,8 +19,8 @@ export const amazonqSettings = { "amazonQSessionConfigurationMessage": {}, "minIdeVersion": {}, "ssoCacheError": {}, - "AmazonQLspManifestMessage": {}, - "AmazonQ-WorkspaceLspManifestMessage":{} + "amazonQLspManifestMessage": {}, + "amazonQWorkspaceLspManifestMessage": {} }, "amazonQ.showCodeWithReferences": {}, "amazonQ.allowFeatureDevelopmentToRunCodeAndTests": {}, diff --git a/packages/core/src/test/shared/lsp/manifestResolver.test.ts b/packages/core/src/test/shared/lsp/manifestResolver.test.ts index eb5f8e90893..953a469c0ea 100644 --- a/packages/core/src/test/shared/lsp/manifestResolver.test.ts +++ b/packages/core/src/test/shared/lsp/manifestResolver.test.ts @@ -44,7 +44,7 @@ describe('manifestResolver', function () { it('attempts to fetch from remote first', async function () { remoteStub.resolves(manifestResult('remote')) - const r = await new ManifestResolver('remote-manifest.com', serverName).resolve() + const r = await new ManifestResolver('remote-manifest.com', serverName, '').resolve() assert.strictEqual(r.location, 'remote') assertTelemetry('languageServer_setup', { manifestLocation: 'remote', @@ -59,7 +59,7 @@ describe('manifestResolver', function () { remoteStub.rejects(new Error('failed to fetch')) localStub.resolves(manifestResult('cache')) - const r = await new ManifestResolver('remote-manifest.com', serverName).resolve() + const r = await new ManifestResolver('remote-manifest.com', serverName, '').resolve() assert.strictEqual(r.location, 'cache') assertTelemetry('languageServer_setup', [ { @@ -82,7 +82,7 @@ describe('manifestResolver', function () { remoteStub.rejects(new Error('failed to fetch')) localStub.rejects(new Error('failed to fetch')) - await assert.rejects(new ManifestResolver('remote-manifest.com', serverName).resolve(), /failed to fetch/) + await assert.rejects(new ManifestResolver('remote-manifest.com', serverName, '').resolve(), /failed to fetch/) assertTelemetry('languageServer_setup', [ { manifestLocation: 'remote', From dc9b623d58f396a7f175f2beec14e31cda0dd715 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:54:18 -0400 Subject: [PATCH 02/10] refactor(amazonq): expose lsp inline completion as an experiment (#6858) ## Problem - We previously thought inline completion was going to be launched first so we didn't end up putting it behind a feature flag ## Solution - Add inline completion behind a feature flag --- - 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. --- docs/lsp.md | 12 +++++-- packages/amazonq/src/extension.ts | 6 ++-- packages/amazonq/src/lsp/activation.ts | 10 +----- packages/amazonq/src/lsp/client.ts | 31 +++++++++++++++---- .../core/src/shared/settings-toolkit.gen.ts | 1 + packages/toolkit/package.json | 4 +++ 6 files changed, 45 insertions(+), 19 deletions(-) diff --git a/docs/lsp.md b/docs/lsp.md index 64a3af3e8c0..601de15c33b 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -45,8 +45,16 @@ sequenceDiagram npm run package ``` to get the project setup -3. Uncomment the `__AMAZONQLSP_PATH` variable in `amazonq/.vscode/launch.json` Extension configuration -4. Use the `Launch LSP with Debugging` configuration and set breakpoints in VSCode or the language server +3. Enable the lsp experiment: + ``` + "aws.experiments": { + "amazonqLSP": true, + "amazonqLSPInline": true, // optional: enables inline completion from flare + "amazonqLSPChat": true // optional: enables chat from flare + } + ``` +4. Uncomment the `__AMAZONQLSP_PATH` variable in `amazonq/.vscode/launch.json` Extension configuration +5. Use the `Launch LSP with Debugging` configuration and set breakpoints in VSCode or the language server ## Amazon Q Inline Activation diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index ad89a44ed4d..ae9e0c5657d 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -120,7 +120,9 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is await activateCodeWhisperer(extContext as ExtContext) if (Experiments.instance.get('amazonqLSP', false)) { await activateAmazonqLsp(context) - } else { + } + + if (!Experiments.instance.get('amazonqLSPInline', false)) { await activateInlineCompletion() } @@ -157,7 +159,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is context.subscriptions.push( Experiments.instance.onDidChange(async (event) => { - if (event.key === 'amazonqLSP' || event.key === 'amazonqChatLSP') { + if (event.key === 'amazonqLSP' || event.key === 'amazonqChatLSP' || event.key === 'amazonqLSPInline') { await vscode.window .showInformationMessage( 'Amazon Q LSP setting has changed. Reload VS Code for the changes to take effect.', diff --git a/packages/amazonq/src/lsp/activation.ts b/packages/amazonq/src/lsp/activation.ts index 6e614412f21..c4855c17970 100644 --- a/packages/amazonq/src/lsp/activation.ts +++ b/packages/amazonq/src/lsp/activation.ts @@ -6,7 +6,7 @@ import vscode from 'vscode' import { startLanguageServer } from './client' import { AmazonQLspInstaller } from './lspInstaller' -import { Commands, lspSetupStage, ToolkitError } from 'aws-core-vscode/shared' +import { lspSetupStage, ToolkitError } from 'aws-core-vscode/shared' export async function activate(ctx: vscode.ExtensionContext): Promise { try { @@ -14,14 +14,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { const installResult = await new AmazonQLspInstaller().resolve() await lspSetupStage('launch', async () => await startLanguageServer(ctx, installResult.resourcePaths)) }) - ctx.subscriptions.push( - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - }), - vscode.workspace.onDidCloseTextDocument(async () => { - await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') - }) - ) } catch (err) { const e = err as ToolkitError void vscode.window.showInformationMessage(`Unable to launch amazonq language server: ${e.message}`) diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index d43e78ab42b..31e4e7b7c93 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -11,7 +11,15 @@ import { InlineCompletionManager } from '../app/inline/completion' import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { ConnectionMetadata } from '@aws/language-server-runtimes/protocol' -import { Settings, oidcClientName, createServerOptions, globals, Experiments, getLogger } from 'aws-core-vscode/shared' +import { + Settings, + oidcClientName, + createServerOptions, + globals, + Experiments, + getLogger, + Commands, +} from 'aws-core-vscode/shared' import { activate } from './chat/activation' import { AmazonQResourcePaths } from './lspInstaller' @@ -101,10 +109,22 @@ export async function startLanguageServer( }, } }) - await auth.init() - const inlineManager = new InlineCompletionManager(client) - inlineManager.registerInlineCompletion() + + if (Experiments.instance.get('amazonqLSPInline', false)) { + const inlineManager = new InlineCompletionManager(client) + inlineManager.registerInlineCompletion() + toDispose.push( + inlineManager, + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + }), + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }) + ) + } + if (Experiments.instance.get('amazonqChatLSP', false)) { activate(client, encryptionKey, resourcePaths.mynahUI) } @@ -125,8 +145,7 @@ export async function startLanguageServer( }), AuthUtil.instance.auth.onDidDeleteConnection(async () => { client.sendNotification(notificationTypes.deleteBearerToken.method) - }), - inlineManager + }) ) }) } diff --git a/packages/core/src/shared/settings-toolkit.gen.ts b/packages/core/src/shared/settings-toolkit.gen.ts index 5cd9854b1fc..59a637a4870 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -43,6 +43,7 @@ export const toolkitSettings = { "aws.experiments": { "jsonResourceModification": {}, "amazonqLSP": {}, + "amazonqLSPInline": {}, "amazonqChatLSP": {} }, "aws.resources.enabledResources": {}, diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index b21c6ef0204..ee842e6fcb4 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -251,6 +251,10 @@ "type": "boolean", "default": false }, + "amazonqLSPInline": { + "type": "boolean", + "default": false + }, "amazonqChatLSP": { "type": "boolean", "default": false From 0fa2b26ebbeacae09b694c15d901a4bfe23fa886 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:49:56 -0400 Subject: [PATCH 03/10] fix(amazonq): @-prompts not added to context ## Problem `@Prompts` not being sent to context due to change in [last week's release](https://github.com/aws/aws-toolkit-vscode/pull/6831/files#r2021948984) ## Solution Add 'file' label to user prompts --- .../Bug Fix-ccbc5e50-0a90-4340-b6d5-e57537949898.json | 4 ++++ .../core/src/codewhispererChat/controllers/chat/controller.ts | 1 + 2 files changed, 5 insertions(+) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-ccbc5e50-0a90-4340-b6d5-e57537949898.json diff --git a/packages/amazonq/.changes/next-release/Bug Fix-ccbc5e50-0a90-4340-b6d5-e57537949898.json b/packages/amazonq/.changes/next-release/Bug Fix-ccbc5e50-0a90-4340-b6d5-e57537949898.json new file mode 100644 index 00000000000..659d32dfc59 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-ccbc5e50-0a90-4340-b6d5-e57537949898.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q chat: `@prompts` not added to context" +} diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 7264bcece69..a37ce3acdcd 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -498,6 +498,7 @@ export class ChatController { command: path.basename(name, promptFileExtension), icon: 'magic' as MynahIconsType, id: 'prompt', + label: 'file' as ContextCommandItemType, route: [userPromptsDirectory, name], })) ) From 708f5381c7627d06a6f63552b3f8074672351ed1 Mon Sep 17 00:00:00 2001 From: Zoe Lin <60411978+zixlin7@users.noreply.github.com> Date: Tue, 1 Apr 2025 07:52:39 -0700 Subject: [PATCH 04/10] feat(amazonq): support import through language server --- package-lock.json | 47 ++++++++++--------- packages/amazonq/src/app/inline/completion.ts | 7 +++ packages/core/package.json | 2 +- packages/core/src/codewhisperer/index.ts | 1 + 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fd4035f62d..90b376315f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10831,16 +10831,16 @@ } }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.44", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.44.tgz", - "integrity": "sha512-eFuBSb1NgW33uZPWCd1oSmc5kJDLJVaBYYpYQxHZHm+lWaHHddPfpmobsVg+FYITYlRYO66d3raw6oYVbH6jNQ==", + "version": "0.2.49", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.49.tgz", + "integrity": "sha512-GJMxZiDuV4zXGh4RBGDEobVH0CvgmFT3O3ZxYfmVGq3JvNhnKXXNYFNzWDUvF8aUUJZJGv4u4OyHitm8TSjrBg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.9.1", + "@apidevtools/json-schema-ref-parser": "^11.9.3", "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-cognito-identity": "^3.758.0", - "@aws/language-server-runtimes-types": "^0.1.4", + "@aws/language-server-runtimes-types": "^0.1.6", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2", "@opentelemetry/resources": "^1.30.1", @@ -10853,11 +10853,11 @@ "@smithy/signature-v4": "^5.0.1", "ajv": "^8.17.1", "aws-sdk": "^2.1692.0", - "axios": "^1.8.2", + "axios": "^1.8.4", "hpagent": "^1.2.0", "jose": "^5.9.6", "mac-ca": "^3.1.1", - "rxjs": "^7.8.1", + "rxjs": "^7.8.2", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "win-ca": "^3.5.1" @@ -11278,9 +11278,9 @@ } }, "node_modules/@aws/language-server-runtimes/node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.4.tgz", - "integrity": "sha512-UPDRghS9SKJlC1yscYFAryDT10UW9zjrnLCsEm05kDb96P0/4tKMgb0urDubN8mfSzVqnLOXZoTA3CutuWRsUA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.6.tgz", + "integrity": "sha512-D8BukMtJRauOw2h/VHOMq7xZxXtCui1zNPzfoU8p6NAAveQL2+eJiLb0l+eYfAB/F1BfWGDcCU/T6cuYg3A/Ng==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11925,15 +11925,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@aws/language-server-runtimes/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@aws/language-server-runtimes/node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -16163,9 +16154,9 @@ "link": true }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "dev": true, "license": "MIT", "dependencies": { @@ -23701,6 +23692,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "funding": [ @@ -26799,7 +26800,7 @@ "devDependencies": { "@aws-sdk/types": "^3.13.1", "@aws/chat-client-ui-types": "^0.0.8", - "@aws/language-server-runtimes": "^0.2.44", + "@aws/language-server-runtimes": "^0.2.49", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index fd5ef749d0d..be390cef34c 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -29,6 +29,7 @@ import { ReferenceHoverProvider, ReferenceInlineProvider, ReferenceLogViewProvider, + ImportAdderProvider, } from 'aws-core-vscode/codewhisperer' export class InlineCompletionManager implements Disposable { @@ -66,6 +67,7 @@ export class InlineCompletionManager implements Disposable { item: InlineCompletionItemWithReferences, editor: TextEditor, requestStartTime: number, + startLine: number, firstCompletionDisplayLatency?: number ) => { // TODO: also log the seen state for other suggestions in session @@ -96,6 +98,9 @@ export class InlineCompletionManager implements Disposable { ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) ReferenceHoverProvider.instance.addCodeReferences(item.insertText as string, item.references) } + if (item.mostRelevantMissingImports?.length) { + await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine) + } } commands.registerCommand('aws.amazonq.acceptInline', onInlineAcceptance) @@ -200,6 +205,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem item, editor, session.requestStartTime, + position.line, session.firstCompletionDisplayLatency, ], } @@ -208,6 +214,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem item.insertText as string, item.references ) + ImportAdderProvider.instance.onShowRecommendation(document, position.line, item) } return items as InlineCompletionItem[] } diff --git a/packages/core/package.json b/packages/core/package.json index 7d64a740305..c58b81ad166 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -441,7 +441,7 @@ "devDependencies": { "@aws-sdk/types": "^3.13.1", "@aws/chat-client-ui-types": "^0.0.8", - "@aws/language-server-runtimes": "^0.2.44", + "@aws/language-server-runtimes": "^0.2.49", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 565e9e3c238..affe4b8457e 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -70,6 +70,7 @@ export { RecommendationService } from './service/recommendationService' export { ClassifierTrigger } from './service/classifierTrigger' export { DocumentChangedSource, KeyStrokeHandler, DefaultDocumentChangedType } from './service/keyStrokeHandler' export { ReferenceLogViewProvider } from './service/referenceLogViewProvider' +export { ImportAdderProvider } from './service/importAdderProvider' export { LicenseUtil } from './util/licenseUtil' export { SecurityIssueProvider } from './service/securityIssueProvider' export { listScanResults, mapToAggregatedList, pollScanJobStatus } from './service/securityScanHandler' From 44a7dbce906eafa1e63f0cfee6685fc0666e2ecc Mon Sep 17 00:00:00 2001 From: Tai Lai Date: Tue, 1 Apr 2025 07:53:21 -0700 Subject: [PATCH 05/10] fix(chat): preserve context in agentic loop --- .../src/codewhispererChat/clients/chat/v0/chat.ts | 11 +++++++++++ .../codewhispererChat/controllers/chat/controller.ts | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index 75b93ff47bf..f8f08f71148 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -14,16 +14,19 @@ 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' export class ChatSession { private sessionId?: string /** * _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 */ private _readFiles: string[] = [] private _toolUse: ToolUse | undefined private _showDiffOnFileWrite: boolean = false + private _context: PromptMessage['context'] contexts: Map = new Map() // TODO: doesn't handle the edge case when two files share the same relativePath string but from different root @@ -41,6 +44,14 @@ export class ChatSession { this._toolUse = toolUse } + public get context(): PromptMessage['context'] { + return this._context + } + + public setContext(context: PromptMessage['context']) { + this._context = context + } + public tokenSource!: vscode.CancellationTokenSource constructor() { diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index a56bf5c811e..4f58b3ab85a 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -709,7 +709,7 @@ export class ChatController { toolResults: toolResults, origin: Origin.IDE, chatHistory: this.chatHistoryManager.getHistory(), - context: [], + context: session.context ?? [], relevantTextDocuments: [], additionalContents: [], documentReferences: [], @@ -1081,6 +1081,10 @@ export class ChatController { type: 'chat_message', context, }) + + // Save the context for the agentic loop + session.setContext(message.context) + await this.generateResponse( { message: message.message ?? '', From 1b3681c9d47e30196cb32138338aeba65c51e944 Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Tue, 1 Apr 2025 15:09:03 -0400 Subject: [PATCH 06/10] feat(amazonq lsp): add authFollowUpClicked implementation (#6886) ## Problem There was no implementation for the `authFollowUpClicked` message received from LSP for Amazon Q. Because of that, the `authenticate` button that appears in case of expired SSO connection had no effect. ## Solution I added the implementation as follows: * If the received `authFollowUpType` is `'re-auth'` or `'missing_scopes'`, initiate a re-authentication * If the received `authFollowUpType` is `'full-auth'` or `'use-supported-auth'`, delete the connection so a new full authentication flow kicks off I added a unit test file for this message type, which is extendable for every command handled in `registerLanguageServerEventListener` --- - 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. --- packages/amazonq/src/lsp/chat/messages.ts | 29 +++- .../unit/amazonq/lsp/chat/messages.test.ts | 129 ++++++++++++++++++ packages/core/src/amazonq/index.ts | 2 + 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 4c9d93f7f65..b2597d21915 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -9,6 +9,7 @@ import { AUTH_FOLLOW_UP_CLICKED, CHAT_OPTIONS, COPY_TO_CLIPBOARD, + AuthFollowUpType, } from '@aws/chat-client-ui-types' import { ChatResult, @@ -25,6 +26,7 @@ import { window } 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' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { languageClient.onDidChangeState(({ oldState, newState }) => { @@ -77,10 +79,33 @@ export function registerMessageListeners( }) break } - case AUTH_FOLLOW_UP_CLICKED: - // TODO hook this into auth + case AUTH_FOLLOW_UP_CLICKED: { languageClient.info('[VSCode Client] AuthFollowUp clicked') + const authType = message.params.authFollowupType + const reAuthTypes: AuthFollowUpType[] = ['re-auth', 'missing_scopes'] + const fullAuthTypes: AuthFollowUpType[] = ['full-auth', 'use-supported-auth'] + + if (reAuthTypes.includes(authType)) { + try { + await AuthUtil.instance.reauthenticate() + } catch (e) { + languageClient.error( + `[VSCode Client] Failed to re-authenticate after AUTH_FOLLOW_UP_CLICKED: ${(e as Error).message}` + ) + } + } + + if (fullAuthTypes.includes(authType)) { + try { + await AuthUtil.instance.secondaryAuth.deleteConnection() + } catch (e) { + languageClient.error( + `[VSCode Client] Failed to authenticate after AUTH_FOLLOW_UP_CLICKED: ${(e as Error).message}` + ) + } + } break + } case chatRequestType.method: { const partialResultToken = uuidv4() const chatDisposable = languageClient.onProgress(chatRequestType, partialResultToken, (partialResult) => diff --git a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts new file mode 100644 index 00000000000..06459cd8932 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts @@ -0,0 +1,129 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import { LanguageClient } from 'vscode-languageclient' +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' + +describe('registerMessageListeners', () => { + let languageClient: LanguageClient + let provider: AmazonQChatViewProvider + let sandbox: sinon.SinonSandbox + let messageHandler: (message: any) => void | Promise + let errorStub: sinon.SinonStub + + beforeEach(() => { + sandbox = sinon.createSandbox() + errorStub = sandbox.stub() + + languageClient = { + info: sandbox.stub(), + error: errorStub, + sendNotification: sandbox.stub(), + } as unknown as LanguageClient + + provider = { + webview: { + onDidReceiveMessage: (callback: (message: any) => void | Promise) => { + messageHandler = callback + return { + dispose: (): void => {}, + } + }, + }, + } as any + + registerMessageListeners(languageClient, provider, Buffer.from('test-key')) + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('AUTH_FOLLOW_UP_CLICKED', () => { + let mockAuthUtil: AuthUtil + let deleteConnectionStub: sinon.SinonStub + let reauthenticateStub: sinon.SinonStub + + const authFollowUpClickedCommand = 'authFollowUpClicked' + + interface TestCase { + authType: AuthFollowUpType + stubToReject: sinon.SinonStub + errorMessage: string + } + + const testFailure = async (testCase: TestCase) => { + testCase.stubToReject.rejects(new Error()) + + await messageHandler({ + command: authFollowUpClickedCommand, + params: { + authFollowupType: testCase.authType, + }, + }) + + sinon.assert.calledOnce(errorStub) + sinon.assert.calledWith(errorStub, sinon.match(testCase.errorMessage)) + } + + beforeEach(() => { + deleteConnectionStub = sandbox.stub().resolves() + reauthenticateStub = sandbox.stub().resolves() + + mockAuthUtil = { + reauthenticate: reauthenticateStub, + secondaryAuth: { + deleteConnection: deleteConnectionStub, + } as unknown as secondaryAuth.SecondaryAuth, + } as unknown as AuthUtil + + sandbox.replaceGetter(AuthUtil, 'instance', () => mockAuthUtil) + }) + + it('handles re-authentication request', async () => { + await messageHandler({ + command: authFollowUpClickedCommand, + params: { + authFollowupType: 're-auth', + }, + }) + + sinon.assert.calledOnce(reauthenticateStub) + sinon.assert.notCalled(deleteConnectionStub) + }) + + it('handles full authentication request', async () => { + await messageHandler({ + command: authFollowUpClickedCommand, + params: { + authFollowupType: 'full-auth', + }, + }) + + sinon.assert.notCalled(reauthenticateStub) + sinon.assert.calledOnce(deleteConnectionStub) + }) + + it('logs error if re-authentication fails', async () => { + await testFailure({ + authType: 're-auth', + stubToReject: reauthenticateStub, + errorMessage: 'Failed to re-authenticate', + }) + }) + + it('logs error if full authentication fails', async () => { + await testFailure({ + authType: 'full-auth', + stubToReject: deleteConnectionStub, + errorMessage: 'Failed to authenticate', + }) + }) + }) +}) diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 98ab00087a3..70223bf72d7 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -46,6 +46,8 @@ export { extractAuthFollowUp } from './util/authUtils' export { Messenger } from './commons/connector/baseMessenger' export * from './lsp/config' export * as WorkspaceLspInstaller from './lsp/workspaceInstaller' +export * as secondaryAuth from '../auth/secondaryAuth' +export * as authConnection from '../auth/connection' import { FeatureContext } from '../shared/featureConfig' /** From 5226db61444c47bc75dff2e3cfe161125bfa81e8 Mon Sep 17 00:00:00 2001 From: David <60020664+dhasani23@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:34:45 -0700 Subject: [PATCH 07/10] fix(amazonq): fix /transform test, remove feedback form #6903 ## Problem We don't yet have legal approval to show users a feedback form. It is currently disabled / commented out, but better to remove the unused code until we get approval, since it may take a while and may not even happen. ## Solution Remove unused code and fix test. --- .../test/e2e/amazonq/transformByQ.test.ts | 33 ++------- .../chat/controller/controller.ts | 16 ----- .../chat/controller/messenger/messenger.ts | 69 ------------------- .../controller/messenger/messengerUtils.ts | 1 - 4 files changed, 6 insertions(+), 113 deletions(-) diff --git a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts index d8352d1c916..784aed4884c 100644 --- a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts +++ b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts @@ -65,7 +65,7 @@ describe('Amazon Q Code Transformation', function () { }) describe('Starting a transformation from chat', () => { - it.skip('Can click through all user input forms for a Java upgrade', async () => { + it('Can click through all user input forms for a Java upgrade', async () => { sinon.stub(startTransformByQ, 'getValidSQLConversionCandidateProjects').resolves([]) sinon.stub(GumbyController.prototype, 'validateLanguageUpgradeProjects' as keyof GumbyController).resolves([ { @@ -168,39 +168,18 @@ describe('Amazon Q Code Transformation', function () { .getChatMessenger() ?.sendJobFinishedMessage(tab.tabID, CodeWhispererConstants.viewProposedChangesChatMessage) - // wait for download message and feedback form to be sent - await tab.waitForEvent(() => tab.getChatItems().length > 15, { - waitTimeoutInMs: 5000, - waitIntervalInMs: 1000, - }) - - const submitFeedbackForm = tab.getChatItems().pop() - assert.strictEqual(submitFeedbackForm?.formItems?.[0]?.id ?? undefined, 'TransformFeedbackRerunJob') - assert.strictEqual(submitFeedbackForm?.formItems?.[1]?.id ?? undefined, 'TransformFeedbackViewLogs') - - const submitFeedbackFormItemValues = { - TransformFeedbackRerunJob: 'No', - TransformFeedbackViewLogs: 'Yes', - } - const submitFeedbackFormValues: Record = { ...submitFeedbackFormItemValues } - - const viewSummaryChatItem = tab.getChatItems()[tab.getChatItems().length - 2] - assert.strictEqual(viewSummaryChatItem?.body?.includes('view a summary'), true) - tab.clickCustomFormButton({ - id: 'gumbyFeedbackFormConfirm', - text: 'Confirm', - formItemValues: submitFeedbackFormValues, + id: 'gumbyViewSummary', + text: 'View summary', }) - // wait for feedback received message to be sent - await tab.waitForEvent(() => tab.getChatItems().length > 16, { + await tab.waitForEvent(() => tab.getChatItems().length > 14, { waitTimeoutInMs: 5000, waitIntervalInMs: 1000, }) - const feedbackReceivedMessage = tab.getChatItems().pop() - assert.strictEqual(feedbackReceivedMessage?.body?.includes('Feedback received') ?? undefined, true) + const viewSummaryChatItem = tab.getChatItems().pop() + assert.strictEqual(viewSummaryChatItem?.body?.includes('view a summary'), true) }) it('CANNOT do a Java 21 to Java 17 transformation', async () => { diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index f462b216261..5f45cc66d12 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -28,7 +28,6 @@ import { validateCanCompileProject, getValidSQLConversionCandidateProjects, openHilPomFile, - getFeedbackCommentData, } from '../../../codewhisperer/commands/startTransformByQ' import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model' import { @@ -62,7 +61,6 @@ import { validateSQLMetadataFile, } from '../../../codewhisperer/service/transformByQ/transformFileHandler' import { getAuthType } from '../../../auth/utils' -import globals from '../../../shared/extensionGlobals' // These events can be interactions within the chat, // or elsewhere in the IDE @@ -372,9 +370,6 @@ export class GumbyController { case ButtonActions.CONFIRM_SKIP_TESTS_FORM: await this.handleSkipTestsSelection(message) break - case ButtonActions.CONFIRM_FEEDBACK_FORM: - await this.handleFeedback(message) - break case ButtonActions.CONFIRM_SELECTIVE_TRANSFORMATION_FORM: await this.handleOneOrMultipleDiffs(message) break @@ -639,17 +634,6 @@ export class GumbyController { } } - private async handleFeedback(message: any) { - const canRerunJob = message.formSelectedValues['TransformFeedbackRerunJob'] - const canViewLogs = message.formSelectedValues['TransformFeedbackViewLogs'] - const comment = `Permission to re-run job: ${canRerunJob}\nPermission to view logs: ${canViewLogs}\n${getFeedbackCommentData()}` - this.messenger.sendFeedbackReceivedMessage(canRerunJob, canViewLogs, message.tabID) - if (comment.toLowerCase().includes('yes')) { - // post feedback if user says yes to at least one of the questions - await globals.telemetry.postFeedback({ comment: comment, sentiment: 'Positive' }) - } - } - private resetTransformationChatFlow() { this.sessionStorage.getSession().conversationState = ConversationState.IDLE } diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 5de13cad10b..1f80aaf26c6 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -542,15 +542,6 @@ export class Messenger { }) } - if ( - transformByQState.isPartiallySucceeded() && - message === CodeWhispererConstants.viewProposedChangesChatMessage - ) { - // get permission to re-run job and view logs after partially successful job is downloaded - // TODO: uncomment this when feature is ready - // this.sendFeedbackFormMessage(tabID) - } - this.dispatcher.sendChatMessage( new ChatMessage( { @@ -808,64 +799,4 @@ ${codeSnippet} ) ) } - - public sendFeedbackFormMessage(tabID: string) { - const formItems: ChatItemFormItem[] = [] - formItems.push({ - id: 'TransformFeedbackRerunJob', - type: 'radiogroup', - title: 'To improve our service, do we have permission to re-run your job? (you will *not* be charged)', - mandatory: true, - options: [ - { - value: 'Yes', - label: 'Yes', - }, - { - value: 'No', - label: 'No', - }, - ], - }) - - formItems.push({ - id: 'TransformFeedbackViewLogs', - type: 'radiogroup', - title: 'Do we also have permission to view the logs associated with your job?', - mandatory: true, - options: [ - { - value: 'Yes', - label: 'Yes', - }, - { - value: 'No', - label: 'No', - }, - ], - }) - - this.dispatcher.sendChatPrompt( - new ChatPrompt( - { - message: 'Amazon Q Permissions Form', - formItems: formItems, - }, - 'FeedbackForm', - tabID, - false - ) - ) - } - - public sendFeedbackReceivedMessage(canRerunJob: string, canViewLogs: string, tabID: string) { - const message = `### Response received -------------- -| | | -| :------------------- | -------: | -| **Permission to re-run job** | ${canRerunJob} | -| **Permission to view logs** | ${canViewLogs} | - ` - this.dispatcher.sendChatMessage(new ChatMessage({ message, messageType: 'prompt' }, tabID)) - } } diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts index 88395c029ca..a7275ac98a3 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts @@ -18,7 +18,6 @@ export enum ButtonActions { CONFIRM_SQL_CONVERSION_TRANSFORMATION_FORM = 'gumbySQLConversionTransformFormConfirm', CANCEL_TRANSFORMATION_FORM = 'gumbyTransformFormCancel', // shared between Language Upgrade & SQL Conversion CONFIRM_SKIP_TESTS_FORM = 'gumbyTransformSkipTestsFormConfirm', - CONFIRM_FEEDBACK_FORM = 'gumbyFeedbackFormConfirm', CONFIRM_SELECTIVE_TRANSFORMATION_FORM = 'gumbyTransformOneOrMultipleDiffsFormConfirm', SELECT_SQL_CONVERSION_METADATA_FILE = 'gumbySQLConversionMetadataTransformFormConfirm', CONFIRM_DEPENDENCY_FORM = 'gumbyTransformDependencyFormConfirm', From e26ab78a1fca8b4017e07ea6fc13ee5a7d76bf12 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:41:54 -0400 Subject: [PATCH 08/10] deps(amazonq): mynah-ui 4.27.0 #6901 Upgrade to mynah-ui 4.27.0 https://github.com/aws/mynah-ui/releases/tag/v4.27.0 Includes a visual change: Code blocks now have max-height of 21 lines --- package-lock.json | 8 ++++---- .../Feature-e5ed7aac-542d-41ee-8abe-d32db5de73ae.json | 4 ++++ packages/core/package.json | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Feature-e5ed7aac-542d-41ee-8abe-d32db5de73ae.json diff --git a/package-lock.json b/package-lock.json index 90b376315f5..3a4c474f54e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11957,9 +11957,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.26.1.tgz", - "integrity": "sha512-qUgQ6NVmiCSp6qF43cM6522U8QtBTBbqDv5yKS/5tl9cEQuZJSfKDq2zFUstQNZmsw0GE8p/NboTDo3mRv4sSQ==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.27.0.tgz", + "integrity": "sha512-DkwenNLU+BHUYf0ntwkGllfjM+LrQXvCTiV2Eh7zV3HA59+ba5e34zge3sf7F71XukR9ckoKHXrxc0GjvVchbA==", "hasInstallScript": true, "dependencies": { "escape-html": "^1.0.3", @@ -26745,7 +26745,7 @@ "@aws-sdk/s3-request-presigner": "<3.731.0", "@aws-sdk/smithy-client": "<3.731.0", "@aws-sdk/util-arn-parser": "<3.731.0", - "@aws/mynah-ui": "^4.26.1", + "@aws/mynah-ui": "^4.27.0", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", diff --git a/packages/amazonq/.changes/next-release/Feature-e5ed7aac-542d-41ee-8abe-d32db5de73ae.json b/packages/amazonq/.changes/next-release/Feature-e5ed7aac-542d-41ee-8abe-d32db5de73ae.json new file mode 100644 index 00000000000..028feced804 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-e5ed7aac-542d-41ee-8abe-d32db5de73ae.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Amazon Q chat: Code blocks in chat messages have a max-height of 21 lines and can be scrolled inside" +} diff --git a/packages/core/package.json b/packages/core/package.json index c58b81ad166..83c1dd5b96d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -498,6 +498,7 @@ "@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client", "@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming", "@aws-sdk/client-api-gateway": "<3.731.0", + "@aws-sdk/client-apprunner": "<3.731.0", "@aws-sdk/client-cloudcontrol": "<3.731.0", "@aws-sdk/client-cloudformation": "<3.731.0", "@aws-sdk/client-cloudwatch-logs": "<3.731.0", @@ -512,7 +513,6 @@ "@aws-sdk/client-ssm": "<3.731.0", "@aws-sdk/client-sso": "<3.731.0", "@aws-sdk/client-sso-oidc": "<3.731.0", - "@aws-sdk/client-apprunner": "<3.731.0", "@aws-sdk/credential-provider-env": "<3.731.0", "@aws-sdk/credential-provider-process": "<3.731.0", "@aws-sdk/credential-provider-sso": "<3.731.0", @@ -522,7 +522,7 @@ "@aws-sdk/s3-request-presigner": "<3.731.0", "@aws-sdk/smithy-client": "<3.731.0", "@aws-sdk/util-arn-parser": "<3.731.0", - "@aws/mynah-ui": "^4.26.1", + "@aws/mynah-ui": "^4.27.0", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", From 7a47e33652919b32a61f632a0f9f8317b0358c63 Mon Sep 17 00:00:00 2001 From: tsmithsz <84354541+tsmithsz@users.noreply.github.com> Date: Tue, 1 Apr 2025 13:09:48 -0700 Subject: [PATCH 09/10] feat(chat): Add warning UI for high risk bash command (#6895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem AppSec has required that we give the user an indication if they are about to run a potentially dangerous command ## Solution - Parse bash command to determine whether or not commands are dangerous or not. - Display message warning to user ## Tests ![Screenshot 2025-03-31 at 1 03 14 PM](https://github.com/user-attachments/assets/a3bcd519-46de-47c9-a947-32f58dd6ca51) --- - 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. --- package-lock.json | 8 + packages/core/package.json | 3 +- .../controllers/chat/controller.ts | 4 +- .../controllers/chat/messenger/messenger.ts | 18 +- .../src/codewhispererChat/tools/chatStream.ts | 7 +- .../codewhispererChat/tools/executeBash.ts | 216 ++++++++++++++---- .../src/codewhispererChat/tools/toolUtils.ts | 10 +- .../tools/executeBash.test.ts | 4 +- .../tools/toolShared.test.ts | 14 +- 9 files changed, 210 insertions(+), 74 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fd4035f62d..a4d15a725ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24096,6 +24096,13 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/shlex": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/shlex/-/shlex-2.1.2.tgz", + "integrity": "sha512-Nz6gtibMVgYeMEhUjp2KuwAgqaJA1K155dU/HuDaEJUGgnmYfVtVZah+uerVWdH8UGnyahhDCgABbYTbs254+w==", + "dev": true, + "license": "MIT" + }, "node_modules/side-channel": { "version": "1.0.6", "license": "MIT", @@ -26843,6 +26850,7 @@ "readline-sync": "^1.4.9", "sass": "^1.49.8", "sass-loader": "^16.0.2", + "shlex": "^2.1.2", "sinon": "^14.0.0", "style-loader": "^3.3.1", "ts-node": "^10.9.1", diff --git a/packages/core/package.json b/packages/core/package.json index 7d64a740305..ad1b7b732f9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -492,7 +492,8 @@ "umd-compat-loader": "^2.1.2", "vue-loader": "^17.2.2", "vue-style-loader": "^4.1.3", - "webfont": "^11.2.26" + "webfont": "^11.2.26", + "shlex": "^2.1.2" }, "dependencies": { "@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client", diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 4f58b3ab85a..aa8f0ac992c 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -662,7 +662,9 @@ export class ChatController { try { await ToolUtils.validate(tool) - const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse) + const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, { + requiresAcceptance: false, + }) const output = await ToolUtils.invoke(tool, chatStream) toolResults.push({ diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index d19e88c0b47..942a2944542 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -45,6 +45,7 @@ import { ToolType, ToolUtils } from '../../../tools/toolUtils' import { ChatStream } from '../../../tools/chatStream' import path from 'path' import { getWorkspaceForFile } from '../../../../shared/utilities/workspaceUtils' +import { CommandValidation } from '../../../tools/executeBash' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -220,11 +221,12 @@ export class Messenger { if (tool.type === ToolType.FsWrite) { session.setShowDiffOnFileWrite(true) } - const requiresAcceptance = ToolUtils.requiresAcceptance(tool) - const chatStream = new ChatStream(this, tabID, triggerID, toolUse, requiresAcceptance) + const validation = ToolUtils.requiresAcceptance(tool) + + const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation) ToolUtils.queueDescription(tool, chatStream) - if (!requiresAcceptance) { + if (!validation.requiresAcceptance) { // Need separate id for read tool and safe bash command execution as 'confirm-tool-use' id is required to change button status from `Confirm` to `Confirmed` state in cwChatConnector.ts which will impact generic tool execution. this.dispatcher.sendCustomFormActionMessage( new CustomFormActionMessage(tabID, { @@ -432,17 +434,21 @@ export class Messenger { tabID: string, triggerID: string, toolUse: ToolUse | undefined, - requiresAcceptance = false + validation: CommandValidation ) { const buttons: ChatItemButton[] = [] let fileList: ChatItemContent['fileList'] = undefined - if (requiresAcceptance && toolUse?.name === ToolType.ExecuteBash) { + if (validation.requiresAcceptance && toolUse?.name === ToolType.ExecuteBash) { buttons.push({ id: 'confirm-tool-use', text: 'Confirm', position: 'outside', status: 'info', }) + + if (validation.warning) { + message = validation.warning + message + } } else if (toolUse?.name === ToolType.FsWrite) { // FileList const absoluteFilePath = (toolUse?.input as any).path @@ -471,7 +477,7 @@ export class Messenger { this.dispatcher.sendChatMessage( new ChatMessage( { - message, + message: message, messageType: 'answer-part', followUps: undefined, followUpsHeader: undefined, diff --git a/packages/core/src/codewhispererChat/tools/chatStream.ts b/packages/core/src/codewhispererChat/tools/chatStream.ts index 621da53c1b7..07af534c06e 100644 --- a/packages/core/src/codewhispererChat/tools/chatStream.ts +++ b/packages/core/src/codewhispererChat/tools/chatStream.ts @@ -7,6 +7,7 @@ import { Writable } from 'stream' import { getLogger } from '../../shared/logger/logger' import { Messenger } from '../controllers/chat/messenger/messenger' import { ToolUse } from '@amzn/codewhisperer-streaming' +import { CommandValidation } from './executeBash' /** * A writable stream that feeds each chunk/line to the chat UI. @@ -20,7 +21,7 @@ export class ChatStream extends Writable { private readonly tabID: string, private readonly triggerID: string, private readonly toolUse: ToolUse | undefined, - private readonly requiresAcceptance = false, + private readonly validation: CommandValidation, private readonly logger = getLogger('chatStream') ) { super() @@ -37,7 +38,7 @@ export class ChatStream extends Writable { this.tabID, this.triggerID, this.toolUse, - this.requiresAcceptance + this.validation ) callback() } @@ -49,7 +50,7 @@ export class ChatStream extends Writable { this.tabID, this.triggerID, this.toolUse, - this.requiresAcceptance + this.validation ) } callback() diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index 73aa52e3762..bc6a517210d 100644 --- a/packages/core/src/codewhispererChat/tools/executeBash.ts +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -5,20 +5,130 @@ import { Writable } from 'stream' import { getLogger } from '../../shared/logger/logger' -import { fs } from '../../shared/fs/fs' // e.g. for getUserHomeDir() +import { fs } from '../../shared/fs/fs' import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' import { InvokeOutput, OutputKind, sanitizePath } from './toolShared' +import { split } from 'shlex' -export const readOnlyCommands: string[] = ['ls', 'cat', 'echo', 'pwd', 'which', 'head', 'tail'] +export enum CommandCategory { + ReadOnly, + HighRisk, + Destructive, +} + +export const dangerousPatterns = new Set(['<(', '$(', '`', '>', '&&', '||']) +export const commandCategories = new Map([ + // ReadOnly commands + ['ls', CommandCategory.ReadOnly], + ['cat', CommandCategory.ReadOnly], + ['bat', CommandCategory.ReadOnly], + ['pwd', CommandCategory.ReadOnly], + ['echo', CommandCategory.ReadOnly], + ['file', CommandCategory.ReadOnly], + ['less', CommandCategory.ReadOnly], + ['more', CommandCategory.ReadOnly], + ['tree', CommandCategory.ReadOnly], + ['find', CommandCategory.ReadOnly], + ['top', CommandCategory.ReadOnly], + ['htop', CommandCategory.ReadOnly], + ['ps', CommandCategory.ReadOnly], + ['df', CommandCategory.ReadOnly], + ['du', CommandCategory.ReadOnly], + ['free', CommandCategory.ReadOnly], + ['uname', CommandCategory.ReadOnly], + ['date', CommandCategory.ReadOnly], + ['whoami', CommandCategory.ReadOnly], + ['which', CommandCategory.ReadOnly], + ['ping', CommandCategory.ReadOnly], + ['ifconfig', CommandCategory.ReadOnly], + ['ip', CommandCategory.ReadOnly], + ['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], + + // Destructive commands + ['rm', CommandCategory.Destructive], + ['dd', CommandCategory.Destructive], + ['mkfs', CommandCategory.Destructive], + ['fdisk', CommandCategory.Destructive], + ['shutdown', CommandCategory.Destructive], + ['reboot', CommandCategory.Destructive], + ['poweroff', CommandCategory.Destructive], + ['sudo', CommandCategory.Destructive], + ['su', CommandCategory.Destructive], + ['useradd', CommandCategory.Destructive], + ['userdel', CommandCategory.Destructive], + ['passwd', CommandCategory.Destructive], + ['visudo', CommandCategory.Destructive], + ['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], +]) export const maxBashToolResponseSize: number = 1024 * 1024 // 1MB export const lineCount: number = 1024 -export const dangerousPatterns: string[] = ['|', '<(', '$(', '`', '>', '&&', '||'] +export const destructiveCommandWarningMessage = '⚠️ WARNING: Destructive command detected:\n\n' +export const highRiskCommandWarningMessage = '⚠️ WARNING: High risk command detected:\n\n' export interface ExecuteBashParams { command: string cwd?: string } +export interface CommandValidation { + requiresAcceptance: boolean + warning?: string +} + export class ExecuteBash { private readonly command: string private readonly workingDirectory?: string @@ -34,7 +144,7 @@ export class ExecuteBash { throw new Error('Bash command cannot be empty.') } - const args = ExecuteBash.parseCommand(this.command) + const args = split(this.command) if (!args || args.length === 0) { throw new Error('No command found.') } @@ -46,22 +156,67 @@ export class ExecuteBash { } } - public requiresAcceptance(): boolean { + public requiresAcceptance(): CommandValidation { try { - const args = ExecuteBash.parseCommand(this.command) + const args = split(this.command) if (!args || args.length === 0) { - return true + return { requiresAcceptance: true } } - if (args.some((arg) => dangerousPatterns.some((pattern) => arg.includes(pattern)))) { - return true + // Split commands by pipe and process each segment + let currentCmd: string[] = [] + const allCommands: string[][] = [] + + for (const arg of args) { + if (arg === '|') { + if (currentCmd.length > 0) { + allCommands.push(currentCmd) + } + currentCmd = [] + } else if (arg.includes('|')) { + return { requiresAcceptance: true } + } else { + currentCmd.push(arg) + } + } + + if (currentCmd.length > 0) { + allCommands.push(currentCmd) } - const command = args[0] - return !readOnlyCommands.includes(command) + for (const cmdArgs of allCommands) { + if (cmdArgs.length === 0) { + return { requiresAcceptance: true } + } + + const command = cmdArgs[0] + const category = commandCategories.get(command) + + switch (category) { + case CommandCategory.Destructive: + return { requiresAcceptance: true, warning: destructiveCommandWarningMessage } + case CommandCategory.HighRisk: + return { + requiresAcceptance: true, + warning: highRiskCommandWarningMessage, + } + case CommandCategory.ReadOnly: + if ( + cmdArgs.some((arg) => + Array.from(dangerousPatterns).some((pattern) => arg.includes(pattern)) + ) + ) { + return { requiresAcceptance: true, warning: highRiskCommandWarningMessage } + } + return { requiresAcceptance: false } + default: + return { requiresAcceptance: true, warning: highRiskCommandWarningMessage } + } + } + return { requiresAcceptance: true } } catch (error) { this.logger.warn(`Error while checking acceptance: ${(error as Error).message}`) - return true + return { requiresAcceptance: true } } } @@ -167,43 +322,6 @@ export class ExecuteBash { return output } - private static parseCommand(command: string): string[] | undefined { - const result: string[] = [] - let current = '' - let inQuote: string | undefined - let escaped = false - - for (const char of command) { - if (escaped) { - current += char - escaped = false - } else if (char === '\\') { - escaped = true - } else if (inQuote) { - if (char === inQuote) { - inQuote = undefined - } else { - current += char - } - } else if (char === '"' || char === "'") { - inQuote = char - } else if (char === ' ' || char === '\t') { - if (current) { - result.push(current) - current = '' - } - } else { - current += char - } - } - - if (current) { - result.push(current) - } - - return result - } - public queueDescription(updates: Writable): void { updates.write(`I will run the following shell command:\n`) updates.write('```bash\n' + this.command + '\n```') diff --git a/packages/core/src/codewhispererChat/tools/toolUtils.ts b/packages/core/src/codewhispererChat/tools/toolUtils.ts index 9eac453761c..52a073e951b 100644 --- a/packages/core/src/codewhispererChat/tools/toolUtils.ts +++ b/packages/core/src/codewhispererChat/tools/toolUtils.ts @@ -5,7 +5,7 @@ import { Writable } from 'stream' import { FsRead, FsReadParams } from './fsRead' import { FsWrite, FsWriteParams } from './fsWrite' -import { ExecuteBash, ExecuteBashParams } from './executeBash' +import { CommandValidation, ExecuteBash, ExecuteBashParams } from './executeBash' import { ToolResult, ToolResultContentBlock, ToolResultStatus, ToolUse } from '@amzn/codewhisperer-streaming' import { InvokeOutput } from './toolShared' import { ListDirectory, ListDirectoryParams } from './listDirectory' @@ -37,16 +37,16 @@ export class ToolUtils { } } - static requiresAcceptance(tool: Tool) { + static requiresAcceptance(tool: Tool): CommandValidation { switch (tool.type) { case ToolType.FsRead: - return false + return { requiresAcceptance: false } case ToolType.FsWrite: - return true + return { requiresAcceptance: true } case ToolType.ExecuteBash: return tool.tool.requiresAcceptance() case ToolType.ListDirectory: - return false + return { requiresAcceptance: false } } } diff --git a/packages/core/src/test/codewhispererChat/tools/executeBash.test.ts b/packages/core/src/test/codewhispererChat/tools/executeBash.test.ts index 68ec48d8851..38a2e411187 100644 --- a/packages/core/src/test/codewhispererChat/tools/executeBash.test.ts +++ b/packages/core/src/test/codewhispererChat/tools/executeBash.test.ts @@ -44,13 +44,13 @@ describe('ExecuteBash Tool', () => { it('set requiresAcceptance=true if the command has dangerous patterns', () => { const execBash = new ExecuteBash({ command: 'ls && rm -rf /' }) - const needsAcceptance = execBash.requiresAcceptance() + const needsAcceptance = execBash.requiresAcceptance().requiresAcceptance assert.equal(needsAcceptance, true, 'Should require acceptance for dangerous pattern') }) it('set requiresAcceptance=false if it is a read-only command', () => { const execBash = new ExecuteBash({ command: 'cat file.txt' }) - const needsAcceptance = execBash.requiresAcceptance() + const needsAcceptance = execBash.requiresAcceptance().requiresAcceptance assert.equal(needsAcceptance, false, 'Read-only command should not require acceptance') }) diff --git a/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts index b3092973214..8116059451c 100644 --- a/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts +++ b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts @@ -64,28 +64,28 @@ describe('ToolUtils', function () { describe('requiresAcceptance', function () { it('returns false for FsRead tool', function () { const tool: Tool = { type: ToolType.FsRead, tool: mockFsRead as unknown as FsRead } - assert.strictEqual(ToolUtils.requiresAcceptance(tool), false) + assert.strictEqual(ToolUtils.requiresAcceptance(tool).requiresAcceptance, false) }) it('returns true for FsWrite tool', function () { const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite } - assert.strictEqual(ToolUtils.requiresAcceptance(tool), true) + assert.strictEqual(ToolUtils.requiresAcceptance(tool).requiresAcceptance, true) }) it('delegates to the tool for ExecuteBash', function () { - mockExecuteBash.requiresAcceptance.returns(true) + mockExecuteBash.requiresAcceptance.returns({ requiresAcceptance: true }) const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash } - assert.strictEqual(ToolUtils.requiresAcceptance(tool), true) + assert.strictEqual(ToolUtils.requiresAcceptance(tool).requiresAcceptance, true) - mockExecuteBash.requiresAcceptance.returns(false) - assert.strictEqual(ToolUtils.requiresAcceptance(tool), false) + mockExecuteBash.requiresAcceptance.returns({ requiresAcceptance: false }) + assert.strictEqual(ToolUtils.requiresAcceptance(tool).requiresAcceptance, false) assert(mockExecuteBash.requiresAcceptance.calledTwice) }) it('returns false for ListDirectory tool', function () { const tool: Tool = { type: ToolType.ListDirectory, tool: mockListDirectory as unknown as ListDirectory } - assert.strictEqual(ToolUtils.requiresAcceptance(tool), false) + assert.strictEqual(ToolUtils.requiresAcceptance(tool).requiresAcceptance, false) }) }) From 9f805be4c87bf6e2af7209dc997890c13b2a7654 Mon Sep 17 00:00:00 2001 From: Jayakrishna P Date: Tue, 1 Apr 2025 13:26:36 -0700 Subject: [PATCH 10/10] feat(sagemaker): disable Q signout on SMUS #6850 ## Problem In Sagemaker Unified Studio(SMUS), user shouldnt be allowed to sign out of Q Chat extension as per business requirement. In addition to that, Q Chat Introduction message should display Q Free Tier is only supported with the upcoming SMUS release ## Solution 1. `isSageMaker` is used to identify if it is SageMaker AI (CodeEditor) which comes from [here](https://github.com/aws/sagemaker-code-editor/blob/main/patched-vscode/product.json#L2-L3) as shared by Sagemaker team. Since SageMaker Unified Studio also uses the same repository/branch for CodeEditor we required a new environment variable `SERVICE_NAME` to differentiate SMUS case. 2. Read `SERVICE_NAME` and set a new vscode env context variable `aws.isSagemakerStudio` to control visibility of Sign out label 3. Sign out option to users is controlled at two places: - VS Code command palette - Quick pick UI action - Both the sign out options are disabled based on the SMUS flag 4. Similarly Q Chat Introduction message in WebView is modified for SMUS case alone --- ...-390d2096-ea1e-43b9-a889-2494d6e9e6d3.json | 4 ++ ...-3c42f632-46b8-4e59-b5fa-75d638f3f08d.json | 4 ++ packages/amazonq/package.json | 4 +- packages/amazonq/src/extension.ts | 4 ++ .../webview/generators/webViewContent.ts | 3 +- packages/core/src/amazonq/webview/ui/main.ts | 9 +++-- .../src/amazonq/webview/ui/tabs/constants.ts | 14 +++++-- .../src/amazonq/webview/ui/tabs/generator.ts | 11 ++++-- .../src/codewhisperer/ui/statusBarMenu.ts | 5 ++- packages/core/src/extension.ts | 6 ++- .../core/src/shared/extensionUtilities.ts | 37 ++++++++++++++++--- packages/core/src/shared/vscode/constants.ts | 9 +++++ packages/core/src/shared/vscode/setContext.ts | 1 + .../commands/basicCommands.test.ts | 37 +++++++++++++++++++ 14 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Feature-390d2096-ea1e-43b9-a889-2494d6e9e6d3.json create mode 100644 packages/amazonq/.changes/next-release/Feature-3c42f632-46b8-4e59-b5fa-75d638f3f08d.json create mode 100644 packages/core/src/shared/vscode/constants.ts diff --git a/packages/amazonq/.changes/next-release/Feature-390d2096-ea1e-43b9-a889-2494d6e9e6d3.json b/packages/amazonq/.changes/next-release/Feature-390d2096-ea1e-43b9-a889-2494d6e9e6d3.json new file mode 100644 index 00000000000..8bb3bff4eb4 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-390d2096-ea1e-43b9-a889-2494d6e9e6d3.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "SageMaker Unified Studio: Disable Sign out" +} diff --git a/packages/amazonq/.changes/next-release/Feature-3c42f632-46b8-4e59-b5fa-75d638f3f08d.json b/packages/amazonq/.changes/next-release/Feature-3c42f632-46b8-4e59-b5fa-75d638f3f08d.json new file mode 100644 index 00000000000..9b4965a6fa4 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-3c42f632-46b8-4e59-b5fa-75d638f3f08d.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "SageMaker Unified Studio: Update Q Chat Introduction message" +} diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 087602a00bf..053092dbc4f 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -359,7 +359,7 @@ }, { "command": "aws.amazonq.signout", - "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected", + "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected && !aws.isSageMakerUnifiedStudio", "group": "2_amazonQ@4" }, { @@ -635,7 +635,7 @@ "title": "%AWS.command.codewhisperer.signout%", "category": "%AWS.amazonq.title%", "icon": "$(debug-disconnect)", - "enablement": "aws.codewhisperer.connected" + "enablement": "aws.codewhisperer.connected && !aws.isSageMakerUnifiedStudio" }, { "command": "aws.amazonq.learnMore", diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index ae9e0c5657d..7a62627c6a6 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -32,6 +32,7 @@ import { setupUninstallHandler, maybeShowMinVscodeWarning, Experiments, + isSageMaker, } from 'aws-core-vscode/shared' import { ExtStartUpSources } from 'aws-core-vscode/telemetry' import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' @@ -146,6 +147,9 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // Hide the Amazon Q tree in toolkit explorer await setContext('aws.toolkit.amazonq.dismissed', true) + // set context var to check if its SageMaker Unified Studio or not + await setContext('aws.isSageMakerUnifiedStudio', isSageMaker('SMUS')) + // reload webviews await vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction') diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts index 81c6cb00a35..22cac41ec27 100644 --- a/packages/core/src/amazonq/webview/generators/webViewContent.ts +++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts @@ -83,6 +83,7 @@ export class WebViewContentGenerator { const disclaimerAcknowledged = globals.globalState.tryGet('aws.amazonq.disclaimerAcknowledged', Boolean, false) const welcomeLoadCount = globals.globalState.tryGet('aws.amazonq.welcomeChatShowCount', Number, 0) + const isSMUS = isSageMaker('SMUS') return ` @@ -91,7 +92,7 @@ export class WebViewContentGenerator { const init = () => { createMynahUI(acquireVsCodeApi(), ${ (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' - },${featureConfigsString},${welcomeLoadCount},${disclaimerAcknowledged},${disabledCommandsString}); + },${featureConfigsString},${welcomeLoadCount},${disclaimerAcknowledged},${disabledCommandsString},${isSMUS}); } ` diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 5ae03840f8f..195f8fc7dfe 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -47,7 +47,8 @@ export const createMynahUI = ( featureConfigsSerialized: [string, FeatureContext][], welcomeCount: number, disclaimerAcknowledged: boolean, - disabledCommands?: string[] + disabledCommands?: string[], + isSMUS?: boolean ) => { let disclaimerCardActive = !disclaimerAcknowledged // eslint-disable-next-line prefer-const @@ -624,7 +625,7 @@ export const createMynahUI = ( tabsStorage.updateTabTypeFromUnknown(tabID, 'cwc') mynahUI?.updateTabDefaults({ store: { - ...tabDataGenerator.getTabData('cwc', true), + ...tabDataGenerator.getTabData('cwc', true, undefined, isSMUS), tabHeaderDetails: void 0, compactMode: false, tabBackground: false, @@ -931,7 +932,7 @@ export const createMynahUI = ( store: { ...(showWelcomePage() ? welcomeScreenTabData(tabDataGenerator).store - : tabDataGenerator.getTabData('cwc', true)), + : tabDataGenerator.getTabData('cwc', true, undefined, isSMUS)), ...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}), }, }, @@ -939,7 +940,7 @@ export const createMynahUI = ( defaults: { store: showWelcomePage() ? welcomeScreenTabData(tabDataGenerator).store - : tabDataGenerator.getTabData('cwc', true), + : tabDataGenerator.getTabData('cwc', true, undefined, isSMUS), }, config: { maxTabs: 10, diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index 1ac0c5ae156..19d48fa9695 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -6,6 +6,16 @@ import { TabType } from '../storages/tabsStorage' import { QuickActionCommandGroup } from '@aws/mynah-ui' import { userGuideURL } from '../texts/constants' +const qChatIntroMessage = `Hi, I'm Amazon Q. I can answer your software development questions. + Ask me to explain, debug, or optimize your code. + You can enter \`/\` to see a list of quick actions. Use \`@\` to add saved prompts, files, folders, or your entire workspace as context.` + +export const qChatIntroMessageForSMUS = `Hi, I'm Amazon Q. I can answer your software development questions.\n\ + Ask me to explain, debug, or optimize your code.\n\ + You can enter \`/\` to see a list of quick actions. Use \`@\` to add saved prompts, files, folders, or your entire workspace as context. + You are now using Q free tier.\n\ + ` + export type TabTypeData = { title: string placeholder: string @@ -26,9 +36,7 @@ export const workspaceCommand: QuickActionCommandGroup = { export const commonTabData: TabTypeData = { title: 'Chat', placeholder: 'Ask a question. Use @ to add context, / for quick actions', - welcome: `Hi, I'm Amazon Q. I can answer your software development questions. - Ask me to explain, debug, or optimize your code. - You can enter \`/\` to see a list of quick actions. Use \`@\` to add saved prompts, files, folders, or your entire workspace as context.`, + welcome: qChatIntroMessage, contextCommands: [workspaceCommand], } diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts index f037e4c56ef..aeff92c33f2 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts @@ -7,7 +7,7 @@ import { ChatItemType, MynahUIDataModel, QuickActionCommandGroup } from '@aws/my import { TabType } from '../storages/tabsStorage' import { FollowUpGenerator } from '../followUps/generator' import { QuickActionGenerator } from '../quickActions/generator' -import { TabTypeDataMap } from './constants' +import { qChatIntroMessageForSMUS, TabTypeDataMap } from './constants' import { agentWalkthroughDataModel } from '../walkthrough/agent' import { FeatureContext } from '../../../../shared/featureConfig' @@ -39,7 +39,12 @@ export class TabDataGenerator { this.highlightCommand = props.commandHighlight } - public getTabData(tabType: TabType, needWelcomeMessages: boolean, taskName?: string): MynahUIDataModel { + public getTabData( + tabType: TabType, + needWelcomeMessages: boolean, + taskName?: string, + isSMUS?: boolean + ): MynahUIDataModel { if (tabType === 'agentWalkthrough') { return agentWalkthroughDataModel } @@ -59,7 +64,7 @@ export class TabDataGenerator { ? [ { type: ChatItemType.ANSWER, - body: TabTypeDataMap[tabType].welcome, + body: isSMUS ? qChatIntroMessageForSMUS : TabTypeDataMap[tabType].welcome, }, { type: ChatItemType.ANSWER, diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index 9b5fa43672e..01a59f61fc9 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -30,6 +30,7 @@ import { Commands } from '../../shared/vscode/commands2' import { createExitButton } from '../../shared/ui/buttons' import { telemetry } from '../../shared/telemetry/telemetry' import { getLogger } from '../../shared/logger/logger' +import { isSageMaker } from '../../shared/extensionUtilities' function getAmazonQCodeWhispererNodes() { const autoTriggerEnabled = CodeSuggestionsState.instance.isSuggestionsEnabled() @@ -92,7 +93,9 @@ export function getQuickPickItems(): DataQuickPickItem[] { // Add settings and signout createSeparator(), createSettingsNode(), - ...(AuthUtil.instance.isConnected() && !hasVendedIamCredentials() ? [createSignout()] : []), + ...(AuthUtil.instance.isConnected() && !(hasVendedIamCredentials() || isSageMaker('SMUS')) + ? [createSignout()] + : []), ] return children diff --git a/packages/core/src/extension.ts b/packages/core/src/extension.ts index 0951a397a7b..e400c3e0ddb 100644 --- a/packages/core/src/extension.ts +++ b/packages/core/src/extension.ts @@ -17,7 +17,7 @@ import globals, { initialize, isWeb } from './shared/extensionGlobals' import { join } from 'path' import { Commands } from './shared/vscode/commands2' import { endpointsFileUrl, githubCreateIssueUrl, githubUrl } from './shared/constants' -import { getIdeProperties, aboutExtension, getDocUrl } from './shared/extensionUtilities' +import { getIdeProperties, aboutExtension, getDocUrl, isSageMaker } from './shared/extensionUtilities' import { logAndShowError, logAndShowWebviewError } from './shared/utilities/logAndShowUtils' import { telemetry } from './shared/telemetry/telemetry' import { openUrl } from './shared/utilities/vsCodeUtils' @@ -54,6 +54,7 @@ import { AWSClientBuilderV3 } from './shared/awsClientBuilderV3' import { setupUninstallHandler } from './shared/handleUninstall' import { maybeShowMinVscodeWarning } from './shared/extensionStartup' import { getLogger } from './shared/logger/logger' +import { setContext } from './shared/vscode/setContext' disableAwsSdkWarning() @@ -118,6 +119,9 @@ export async function activateCommon( // telemetry await activateTelemetry(context, globals.awsContext, Settings.instance, 'AWS Toolkit For VS Code') + // set context var to identify if its SageMaker Unified Studio or not + await setContext('aws.isSageMakerUnifiedStudio', isSageMaker('SMUS')) + // Create this now, but don't call vscode.window.registerUriHandler() until after all // Toolkit services have a chance to register their path handlers. #4105 globals.uriHandler = new UriHandler() diff --git a/packages/core/src/shared/extensionUtilities.ts b/packages/core/src/shared/extensionUtilities.ts index 184ae028cc1..a7be7a2bef4 100644 --- a/packages/core/src/shared/extensionUtilities.ts +++ b/packages/core/src/shared/extensionUtilities.ts @@ -22,13 +22,16 @@ import { samDeployDocUrl, samInitDocUrl, } from './constants' +import { + cloud9Appname, + cloud9CnAppname, + sageMakerAppname, + sageMakerUnifiedStudio, + vscodeAppname, +} from './vscode/constants' const localize = nls.loadMessageBundle() -const vscodeAppname = 'Visual Studio Code' -const cloud9Appname = 'AWS Cloud9' -const cloud9CnAppname = 'Amazon Cloud9' -const sageMakerAppname = 'SageMaker Code Editor' const notInitialized = 'notInitialized' function _isAmazonQ() { @@ -65,6 +68,8 @@ export function commandsPrefix(): string { } let computeRegion: string | undefined = notInitialized +let serviceName: string = notInitialized +let isSMUS: boolean = false export function getIdeType(): 'vscode' | 'cloud9' | 'sagemaker' | 'unknown' { if (vscode.env.appName === cloud9Appname || vscode.env.appName === cloud9CnAppname) { @@ -145,6 +150,14 @@ function createCloud9Properties(company: string): IdeProperties { } } +function isSageMakerUnifiedStudio(): boolean { + if (serviceName === notInitialized) { + serviceName = process.env.SERVICE_NAME ?? '' + isSMUS = serviceName === sageMakerUnifiedStudio + } + return isSMUS +} + /** * Decides if the current system is (the specified flavor of) Cloud9. */ @@ -157,8 +170,20 @@ export function isCloud9(flavor: 'classic' | 'codecatalyst' | 'any' = 'any'): bo return (flavor === 'classic' && !codecat) || (flavor === 'codecatalyst' && codecat) } -export function isSageMaker(): boolean { - return vscode.env.appName === sageMakerAppname +/** + * + * @param appName to identify the proper SM instance + * @returns true if the current system is SageMaker(SMAI or SMUS) + */ +export function isSageMaker(appName: string = 'SMAI'): boolean { + switch (appName) { + case 'SMAI': + return vscode.env.appName === sageMakerAppname + case 'SMUS': + return vscode.env.appName === sageMakerAppname && isSageMakerUnifiedStudio() + default: + return false + } } export function isCn(): boolean { diff --git a/packages/core/src/shared/vscode/constants.ts b/packages/core/src/shared/vscode/constants.ts new file mode 100644 index 00000000000..fbe266e0e0b --- /dev/null +++ b/packages/core/src/shared/vscode/constants.ts @@ -0,0 +1,9 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +export const vscodeAppname = 'Visual Studio Code' +export const cloud9Appname = 'AWS Cloud9' +export const cloud9CnAppname = 'Amazon Cloud9' +export const sageMakerAppname = 'SageMaker Code Editor' +export const sageMakerUnifiedStudio = 'SageMakerUnifiedStudio' diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts index 6768e457333..1196ea8cc1d 100644 --- a/packages/core/src/shared/vscode/setContext.ts +++ b/packages/core/src/shared/vscode/setContext.ts @@ -13,6 +13,7 @@ import * as vscode from 'vscode' export type contextKey = | 'aws.isDevMode' | 'aws.isSageMaker' + | 'aws.isSageMakerUnifiedStudio' | 'aws.isWebExtHost' | 'aws.isInternalUser' | 'aws.amazonq.showLoginView' diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 61adef196f2..6963e064544 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -68,6 +68,7 @@ import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhisperer import { confirm } from '../../../shared' import * as commentUtils from '../../../shared/utilities/commentUtils' import * as startCodeFixGeneration from '../../../codewhisperer/commands/startCodeFixGeneration' +import * as extUtils from '../../../shared/extensionUtilities' describe('CodeWhisperer-basicCommands', function () { let targetCommand: Command & vscode.Disposable @@ -520,6 +521,42 @@ describe('CodeWhisperer-basicCommands', function () { }) await listCodeWhispererCommands.execute() }) + + it('includes sign out when connected and not in SageMaker', async function () { + sinon.stub(AuthUtil.instance, 'isConnected').returns(true) + sinon.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) + sinon.stub(extUtils, 'isSageMaker').value(false) + await CodeScansState.instance.setScansEnabled(false) + + getTestWindow().onDidShowQuickPick((e) => { + e.assertContainsItems( + createAutoSuggestions(true), + createOpenReferenceLog(), + createGettingStarted(), + createAutoScans(false), + switchToAmazonQNode(), + ...genericItems(), + createSettingsNode(), + createSignout() + ) + e.dispose() + }) + + await listCodeWhispererCommands.execute() + }) + + it('shows expected items when connection is expired and in SageMaker', async function () { + sinon.stub(AuthUtil.instance, 'isConnected').returns(true) + sinon.stub(AuthUtil.instance, 'isConnectionExpired').returns(true) + sinon.stub(extUtils, 'isSageMaker').value(true) + + getTestWindow().onDidShowQuickPick((e) => { + e.assertContainsItems(createReconnect(), createLearnMore(), ...genericItems()) + e.dispose() + }) + + await listCodeWhispererCommands.execute() + }) }) describe('applySecurityFix', function () {