From 15efdd95b8d8e6c1a343f7c6cdf7880e6b6fac33 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Tue, 6 May 2025 13:24:17 -0400 Subject: [PATCH 01/48] feat(amazonq): enable inline suggestions through flare by default (#7232) ## Problem We want to enable inline suggestions from flare on this branch ## Solution - enable it - leave the toggle setting so its easy to turn off/on to compare behaviours --- - 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/extension.ts | 2 +- packages/amazonq/src/lsp/client.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 5ae9e397119..9d9c3061e6b 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -129,7 +129,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // for AL2, start LSP if glibc patch is found await activateAmazonqLsp(context) } - if (!Experiments.instance.get('amazonqLSPInline', false)) { + if (!Experiments.instance.get('amazonqLSPInline', true)) { await activateInlineCompletion() } diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index a8cb8d76a40..5e9a482e7fb 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -164,7 +164,7 @@ export async function startLanguageServer( return client.onReady().then(async () => { await auth.refreshConnection() - if (Experiments.instance.get('amazonqLSPInline', false)) { + if (Experiments.instance.get('amazonqLSPInline', true)) { const inlineManager = new InlineCompletionManager(client) inlineManager.registerInlineCompletion() toDispose.push( From da499d5471554ff8042369506511c09a871b4710 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Tue, 6 May 2025 13:25:25 -0400 Subject: [PATCH 02/48] fix(amazonq): respect suggestions enabled setting for auto trigger (#7233) ## Problem the isSuggestionsEnable setting isn't respected ## Solution if someone disabled suggestions via a setting or from the status bar then don't return anything for automatic triggers --- - 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/app/inline/completion.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index be390cef34c..6783658cd2d 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -16,6 +16,7 @@ import { Disposable, window, TextEditor, + InlineCompletionTriggerKind, } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { @@ -30,6 +31,7 @@ import { ReferenceInlineProvider, ReferenceLogViewProvider, ImportAdderProvider, + CodeSuggestionsState, } from 'aws-core-vscode/codewhisperer' export class InlineCompletionManager implements Disposable { @@ -180,6 +182,12 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem token: CancellationToken ): Promise { if (this.isNewSession) { + const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic + if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { + // return early when suggestions are disabled with auto trigger + return [] + } + // make service requests if it's a new session await this.recommendationService.getAllRecommendations( this.languageClient, From d57cac566ee4d9e87fbcf3436359e0910317e5e6 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Tue, 6 May 2025 15:22:56 -0400 Subject: [PATCH 03/48] refactor(amazonq): deprecate amazon q inline through vscode (#7237) ## Problem - we're moving to inline via flare ## Solution - deprecate amazon q inline ## Notes #### deprecation steps: 1. removed recommendation handler and recommendation service and all regular dependencies, since those are the bulk of inline suggestions 2. removed command registrations for onacceptance 3. removed tests that are no longer relevant to the vscode implementation since they are already in flare 4. modified the lineAnnotationController and activeStateControllers to comment out any missing imports, since those still need to be there in the new implementation 5. removed pagination calls, since those are now done through flare 6. remove keystroke handler, since that's now done by the vscode api 7. removed old cloud9 compatability code for inline #### Future PRs: - Refactor lineAnnoationController and activeStateControllers - re-implement the `aws.amazonq.refreshAnnotation` command - fix the inline e2e tests, since now they will fully go through flare instead of the recommendation handler - fix any unit tests that are now failing - updating the status bar when a request is in progress --- - 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/app/inline/activation.ts | 56 +- .../amazonq/test/e2e/inline/inline.test.ts | 19 +- .../commands/invokeRecommendation.test.ts | 43 -- .../commands/onAcceptance.test.ts | 64 -- .../commands/onInlineAcceptance.test.ts | 43 -- .../service/completionProvider.test.ts | 117 --- .../service/inlineCompletionService.test.ts | 255 ------ .../service/keyStrokeHandler.test.ts | 237 ------ .../service/recommendationHandler.test.ts | 271 ------- .../codewhisperer/service/telemetry.test.ts | 6 - packages/core/src/codewhisperer/activation.ts | 51 +- .../commands/invokeRecommendation.ts | 45 -- .../codewhisperer/commands/onAcceptance.ts | 85 -- .../commands/onInlineAcceptance.ts | 146 ---- packages/core/src/codewhisperer/index.ts | 14 - .../service/completionProvider.ts | 77 -- .../service/inlineCompletionService.ts | 139 +--- .../codewhisperer/service/keyStrokeHandler.ts | 267 ------- .../service/recommendationHandler.ts | 724 ------------------ .../service/recommendationService.ts | 122 --- .../views/activeStateController.ts | 74 +- .../views/lineAnnotationController.ts | 72 +- .../core/src/test/codewhisperer/testUtil.ts | 2 - .../codewhisperer/referenceTracker.test.ts | 125 --- .../codewhisperer/serviceInvocations.test.ts | 124 --- 25 files changed, 91 insertions(+), 3087 deletions(-) delete mode 100644 packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts delete mode 100644 packages/core/src/codewhisperer/commands/invokeRecommendation.ts delete mode 100644 packages/core/src/codewhisperer/commands/onAcceptance.ts delete mode 100644 packages/core/src/codewhisperer/commands/onInlineAcceptance.ts delete mode 100644 packages/core/src/codewhisperer/service/completionProvider.ts delete mode 100644 packages/core/src/codewhisperer/service/keyStrokeHandler.ts delete mode 100644 packages/core/src/codewhisperer/service/recommendationHandler.ts delete mode 100644 packages/core/src/codewhisperer/service/recommendationService.ts delete mode 100644 packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts delete mode 100644 packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts diff --git a/packages/amazonq/src/app/inline/activation.ts b/packages/amazonq/src/app/inline/activation.ts index d786047b2aa..f1a45c9158c 100644 --- a/packages/amazonq/src/app/inline/activation.ts +++ b/packages/amazonq/src/app/inline/activation.ts @@ -6,68 +6,27 @@ import vscode from 'vscode' import { AuthUtil, - CodeSuggestionsState, CodeWhispererCodeCoverageTracker, CodeWhispererConstants, - CodeWhispererSettings, - ConfigurationEntry, - DefaultCodeWhispererClient, - invokeRecommendation, isInlineCompletionEnabled, - KeyStrokeHandler, - RecommendationHandler, runtimeLanguageContext, TelemetryHelper, UserWrittenCodeTracker, vsCodeState, } from 'aws-core-vscode/codewhisperer' -import { Commands, getLogger, globals, sleep } from 'aws-core-vscode/shared' +import { globals, sleep } from 'aws-core-vscode/shared' export async function activate() { - const codewhispererSettings = CodeWhispererSettings.instance - const client = new DefaultCodeWhispererClient() - if (isInlineCompletionEnabled()) { await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() } - function getAutoTriggerStatus(): boolean { - return CodeSuggestionsState.instance.isSuggestionsEnabled() - } - - async function getConfigEntry(): Promise { - const isShowMethodsEnabled: boolean = - vscode.workspace.getConfiguration('editor').get('suggest.showMethods') || false - const isAutomatedTriggerEnabled: boolean = getAutoTriggerStatus() - const isManualTriggerEnabled: boolean = true - const isSuggestionsWithCodeReferencesEnabled = codewhispererSettings.isSuggestionsWithCodeReferencesEnabled() - - // TODO:remove isManualTriggerEnabled - return { - isShowMethodsEnabled, - isManualTriggerEnabled, - isAutomatedTriggerEnabled, - isSuggestionsWithCodeReferencesEnabled, - } - } - async function setSubscriptionsforInlineCompletion() { - RecommendationHandler.instance.subscribeSuggestionCommands() - /** * Automated trigger */ globals.context.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor(async (editor) => { - await RecommendationHandler.instance.onEditorChange() - }), - vscode.window.onDidChangeWindowState(async (e) => { - await RecommendationHandler.instance.onFocusChange() - }), - vscode.window.onDidChangeTextEditorSelection(async (e) => { - await RecommendationHandler.instance.onCursorChange(e) - }), vscode.workspace.onDidChangeTextDocument(async (e) => { const editor = vscode.window.activeTextEditor if (!editor) { @@ -105,19 +64,6 @@ export async function activate() { * Then this event can be processed by our code. */ await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - if (!RecommendationHandler.instance.isSuggestionVisible()) { - await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) - } - }), - // manual trigger - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - invokeRecommendation( - vscode.window.activeTextEditor as vscode.TextEditor, - client, - await getConfigEntry() - ).catch((e) => { - getLogger().error('invokeRecommendation failed: %s', (e as Error).message) - }) }) ) } diff --git a/packages/amazonq/test/e2e/inline/inline.test.ts b/packages/amazonq/test/e2e/inline/inline.test.ts index 43a9f67ab73..72442b5cc6d 100644 --- a/packages/amazonq/test/e2e/inline/inline.test.ts +++ b/packages/amazonq/test/e2e/inline/inline.test.ts @@ -14,7 +14,7 @@ import { toTextEditor, using, } from 'aws-core-vscode/test' -import { RecommendationHandler, RecommendationService, session } from 'aws-core-vscode/codewhisperer' +import { session } from 'aws-core-vscode/codewhisperer' import { Commands, globals, sleep, waitUntil, collectionUtil } from 'aws-core-vscode/shared' import { loginToIdC } from '../amazonq/utils/setup' @@ -54,7 +54,6 @@ describe('Amazon Q Inline', async function () { const events = getUserTriggerDecision() console.table({ 'telemetry events': JSON.stringify(events), - 'recommendation service status': RecommendationService.instance.isRunning, }) } @@ -76,24 +75,10 @@ describe('Amazon Q Inline', async function () { if (!suggestionShown) { throw new Error(`Suggestion did not show. Suggestion States: ${JSON.stringify(session.suggestionStates)}`) } - const suggestionVisible = await waitUntil( - async () => RecommendationHandler.instance.isSuggestionVisible(), - waitOptions - ) - if (!suggestionVisible) { - throw new Error( - `Suggestions failed to become visible. Suggestion States: ${JSON.stringify(session.suggestionStates)}` - ) - } console.table({ 'suggestions states': JSON.stringify(session.suggestionStates), - 'valid recommendation': RecommendationHandler.instance.isValidResponse(), - 'recommendation service status': RecommendationService.instance.isRunning, recommendations: session.recommendations, }) - if (!RecommendationHandler.instance.isValidResponse()) { - throw new Error('Did not find a valid response') - } } /** @@ -234,7 +219,7 @@ describe('Amazon Q Inline', async function () { if (name === 'automatic') { // It should never get triggered since its not a supported file type - assert.deepStrictEqual(RecommendationService.instance.isRunning, false) + // assert.deepStrictEqual(RecommendationService.instance.isRunning, false) } else { await getTestWindow().waitForMessage('currently not supported by Amazon Q inline suggestions') } diff --git a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts deleted file mode 100644 index 68cebe37bb1..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as sinon from 'sinon' -import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' -import { - ConfigurationEntry, - invokeRecommendation, - InlineCompletionService, - isInlineCompletionEnabled, - DefaultCodeWhispererClient, -} from 'aws-core-vscode/codewhisperer' - -describe('invokeRecommendation', function () { - describe('invokeRecommendation', function () { - let getRecommendationStub: sinon.SinonStub - let mockClient: DefaultCodeWhispererClient - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - getRecommendationStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') - }) - - afterEach(function () { - sinon.restore() - }) - - it('Should call getPaginatedRecommendation with OnDemand as trigger type when inline completion is enabled', async function () { - const mockEditor = createMockTextEditor() - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - await invokeRecommendation(mockEditor, mockClient, config) - assert.strictEqual(getRecommendationStub.called, isInlineCompletionEnabled()) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts deleted file mode 100644 index 0471aaa3601..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { onAcceptance, AcceptedSuggestionEntry, session, CodeWhispererTracker } from 'aws-core-vscode/codewhisperer' -import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' - -describe('onAcceptance', function () { - describe('onAcceptance', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - session.reset() - }) - - afterEach(function () { - sinon.restore() - session.reset() - }) - - it('Should enqueue an event object to tracker', async function () { - const mockEditor = createMockTextEditor() - const trackerSpy = sinon.spy(CodeWhispererTracker.prototype, 'enqueue') - const fakeReferences = [ - { - message: '', - licenseName: 'MIT', - repository: 'http://github.com/fake', - recommendationContentSpan: { - start: 0, - end: 10, - }, - }, - ] - await onAcceptance({ - editor: mockEditor, - range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), - effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), - acceptIndex: 0, - recommendation: "print('Hello World!')", - requestId: '', - sessionId: '', - triggerType: 'OnDemand', - completionType: 'Line', - language: 'python', - references: fakeReferences, - }) - const actualArg = trackerSpy.getCall(0).args[0] as AcceptedSuggestionEntry - assert.ok(trackerSpy.calledOnce) - assert.strictEqual(actualArg.originalString, 'def two_sum(nums, target):') - assert.strictEqual(actualArg.requestId, '') - assert.strictEqual(actualArg.sessionId, '') - assert.strictEqual(actualArg.triggerType, 'OnDemand') - assert.strictEqual(actualArg.completionType, 'Line') - assert.strictEqual(actualArg.language, 'python') - assert.deepStrictEqual(actualArg.startPosition, new vscode.Position(1, 0)) - assert.deepStrictEqual(actualArg.endPosition, new vscode.Position(1, 26)) - assert.strictEqual(actualArg.index, 0) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts deleted file mode 100644 index ed3bc99fa34..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' -import { onInlineAcceptance, RecommendationHandler, session } from 'aws-core-vscode/codewhisperer' - -describe('onInlineAcceptance', function () { - describe('onInlineAcceptance', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - session.reset() - }) - - afterEach(function () { - sinon.restore() - session.reset() - }) - - it('Should dispose inline completion provider', async function () { - const mockEditor = createMockTextEditor() - const spy = sinon.spy(RecommendationHandler.instance, 'disposeInlineCompletion') - await onInlineAcceptance({ - editor: mockEditor, - range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), - effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), - acceptIndex: 0, - recommendation: "print('Hello World!')", - requestId: '', - sessionId: '', - triggerType: 'OnDemand', - completionType: 'Line', - language: 'python', - references: undefined, - }) - assert.ok(spy.calledWith()) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts deleted file mode 100644 index 956999d64ad..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' - -import { - getCompletionItems, - getCompletionItem, - getLabel, - Recommendation, - RecommendationHandler, - session, -} from 'aws-core-vscode/codewhisperer' -import { createMockDocument, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' - -describe('completionProviderService', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('getLabel', function () { - it('should return correct label given recommendation longer than Constants.LABEL_LENGTH', function () { - const mockLongRecommendation = ` - const metaDataFile = path.join(__dirname, 'nls.metadata.json'); - const locale = getUserDefinedLocale(argvConfig);` - const expected = '\n const m..' - assert.strictEqual(getLabel(mockLongRecommendation), expected) - }) - - it('should return correct label given short recommendation', function () { - const mockShortRecommendation = 'function onReady()' - const expected = 'function onReady()..' - assert.strictEqual(getLabel(mockShortRecommendation), expected) - }) - }) - - describe('getCompletionItem', function () { - it('should return targetCompletionItem given input', function () { - session.startPos = new vscode.Position(0, 0) - RecommendationHandler.instance.requestId = 'mock_requestId_getCompletionItem' - session.sessionId = 'mock_sessionId_getCompletionItem' - const mockPosition = new vscode.Position(0, 1) - const mockRecommendationDetail: Recommendation = { - content: "\n\t\tconsole.log('Hello world!');\n\t}", - } - const mockRecommendationIndex = 1 - const mockDocument = createMockDocument('', 'test.ts', 'typescript') - const expected: vscode.CompletionItem = { - label: "\n\t\tconsole.log('Hell..", - kind: 1, - detail: 'CodeWhisperer', - documentation: new vscode.MarkdownString().appendCodeblock( - "\n\t\tconsole.log('Hello world!');\n\t}", - 'typescript' - ), - sortText: '0000000002', - preselect: true, - insertText: new vscode.SnippetString("\n\t\tconsole.log('Hello world!');\n\t}"), - keepWhitespace: true, - command: { - command: 'aws.amazonq.accept', - title: 'On acceptance', - arguments: [ - new vscode.Range(0, 0, 0, 0), - 1, - "\n\t\tconsole.log('Hello world!');\n\t}", - 'mock_requestId_getCompletionItem', - 'mock_sessionId_getCompletionItem', - 'OnDemand', - 'Line', - 'typescript', - undefined, - ], - }, - } - const actual = getCompletionItem( - mockDocument, - mockPosition, - mockRecommendationDetail, - mockRecommendationIndex - ) - assert.deepStrictEqual(actual.command, expected.command) - assert.strictEqual(actual.sortText, expected.sortText) - assert.strictEqual(actual.label, expected.label) - assert.strictEqual(actual.kind, expected.kind) - assert.strictEqual(actual.preselect, expected.preselect) - assert.strictEqual(actual.keepWhitespace, expected.keepWhitespace) - assert.strictEqual(JSON.stringify(actual.documentation), JSON.stringify(expected.documentation)) - assert.strictEqual(JSON.stringify(actual.insertText), JSON.stringify(expected.insertText)) - }) - }) - - describe('getCompletionItems', function () { - it('should return completion items for each non-empty recommendation', async function () { - session.recommendations = [ - { content: "\n\t\tconsole.log('Hello world!');\n\t}" }, - { content: '\nvar a = 10' }, - ] - const mockPosition = new vscode.Position(0, 0) - const mockDocument = createMockDocument('', 'test.ts', 'typescript') - const actual = getCompletionItems(mockDocument, mockPosition) - assert.strictEqual(actual.length, 2) - }) - - it('should return empty completion items when recommendation is empty', async function () { - session.recommendations = [] - const mockPosition = new vscode.Position(14, 83) - const mockDocument = createMockDocument() - const actual = getCompletionItems(mockDocument, mockPosition) - const expected: vscode.CompletionItem[] = [] - assert.deepStrictEqual(actual, expected) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts deleted file mode 100644 index 18fd7d2f21b..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import assert from 'assert' -import * as sinon from 'sinon' -import { - CodeWhispererStatusBar, - InlineCompletionService, - ReferenceInlineProvider, - RecommendationHandler, - CodeSuggestionsState, - ConfigurationEntry, - CWInlineCompletionItemProvider, - session, - AuthUtil, - listCodeWhispererCommandsId, - DefaultCodeWhispererClient, -} from 'aws-core-vscode/codewhisperer' -import { createMockTextEditor, resetCodeWhispererGlobalVariables, createMockDocument } from 'aws-core-vscode/test' - -describe('inlineCompletionService', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('getPaginatedRecommendation', function () { - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - - let mockClient: DefaultCodeWhispererClient - - beforeEach(async function () { - mockClient = new DefaultCodeWhispererClient() - await resetCodeWhispererGlobalVariables() - }) - - afterEach(function () { - sinon.restore() - }) - - it('should call checkAndResetCancellationTokens before showing inline and next token to be null', async function () { - const mockEditor = createMockTextEditor() - sinon.stub(RecommendationHandler.instance, 'getRecommendations').resolves({ - result: 'Succeeded', - errorMessage: undefined, - recommendationCount: 1, - }) - const checkAndResetCancellationTokensStub = sinon.stub( - RecommendationHandler.instance, - 'checkAndResetCancellationTokens' - ) - session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] - await InlineCompletionService.instance.getPaginatedRecommendation( - mockClient, - mockEditor, - 'OnDemand', - config - ) - assert.ok(checkAndResetCancellationTokensStub.called) - assert.strictEqual(RecommendationHandler.instance.hasNextToken(), false) - }) - }) - - describe('clearInlineCompletionStates', function () { - it('should remove inline reference and recommendations', async function () { - const fakeReferences = [ - { - message: '', - licenseName: 'MIT', - repository: 'http://github.com/fake', - recommendationContentSpan: { - start: 0, - end: 10, - }, - }, - ] - ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) - session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] - session.language = 'python' - - assert.ok(session.recommendations.length > 0) - await RecommendationHandler.instance.clearInlineCompletionStates() - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - assert.strictEqual(session.recommendations.length, 0) - }) - }) - - describe('truncateOverlapWithRightContext', function () { - const fileName = 'test.py' - const language = 'python' - const rightContext = 'return target\n' - const doc = `import math\ndef two_sum(nums, target):\n` - const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(0, 0), '') - - it('removes overlap with right context from suggestion', async function () { - const mockSuggestion = 'return target\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, '') - }) - - it('only removes the overlap part from suggestion', async function () { - const mockSuggestion = 'print(nums)\nreturn target\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, 'print(nums)\n') - }) - - it('only removes the last overlap pattern from suggestion', async function () { - const mockSuggestion = 'return target\nprint(nums)\nreturn target\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, 'return target\nprint(nums)\n') - }) - - it('returns empty string if the remaining suggestion only contains white space', async function () { - const mockSuggestion = 'return target\n ' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, '') - }) - - it('returns the original suggestion if no match found', async function () { - const mockSuggestion = 'import numpy\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, 'import numpy\n') - }) - - it('ignores the space at the end of recommendation', async function () { - const mockSuggestion = 'return target\n\n\n\n\n' - const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) - const cursorPosition = new vscode.Position(2, 0) - const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) - assert.strictEqual(result, '') - }) - }) -}) - -describe('CWInlineCompletionProvider', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('provideInlineCompletionItems', function () { - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - afterEach(function () { - sinon.restore() - }) - - it('should return undefined if position is before RecommendationHandler start pos', async function () { - const position = new vscode.Position(0, 0) - const document = createMockDocument() - const fakeContext = { triggerKind: 0, selectedCompletionInfo: undefined } - const token = new vscode.CancellationTokenSource().token - const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(1, 1), '') - const result = await provider.provideInlineCompletionItems(document, position, fakeContext, token) - - assert.ok(result === undefined) - }) - }) -}) - -describe('codewhisperer status bar', function () { - let sandbox: sinon.SinonSandbox - let statusBar: TestStatusBar - let service: InlineCompletionService - - class TestStatusBar extends CodeWhispererStatusBar { - constructor() { - super() - } - - getStatusBar() { - return this.statusBar - } - } - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - sandbox = sinon.createSandbox() - statusBar = new TestStatusBar() - service = new InlineCompletionService(statusBar) - }) - - afterEach(function () { - sandbox.restore() - }) - - it('shows correct status bar when auth is not connected', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(false) - sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(chrome-close) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual(actualStatusBar.backgroundColor, new vscode.ThemeColor('statusBarItem.errorBackground')) - }) - - it('shows correct status bar when auth is connected', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(true) - sandbox.stub(CodeSuggestionsState.instance, 'isSuggestionsEnabled').returns(true) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(debug-start) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual(actualStatusBar.backgroundColor, undefined) - }) - - it('shows correct status bar when auth is connected but paused', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(true) - sandbox.stub(CodeSuggestionsState.instance, 'isSuggestionsEnabled').returns(false) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(debug-pause) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual(actualStatusBar.backgroundColor, undefined) - }) - - it('shows correct status bar when auth is expired', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(false) - sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(true) - - await service.refreshStatusBar() - - const actualStatusBar = statusBar.getStatusBar() - assert.strictEqual(actualStatusBar.text, '$(debug-disconnect) Amazon Q') - assert.strictEqual(actualStatusBar.command, listCodeWhispererCommandsId) - assert.deepStrictEqual( - actualStatusBar.backgroundColor, - new vscode.ThemeColor('statusBarItem.warningBackground') - ) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts deleted file mode 100644 index 4b6a5291f22..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as codewhispererSdkClient from 'aws-core-vscode/codewhisperer' -import { - createMockTextEditor, - createTextDocumentChangeEvent, - resetCodeWhispererGlobalVariables, -} from 'aws-core-vscode/test' -import * as EditorContext from 'aws-core-vscode/codewhisperer' -import { - ConfigurationEntry, - DocumentChangedSource, - KeyStrokeHandler, - DefaultDocumentChangedType, - RecommendationService, - ClassifierTrigger, - isInlineCompletionEnabled, - RecommendationHandler, - InlineCompletionService, -} from 'aws-core-vscode/codewhisperer' - -describe('keyStrokeHandler', function () { - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - describe('processKeyStroke', async function () { - let invokeSpy: sinon.SinonStub - let startTimerSpy: sinon.SinonStub - let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient - beforeEach(async function () { - invokeSpy = sinon.stub(KeyStrokeHandler.instance, 'invokeAutomatedTrigger') - startTimerSpy = sinon.stub(KeyStrokeHandler.instance, 'startIdleTimeTriggerTimer') - sinon.spy(RecommendationHandler.instance, 'getRecommendations') - mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() - await resetCodeWhispererGlobalVariables() - sinon.stub(mockClient, 'listRecommendations') - sinon.stub(mockClient, 'generateRecommendations') - }) - afterEach(function () { - sinon.restore() - }) - - it('Whatever the input is, should skip when automatic trigger is turned off, should not call invokeAutomatedTrigger', async function () { - const mockEditor = createMockTextEditor() - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - ' ' - ) - const cfg: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: false, - isSuggestionsWithCodeReferencesEnabled: true, - } - const keyStrokeHandler = new KeyStrokeHandler() - await keyStrokeHandler.processKeyStroke(mockEvent, mockEditor, mockClient, cfg) - assert.ok(!invokeSpy.called) - assert.ok(!startTimerSpy.called) - }) - - it('Should not call invokeAutomatedTrigger when changed text across multiple lines', async function () { - await testShouldInvoke('\nprint(n', false) - }) - - it('Should not call invokeAutomatedTrigger when doing delete or undo (empty changed text)', async function () { - await testShouldInvoke('', false) - }) - - it('Should call invokeAutomatedTrigger with Enter when inputing \n', async function () { - await testShouldInvoke('\n', true) - }) - - it('Should call invokeAutomatedTrigger with Enter when inputing \r\n', async function () { - await testShouldInvoke('\r\n', true) - }) - - it('Should call invokeAutomatedTrigger with SpecialCharacter when inputing {', async function () { - await testShouldInvoke('{', true) - }) - - it('Should not call invokeAutomatedTrigger for non-special characters for classifier language if classifier says no', async function () { - sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(false) - await testShouldInvoke('a', false) - }) - - it('Should call invokeAutomatedTrigger for non-special characters for classifier language if classifier says yes', async function () { - sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(true) - await testShouldInvoke('a', true) - }) - - it('Should skip invoking if there is immediate right context on the same line and not a single }', async function () { - const casesForSuppressTokenFilling = [ - { - rightContext: 'add', - shouldInvoke: false, - }, - { - rightContext: '}', - shouldInvoke: true, - }, - { - rightContext: '} ', - shouldInvoke: true, - }, - { - rightContext: ')', - shouldInvoke: true, - }, - { - rightContext: ') ', - shouldInvoke: true, - }, - { - rightContext: ' add', - shouldInvoke: true, - }, - { - rightContext: ' ', - shouldInvoke: true, - }, - { - rightContext: '\naddTwo', - shouldInvoke: true, - }, - ] - - for (const o of casesForSuppressTokenFilling) { - await testShouldInvoke('{', o.shouldInvoke, o.rightContext) - } - }) - - async function testShouldInvoke(input: string, shouldTrigger: boolean, rightContext: string = '') { - const mockEditor = createMockTextEditor(rightContext, 'test.js', 'javascript', 0, 0) - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - input - ) - await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config) - assert.strictEqual( - invokeSpy.called, - shouldTrigger, - `invokeAutomatedTrigger ${shouldTrigger ? 'NOT' : 'WAS'} called for rightContext: "${rightContext}"` - ) - } - }) - - describe('invokeAutomatedTrigger', function () { - let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient - beforeEach(async function () { - sinon.restore() - mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() - await resetCodeWhispererGlobalVariables() - sinon.stub(mockClient, 'listRecommendations') - sinon.stub(mockClient, 'generateRecommendations') - }) - afterEach(function () { - sinon.restore() - }) - - it('should call getPaginatedRecommendation when inline completion is enabled', async function () { - const mockEditor = createMockTextEditor() - const keyStrokeHandler = new KeyStrokeHandler() - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - ' ' - ) - const getRecommendationsStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') - await keyStrokeHandler.invokeAutomatedTrigger('Enter', mockEditor, mockClient, config, mockEvent) - assert.strictEqual(getRecommendationsStub.called, isInlineCompletionEnabled()) - }) - }) - - describe('shouldTriggerIdleTime', function () { - it('should return false when inline is enabled and inline completion is in progress ', function () { - const keyStrokeHandler = new KeyStrokeHandler() - sinon.stub(RecommendationService.instance, 'isRunning').get(() => true) - const result = keyStrokeHandler.shouldTriggerIdleTime() - assert.strictEqual(result, !isInlineCompletionEnabled()) - }) - }) - - describe('test checkChangeSource', function () { - const tabStr = ' '.repeat(EditorContext.getTabSize()) - - const cases: [string, DocumentChangedSource][] = [ - ['\n ', DocumentChangedSource.EnterKey], - ['\n', DocumentChangedSource.EnterKey], - ['(', DocumentChangedSource.SpecialCharsKey], - ['()', DocumentChangedSource.SpecialCharsKey], - ['{}', DocumentChangedSource.SpecialCharsKey], - ['(a, b):', DocumentChangedSource.Unknown], - [':', DocumentChangedSource.SpecialCharsKey], - ['a', DocumentChangedSource.RegularKey], - [tabStr, DocumentChangedSource.TabKey], - [' ', DocumentChangedSource.Reformatting], - ['def add(a,b):\n return a + b\n', DocumentChangedSource.Unknown], - ['function suggestedByIntelliSense():', DocumentChangedSource.Unknown], - ] - - for (const tuple of cases) { - const input = tuple[0] - const expected = tuple[1] - it(`test input ${input} should return ${expected}`, function () { - const actual = new DefaultDocumentChangedType( - createFakeDocumentChangeEvent(tuple[0]) - ).checkChangeSource() - assert.strictEqual(actual, expected) - }) - } - - function createFakeDocumentChangeEvent(str: string): ReadonlyArray { - return [ - { - range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 5)), - rangeOffset: 0, - rangeLength: 0, - text: str, - }, - ] - } - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts deleted file mode 100644 index 86dfc5e514c..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { - ReferenceInlineProvider, - session, - AuthUtil, - DefaultCodeWhispererClient, - RecommendationsList, - ConfigurationEntry, - RecommendationHandler, - CodeWhispererCodeCoverageTracker, - supplementalContextUtil, -} from 'aws-core-vscode/codewhisperer' -import { - assertTelemetryCurried, - stub, - createMockTextEditor, - resetCodeWhispererGlobalVariables, -} from 'aws-core-vscode/test' -// import * as supplementalContextUtil from 'aws-core-vscode/codewhisperer' - -describe('recommendationHandler', function () { - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - }) - - describe('getRecommendations', async function () { - const mockClient = stub(DefaultCodeWhispererClient) - const mockEditor = createMockTextEditor() - const testStartUrl = 'testStartUrl' - - beforeEach(async function () { - sinon.restore() - await resetCodeWhispererGlobalVariables() - mockClient.listRecommendations.resolves({}) - mockClient.generateRecommendations.resolves({}) - RecommendationHandler.instance.clearRecommendations() - sinon.stub(AuthUtil.instance, 'startUrl').value(testStartUrl) - }) - - afterEach(function () { - sinon.restore() - }) - - it('should assign correct recommendations given input', async function () { - assert.strictEqual(CodeWhispererCodeCoverageTracker.instances.size, 0) - assert.strictEqual( - CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, - 0 - ) - - const mockServerResult = { - recommendations: [{ content: "print('Hello World!')" }, { content: '' }], - $response: { - requestId: 'test_request', - httpResponse: { - headers: { - 'x-amzn-sessionid': 'test_request', - }, - }, - }, - } - const handler = new RecommendationHandler() - sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) - const actual = session.recommendations - const expected: RecommendationsList = [{ content: "print('Hello World!')" }, { content: '' }] - assert.deepStrictEqual(actual, expected) - assert.strictEqual( - CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, - 1 - ) - }) - - it('should assign request id correctly', async function () { - const mockServerResult = { - recommendations: [{ content: "print('Hello World!')" }, { content: '' }], - $response: { - requestId: 'test_request', - httpResponse: { - headers: { - 'x-amzn-sessionid': 'test_request', - }, - }, - }, - } - const handler = new RecommendationHandler() - sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) - sinon.stub(handler, 'isCancellationRequested').returns(false) - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) - assert.strictEqual(handler.requestId, 'test_request') - assert.strictEqual(session.sessionId, 'test_request') - assert.strictEqual(session.triggerType, 'AutoTrigger') - }) - - it('should call telemetry function that records a CodeWhisperer service invocation', async function () { - const mockServerResult = { - recommendations: [{ content: "print('Hello World!')" }, { content: '' }], - $response: { - requestId: 'test_request', - httpResponse: { - headers: { - 'x-amzn-sessionid': 'test_request', - }, - }, - }, - } - const handler = new RecommendationHandler() - sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) - sinon.stub(supplementalContextUtil, 'fetchSupplementalContext').resolves({ - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: [], - contentsLength: 100, - latency: 0, - strategy: 'empty', - }) - sinon.stub(performance, 'now').returns(0.0) - session.startPos = new vscode.Position(1, 0) - session.startCursorOffset = 2 - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter') - const assertTelemetry = assertTelemetryCurried('codewhisperer_serviceInvocation') - assertTelemetry({ - codewhispererRequestId: 'test_request', - codewhispererSessionId: 'test_request', - codewhispererLastSuggestionIndex: 1, - codewhispererTriggerType: 'AutoTrigger', - codewhispererAutomatedTriggerType: 'Enter', - codewhispererImportRecommendationEnabled: true, - result: 'Succeeded', - codewhispererLineNumber: 1, - codewhispererCursorOffset: 38, - codewhispererLanguage: 'python', - credentialStartUrl: testStartUrl, - codewhispererSupplementalContextIsUtg: false, - codewhispererSupplementalContextTimeout: false, - codewhispererSupplementalContextLatency: 0, - codewhispererSupplementalContextLength: 100, - }) - }) - }) - - describe('isValidResponse', function () { - afterEach(function () { - sinon.restore() - }) - it('should return true if any response is not empty', function () { - const handler = new RecommendationHandler() - session.recommendations = [ - { - content: - '\n // Use the console to output debug info…n of the command with the "command" variable', - }, - { content: '' }, - ] - assert.ok(handler.isValidResponse()) - }) - - it('should return false if response is empty', function () { - const handler = new RecommendationHandler() - session.recommendations = [] - assert.ok(!handler.isValidResponse()) - }) - - it('should return false if all response has no string length', function () { - const handler = new RecommendationHandler() - session.recommendations = [{ content: '' }, { content: '' }] - assert.ok(!handler.isValidResponse()) - }) - }) - - describe('setCompletionType/getCompletionType', function () { - beforeEach(function () { - sinon.restore() - }) - - it('should set the completion type to block given a multi-line suggestion', function () { - session.setCompletionType(0, { content: 'test\n\n \t\r\nanother test' }) - assert.strictEqual(session.getCompletionType(0), 'Block') - - session.setCompletionType(0, { content: 'test\ntest\n' }) - assert.strictEqual(session.getCompletionType(0), 'Block') - - session.setCompletionType(0, { content: '\n \t\r\ntest\ntest' }) - assert.strictEqual(session.getCompletionType(0), 'Block') - }) - - it('should set the completion type to line given a single-line suggestion', function () { - session.setCompletionType(0, { content: 'test' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: 'test\r\t ' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - }) - - it('should set the completion type to line given a multi-line completion but only one-lien of non-blank sequence', function () { - session.setCompletionType(0, { content: 'test\n\t' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: 'test\n ' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: 'test\n\r' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - - session.setCompletionType(0, { content: '\n\n\n\ntest' }) - assert.strictEqual(session.getCompletionType(0), 'Line') - }) - }) - - describe('on event change', async function () { - beforeEach(function () { - const fakeReferences = [ - { - message: '', - licenseName: 'MIT', - repository: 'http://github.com/fake', - recommendationContentSpan: { - start: 0, - end: 10, - }, - }, - ] - ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) - session.sessionId = '' - RecommendationHandler.instance.requestId = '' - }) - - it('should remove inline reference onEditorChange', async function () { - session.sessionId = 'aSessionId' - RecommendationHandler.instance.requestId = 'aRequestId' - await RecommendationHandler.instance.onEditorChange() - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - }) - it('should remove inline reference onFocusChange', async function () { - session.sessionId = 'aSessionId' - RecommendationHandler.instance.requestId = 'aRequestId' - await RecommendationHandler.instance.onFocusChange() - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - }) - it('should not remove inline reference on cursor change from typing', async function () { - await RecommendationHandler.instance.onCursorChange({ - textEditor: createMockTextEditor(), - selections: [], - kind: vscode.TextEditorSelectionChangeKind.Keyboard, - }) - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 1) - }) - - it('should remove inline reference on cursor change from mouse movement', async function () { - await RecommendationHandler.instance.onCursorChange({ - textEditor: vscode.window.activeTextEditor!, - selections: [], - kind: vscode.TextEditorSelectionChangeKind.Mouse, - }) - assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts b/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts index 0f1429f130b..1f11661f002 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts @@ -19,9 +19,7 @@ import { DefaultCodeWhispererClient, ListRecommendationsResponse, Recommendation, - invokeRecommendation, ConfigurationEntry, - RecommendationHandler, session, vsCodeCursorUpdateDelay, AuthUtil, @@ -113,7 +111,6 @@ describe.skip('CodeWhisperer telemetry', async function () { }) async function resetStates() { - await RecommendationHandler.instance.clearInlineCompletionStates() await resetCodeWhispererGlobalVariables() } @@ -424,7 +421,6 @@ describe.skip('CodeWhisperer telemetry', async function () { assert.strictEqual(session.sessionId, 'session_id_1') assert.deepStrictEqual(session.requestIdList, ['request_id_1', 'request_id_1', 'request_id_1_2']) - await RecommendationHandler.instance.onEditorChange() assertSessionClean() await backspace(editor) // todo: without this, the following manual trigger will not be displayed in the test, investigate and fix it @@ -500,7 +496,6 @@ describe.skip('CodeWhisperer telemetry', async function () { await manualTrigger(editor, client, config) await assertTextEditorContains('') - await RecommendationHandler.instance.onFocusChange() assertTelemetry('codewhisperer_userTriggerDecision', [ session1UserTriggerEvent({ codewhispererSuggestionState: 'Reject' }), ]) @@ -513,7 +508,6 @@ async function manualTrigger( client: DefaultCodeWhispererClient, config: ConfigurationEntry ) { - await invokeRecommendation(editor, client, config) await waitUntilSuggestionSeen() } diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index e52e08bb98b..ec49efcedaa 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -16,7 +16,6 @@ import { CodeScanIssue, CodeIssueGroupingStrategyState, } from './models/model' -import { acceptSuggestion } from './commands/onInlineAcceptance' import { CodeWhispererSettings } from './util/codewhispererSettings' import { ExtContext } from '../shared/extensions' import { CodeWhispererTracker } from './tracker/codewhispererTracker' @@ -64,20 +63,16 @@ import { updateSecurityDiagnosticCollection, } from './service/diagnosticsProvider' import { SecurityPanelViewProvider, openEditorAtRange } from './views/securityPanelViewProvider' -import { RecommendationHandler } from './service/recommendationHandler' import { Commands, registerCommandErrorHandler, registerDeclaredCommands } from '../shared/vscode/commands2' -import { InlineCompletionService, refreshStatusBar } from './service/inlineCompletionService' -import { isInlineCompletionEnabled } from './util/commonUtil' +import { refreshStatusBar } from './service/inlineCompletionService' import { AuthUtil } from './util/authUtil' import { ImportAdderProvider } from './service/importAdderProvider' -import { TelemetryHelper } from './util/telemetryHelper' import { openUrl } from '../shared/utilities/vsCodeUtils' import { notifyNewCustomizations, onProfileChangedListener } from './util/customizationUtil' import { CodeWhispererCommandBackend, CodeWhispererCommandDeclarations } from './commands/gettingStartedPageCommands' import { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' import { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' import { listCodeWhispererCommands } from './ui/statusBarMenu' -import { Container } from './service/serviceContainer' import { debounceStartSecurityScan } from './commands/startSecurityScan' import { securityScanLanguageContext } from './util/securityScanLanguageContext' import { registerWebviewErrorHandler } from '../webviews/server' @@ -137,7 +132,6 @@ export async function activate(context: ExtContext): Promise { const client = new codewhispererClient.DefaultCodeWhispererClient() // Service initialization - const container = Container.instance ReferenceInlineProvider.instance ImportAdderProvider.instance @@ -215,20 +209,21 @@ export async function activate(context: ExtContext): Promise { await openSettings('amazonQ') } }), - Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - const editor = vscode.window.activeTextEditor - if (editor) { - if (forceProceed) { - await container.lineAnnotationController.refresh(editor, 'codewhisperer', true) - } else { - await container.lineAnnotationController.refresh(editor, 'codewhisperer') - } - } - }), + // TODO port this to lsp + // Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + // telemetry.record({ + // traceId: TelemetryHelper.instance.traceId, + // }) + + // const editor = vscode.window.activeTextEditor + // if (editor) { + // if (forceProceed) { + // await container.lineAnnotationController.refresh(editor, 'codewhisperer', true) + // } else { + // await container.lineAnnotationController.refresh(editor, 'codewhisperer') + // } + // } + // }), // show introduction showIntroduction.register(), // toggle code suggestions @@ -300,22 +295,10 @@ export async function activate(context: ExtContext): Promise { // notify new customizations notifyNewCustomizationsCmd.register(), selectRegionProfileCommand.register(), - /** - * On recommendation acceptance - */ - acceptSuggestion.register(context), // direct CodeWhisperer connection setup with customization connectWithCustomization.register(), - // on text document close. - vscode.workspace.onDidCloseTextDocument((e) => { - if (isInlineCompletionEnabled() && e.uri.fsPath !== InlineCompletionService.instance.filePath()) { - return - } - RecommendationHandler.instance.reportUserDecisions(-1) - }), - vscode.languages.registerHoverProvider( [...CodeWhispererConstants.platformLanguageIds], ReferenceHoverProvider.instance @@ -473,7 +456,6 @@ export async function activate(context: ExtContext): Promise { }) await Commands.tryExecute('aws.amazonq.refreshConnectionCallback') - container.ready() function setSubscriptionsForCodeIssues() { context.extensionContext.subscriptions.push( @@ -511,7 +493,6 @@ export async function activate(context: ExtContext): Promise { } export async function shutdown() { - RecommendationHandler.instance.reportUserDecisions(-1) await CodeWhispererTracker.getTracker().shutdown() } diff --git a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts deleted file mode 100644 index 37fcb965774..00000000000 --- a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { vsCodeState, ConfigurationEntry } from '../models/model' -import { resetIntelliSenseState } from '../util/globalStateUtil' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { RecommendationHandler } from '../service/recommendationHandler' -import { session } from '../util/codeWhispererSession' -import { RecommendationService } from '../service/recommendationService' - -/** - * This function is for manual trigger CodeWhisperer - */ - -export async function invokeRecommendation( - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry -) { - if (!editor || !config.isManualTriggerEnabled) { - return - } - - /** - * Skip when output channel gains focus and invoke - */ - if (editor.document.languageId === 'Log') { - return - } - /** - * When using intelliSense, if invocation position changed, reject previous active recommendations - */ - if (vsCodeState.isIntelliSenseActive && editor.selection.active !== session.startPos) { - resetIntelliSenseState( - config.isManualTriggerEnabled, - config.isAutomatedTriggerEnabled, - RecommendationHandler.instance.isValidResponse() - ) - } - - await RecommendationService.instance.generateRecommendation(client, editor, 'OnDemand', config, undefined) -} diff --git a/packages/core/src/codewhisperer/commands/onAcceptance.ts b/packages/core/src/codewhisperer/commands/onAcceptance.ts deleted file mode 100644 index e13c197cefd..00000000000 --- a/packages/core/src/codewhisperer/commands/onAcceptance.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { CodeWhispererTracker } from '../tracker/codewhispererTracker' -import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' -import { getLogger } from '../../shared/logger/logger' -import { handleExtraBrackets } from '../util/closingBracketUtil' -import { RecommendationHandler } from '../service/recommendationHandler' -import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' -import { ReferenceHoverProvider } from '../service/referenceHoverProvider' -import path from 'path' - -/** - * This function is called when user accepts a intelliSense suggestion or an inline suggestion - */ -export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { - RecommendationHandler.instance.cancelPaginatedRequest() - /** - * Format document - */ - if (acceptanceEntry.editor) { - const languageContext = runtimeLanguageContext.getLanguageContext( - acceptanceEntry.editor.document.languageId, - path.extname(acceptanceEntry.editor.document.fileName) - ) - const start = acceptanceEntry.range.start - const end = acceptanceEntry.range.end - - // codewhisperer will be doing editing while formatting. - // formatting should not trigger consoals auto trigger - vsCodeState.isCodeWhispererEditing = true - /** - * Mitigation to right context handling mainly for auto closing bracket use case - */ - try { - await handleExtraBrackets(acceptanceEntry.editor, end, start) - } catch (error) { - getLogger().error(`${error} in handleAutoClosingBrackets`) - } - // move cursor to end of suggestion before doing code format - // after formatting, the end position will still be editor.selection.active - acceptanceEntry.editor.selection = new vscode.Selection(end, end) - - vsCodeState.isCodeWhispererEditing = false - CodeWhispererTracker.getTracker().enqueue({ - time: new Date(), - fileUrl: acceptanceEntry.editor.document.uri, - originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), - startPosition: start, - endPosition: end, - requestId: acceptanceEntry.requestId, - sessionId: acceptanceEntry.sessionId, - index: acceptanceEntry.acceptIndex, - triggerType: acceptanceEntry.triggerType, - completionType: acceptanceEntry.completionType, - language: languageContext.language, - }) - const insertedCoderange = new vscode.Range(start, end) - CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( - insertedCoderange, - acceptanceEntry.editor.document.getText(insertedCoderange), - acceptanceEntry.editor.document.fileName - ) - if (acceptanceEntry.references !== undefined) { - const referenceLog = ReferenceLogViewProvider.getReferenceLog( - acceptanceEntry.recommendation, - acceptanceEntry.references, - acceptanceEntry.editor - ) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - ReferenceHoverProvider.instance.addCodeReferences( - acceptanceEntry.recommendation, - acceptanceEntry.references - ) - } - } - - // at the end of recommendation acceptance, report user decisions and clear recommendations. - RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) -} diff --git a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts deleted file mode 100644 index 50af478ba57..00000000000 --- a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts +++ /dev/null @@ -1,146 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as CodeWhispererConstants from '../models/constants' -import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { CodeWhispererTracker } from '../tracker/codewhispererTracker' -import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' -import { getLogger } from '../../shared/logger/logger' -import { RecommendationHandler } from '../service/recommendationHandler' -import { sleep } from '../../shared/utilities/timeoutUtils' -import { handleExtraBrackets } from '../util/closingBracketUtil' -import { Commands } from '../../shared/vscode/commands2' -import { isInlineCompletionEnabled } from '../util/commonUtil' -import { ExtContext } from '../../shared/extensions' -import { onAcceptance } from './onAcceptance' -import * as codewhispererClient from '../client/codewhisperer' -import { - CodewhispererCompletionType, - CodewhispererLanguage, - CodewhispererTriggerType, -} from '../../shared/telemetry/telemetry.gen' -import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' -import { ReferenceHoverProvider } from '../service/referenceHoverProvider' -import { ImportAdderProvider } from '../service/importAdderProvider' -import { session } from '../util/codeWhispererSession' -import path from 'path' -import { RecommendationService } from '../service/recommendationService' -import { Container } from '../service/serviceContainer' -import { telemetry } from '../../shared/telemetry/telemetry' -import { TelemetryHelper } from '../util/telemetryHelper' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' - -export const acceptSuggestion = Commands.declare( - 'aws.amazonq.accept', - (context: ExtContext) => - async ( - range: vscode.Range, - effectiveRange: vscode.Range, - acceptIndex: number, - recommendation: string, - requestId: string, - sessionId: string, - triggerType: CodewhispererTriggerType, - completionType: CodewhispererCompletionType, - language: CodewhispererLanguage, - references: codewhispererClient.References - ) => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - RecommendationService.instance.incrementAcceptedCount() - const editor = vscode.window.activeTextEditor - await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') - const onAcceptanceFunc = isInlineCompletionEnabled() ? onInlineAcceptance : onAcceptance - await onAcceptanceFunc({ - editor, - range, - effectiveRange, - acceptIndex, - recommendation, - requestId, - sessionId, - triggerType, - completionType, - language, - references, - }) - } -) -/** - * This function is called when user accepts a intelliSense suggestion or an inline suggestion - */ -export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { - RecommendationHandler.instance.cancelPaginatedRequest() - RecommendationHandler.instance.disposeInlineCompletion() - - if (acceptanceEntry.editor) { - await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - const languageContext = runtimeLanguageContext.getLanguageContext( - acceptanceEntry.editor.document.languageId, - path.extname(acceptanceEntry.editor.document.fileName) - ) - const start = acceptanceEntry.range.start - const end = acceptanceEntry.editor.selection.active - - vsCodeState.isCodeWhispererEditing = true - /** - * Mitigation to right context handling mainly for auto closing bracket use case - */ - try { - // Do not handle extra bracket if there is a right context merge - if (acceptanceEntry.recommendation === session.recommendations[acceptanceEntry.acceptIndex].content) { - await handleExtraBrackets(acceptanceEntry.editor, end, acceptanceEntry.effectiveRange.start) - } - await ImportAdderProvider.instance.onAcceptRecommendation( - acceptanceEntry.editor, - session.recommendations[acceptanceEntry.acceptIndex], - start.line - ) - } catch (error) { - getLogger().error(`${error} in handling extra brackets or imports`) - } finally { - vsCodeState.isCodeWhispererEditing = false - } - - CodeWhispererTracker.getTracker().enqueue({ - time: new Date(), - fileUrl: acceptanceEntry.editor.document.uri, - originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), - startPosition: start, - endPosition: end, - requestId: acceptanceEntry.requestId, - sessionId: acceptanceEntry.sessionId, - index: acceptanceEntry.acceptIndex, - triggerType: acceptanceEntry.triggerType, - completionType: acceptanceEntry.completionType, - language: languageContext.language, - }) - const insertedCoderange = new vscode.Range(start, end) - CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( - insertedCoderange, - acceptanceEntry.editor.document.getText(insertedCoderange), - acceptanceEntry.editor.document.fileName - ) - UserWrittenCodeTracker.instance.onQFinishesEdits() - if (acceptanceEntry.references !== undefined) { - const referenceLog = ReferenceLogViewProvider.getReferenceLog( - acceptanceEntry.recommendation, - acceptanceEntry.references, - acceptanceEntry.editor - ) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - ReferenceHoverProvider.instance.addCodeReferences( - acceptanceEntry.recommendation, - acceptanceEntry.references - ) - } - - RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) - } -} diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 4235ae28668..12c6783526f 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -9,13 +9,6 @@ export * from './models/model' export * from './models/constants' export * from './commands/basicCommands' export * from './commands/types' -export { - AutotriggerState, - EndState, - ManualtriggerState, - PressTabState, - TryMoreExState, -} from './views/lineAnnotationController' export type { TransformationProgressUpdate, TransformationStep, @@ -53,22 +46,15 @@ export { IssueItem, SeverityItem, } from './service/securityIssueTreeViewProvider' -export { invokeRecommendation } from './commands/invokeRecommendation' -export { onAcceptance } from './commands/onAcceptance' export { CodeWhispererTracker } from './tracker/codewhispererTracker' -export { RecommendationHandler } from './service/recommendationHandler' export { CodeWhispererUserGroupSettings } from './util/userGroupUtil' export { session } from './util/codeWhispererSession' -export { onInlineAcceptance } from './commands/onInlineAcceptance' export { stopTransformByQ } from './commands/startTransformByQ' -export { getCompletionItems, getCompletionItem, getLabel } from './service/completionProvider' export { featureDefinitions, FeatureConfigProvider } from '../shared/featureConfig' export { ReferenceInlineProvider } from './service/referenceInlineProvider' export { ReferenceHoverProvider } from './service/referenceHoverProvider' export { CWInlineCompletionItemProvider } from './service/inlineCompletionItemProvider' -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' diff --git a/packages/core/src/codewhisperer/service/completionProvider.ts b/packages/core/src/codewhisperer/service/completionProvider.ts deleted file mode 100644 index 226d04dec2b..00000000000 --- a/packages/core/src/codewhisperer/service/completionProvider.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as CodeWhispererConstants from '../models/constants' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { Recommendation } from '../client/codewhisperer' -import { LicenseUtil } from '../util/licenseUtil' -import { RecommendationHandler } from './recommendationHandler' -import { session } from '../util/codeWhispererSession' -import path from 'path' -/** - * completion provider for intelliSense popup - */ -export function getCompletionItems(document: vscode.TextDocument, position: vscode.Position) { - const completionItems: vscode.CompletionItem[] = [] - for (const [index, recommendation] of session.recommendations.entries()) { - completionItems.push(getCompletionItem(document, position, recommendation, index)) - session.setSuggestionState(index, 'Showed') - } - return completionItems -} - -export function getCompletionItem( - document: vscode.TextDocument, - position: vscode.Position, - recommendationDetail: Recommendation, - recommendationIndex: number -) { - const start = session.startPos - const range = new vscode.Range(start, start) - const recommendation = recommendationDetail.content - const completionItem = new vscode.CompletionItem(recommendation) - completionItem.insertText = new vscode.SnippetString(recommendation) - completionItem.documentation = new vscode.MarkdownString().appendCodeblock(recommendation, document.languageId) - completionItem.kind = vscode.CompletionItemKind.Method - completionItem.detail = CodeWhispererConstants.completionDetail - completionItem.keepWhitespace = true - completionItem.label = getLabel(recommendation) - completionItem.preselect = true - completionItem.sortText = String(recommendationIndex + 1).padStart(10, '0') - completionItem.range = new vscode.Range(start, position) - const languageContext = runtimeLanguageContext.getLanguageContext( - document.languageId, - path.extname(document.fileName) - ) - let references: typeof recommendationDetail.references - if (recommendationDetail.references !== undefined && recommendationDetail.references.length > 0) { - references = recommendationDetail.references - const licenses = [ - ...new Set(references.map((r) => `[${r.licenseName}](${LicenseUtil.getLicenseHtml(r.licenseName)})`)), - ].join(', ') - completionItem.documentation.appendMarkdown(CodeWhispererConstants.suggestionDetailReferenceText(licenses)) - } - completionItem.command = { - command: 'aws.amazonq.accept', - title: 'On acceptance', - arguments: [ - range, - recommendationIndex, - recommendation, - RecommendationHandler.instance.requestId, - session.sessionId, - session.triggerType, - session.getCompletionType(recommendationIndex), - languageContext.language, - references, - ], - } - return completionItem -} - -export function getLabel(recommendation: string): string { - return recommendation.slice(0, CodeWhispererConstants.labelLength) + '..' -} diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts index cc9887adb1f..18c8f0595aa 100644 --- a/packages/core/src/codewhisperer/service/inlineCompletionService.ts +++ b/packages/core/src/codewhisperer/service/inlineCompletionService.ts @@ -3,36 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { CodeSuggestionsState, ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' -import * as CodeWhispererConstants from '../models/constants' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { RecommendationHandler } from './recommendationHandler' -import { CodewhispererAutomatedTriggerType, CodewhispererTriggerType } from '../../shared/telemetry/telemetry' -import { showTimedMessage } from '../../shared/utilities/messages' -import { getLogger } from '../../shared/logger/logger' -import { TelemetryHelper } from '../util/telemetryHelper' +import { CodeSuggestionsState } from '../models/model' import { AuthUtil } from '../util/authUtil' -import { shared } from '../../shared/utilities/functionUtils' -import { ClassifierTrigger } from './classifierTrigger' import { getSelectedCustomization } from '../util/customizationUtil' import { codicon, getIcon } from '../../shared/icons' -import { session } from '../util/codeWhispererSession' -import { noSuggestions } from '../models/constants' import { Commands } from '../../shared/vscode/commands2' import { listCodeWhispererCommandsId } from '../ui/statusBarMenu' export class InlineCompletionService { - private maxPage = 100 private statusBar: CodeWhispererStatusBar - private _showRecommendationTimer?: NodeJS.Timer constructor(statusBar: CodeWhispererStatusBar = CodeWhispererStatusBar.instance) { this.statusBar = statusBar - RecommendationHandler.instance.onDidReceiveRecommendation((e) => { - this.startShowRecommendationTimer() - }) - CodeSuggestionsState.instance.onDidChangeState(() => { return this.refreshStatusBar() }) @@ -44,126 +27,6 @@ export class InlineCompletionService { return (this.#instance ??= new this()) } - filePath(): string | undefined { - return RecommendationHandler.instance.documentUri?.fsPath - } - - private sharedTryShowRecommendation = shared( - RecommendationHandler.instance.tryShowRecommendation.bind(RecommendationHandler.instance) - ) - - private startShowRecommendationTimer() { - if (this._showRecommendationTimer) { - clearInterval(this._showRecommendationTimer) - this._showRecommendationTimer = undefined - } - this._showRecommendationTimer = setInterval(() => { - const delay = performance.now() - vsCodeState.lastUserModificationTime - if (delay < CodeWhispererConstants.inlineSuggestionShowDelay) { - return - } - this.sharedTryShowRecommendation() - .catch((e) => { - getLogger().error('tryShowRecommendation failed: %s', (e as Error).message) - }) - .finally(() => { - if (this._showRecommendationTimer) { - clearInterval(this._showRecommendationTimer) - this._showRecommendationTimer = undefined - } - }) - }, CodeWhispererConstants.showRecommendationTimerPollPeriod) - } - - async getPaginatedRecommendation( - client: DefaultCodeWhispererClient, - editor: vscode.TextEditor, - triggerType: CodewhispererTriggerType, - config: ConfigurationEntry, - autoTriggerType?: CodewhispererAutomatedTriggerType, - event?: vscode.TextDocumentChangeEvent - ): Promise { - if (vsCodeState.isCodeWhispererEditing || RecommendationHandler.instance.isSuggestionVisible()) { - return { - result: 'Failed', - errorMessage: 'Amazon Q is already running', - recommendationCount: 0, - } - } - - // Call report user decisions once to report recommendations leftover from last invocation. - RecommendationHandler.instance.reportUserDecisions(-1) - TelemetryHelper.instance.setInvokeSuggestionStartTime() - ClassifierTrigger.instance.recordClassifierResultForAutoTrigger(editor, autoTriggerType, event) - - const triggerChar = event?.contentChanges[0]?.text - if (autoTriggerType === 'SpecialCharacters' && triggerChar) { - TelemetryHelper.instance.setTriggerCharForUserTriggerDecision(triggerChar) - } - const isAutoTrigger = triggerType === 'AutoTrigger' - if (AuthUtil.instance.isConnectionExpired()) { - await AuthUtil.instance.notifyReauthenticate(isAutoTrigger) - return { - result: 'Failed', - errorMessage: 'auth', - recommendationCount: 0, - } - } - - await this.setState('loading') - - RecommendationHandler.instance.checkAndResetCancellationTokens() - RecommendationHandler.instance.documentUri = editor.document.uri - let response: GetRecommendationsResponse = { - result: 'Failed', - errorMessage: undefined, - recommendationCount: 0, - } - try { - let page = 0 - while (page < this.maxPage) { - response = await RecommendationHandler.instance.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - true, - page - ) - if (RecommendationHandler.instance.checkAndResetCancellationTokens()) { - RecommendationHandler.instance.reportUserDecisions(-1) - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - if (triggerType === 'OnDemand' && session.recommendations.length === 0) { - void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) - } - return { - result: 'Failed', - errorMessage: 'cancelled', - recommendationCount: 0, - } - } - if (!RecommendationHandler.instance.hasNextToken()) { - break - } - page++ - } - } catch (error) { - getLogger().error(`Error ${error} in getPaginatedRecommendation`) - } - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - if (triggerType === 'OnDemand' && session.recommendations.length === 0) { - void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) - } - TelemetryHelper.instance.tryRecordClientComponentLatency() - - return { - result: 'Succeeded', - errorMessage: undefined, - recommendationCount: session.recommendations.length, - } - } - /** Updates the status bar to represent the latest CW state */ refreshStatusBar() { if (AuthUtil.instance.isConnectionValid()) { diff --git a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts deleted file mode 100644 index 49ef633a98f..00000000000 --- a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts +++ /dev/null @@ -1,267 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import * as CodeWhispererConstants from '../models/constants' -import { ConfigurationEntry } from '../models/model' -import { getLogger } from '../../shared/logger/logger' -import { RecommendationHandler } from './recommendationHandler' -import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' -import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' -import { isInlineCompletionEnabled } from '../util/commonUtil' -import { ClassifierTrigger } from './classifierTrigger' -import { extractContextForCodeWhisperer } from '../util/editorContext' -import { RecommendationService } from './recommendationService' - -/** - * This class is for CodeWhisperer auto trigger - */ -export class KeyStrokeHandler { - /** - * Special character which automated triggers codewhisperer - */ - public specialChar: string - /** - * Key stroke count for automated trigger - */ - - private idleTriggerTimer?: NodeJS.Timer - - public lastInvocationTime?: number - - constructor() { - this.specialChar = '' - } - - static #instance: KeyStrokeHandler - - public static get instance() { - return (this.#instance ??= new this()) - } - - public startIdleTimeTriggerTimer( - event: vscode.TextDocumentChangeEvent, - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry - ) { - if (this.idleTriggerTimer) { - clearInterval(this.idleTriggerTimer) - this.idleTriggerTimer = undefined - } - if (!this.shouldTriggerIdleTime()) { - return - } - this.idleTriggerTimer = setInterval(() => { - const duration = (performance.now() - RecommendationHandler.instance.lastInvocationTime) / 1000 - if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) { - return - } - - this.invokeAutomatedTrigger('IdleTime', editor, client, config, event) - .catch((e) => { - getLogger().error('invokeAutomatedTrigger failed: %s', (e as Error).message) - }) - .finally(() => { - if (this.idleTriggerTimer) { - clearInterval(this.idleTriggerTimer) - this.idleTriggerTimer = undefined - } - }) - }, CodeWhispererConstants.idleTimerPollPeriod) - } - - public shouldTriggerIdleTime(): boolean { - if (isInlineCompletionEnabled() && RecommendationService.instance.isRunning) { - return false - } - return true - } - - async processKeyStroke( - event: vscode.TextDocumentChangeEvent, - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry - ): Promise { - try { - if (!config.isAutomatedTriggerEnabled) { - return - } - - // Skip when output channel gains focus and invoke - if (editor.document.languageId === 'Log') { - return - } - - const { rightFileContent } = extractContextForCodeWhisperer(editor) - const rightContextLines = rightFileContent.split(/\r?\n/) - const rightContextAtCurrentLine = rightContextLines[0] - // we do not want to trigger when there is immediate right context on the same line - // with "}" being an exception because of IDE auto-complete - if ( - rightContextAtCurrentLine.length && - !rightContextAtCurrentLine.startsWith(' ') && - rightContextAtCurrentLine.trim() !== '}' && - rightContextAtCurrentLine.trim() !== ')' - ) { - return - } - - let triggerType: CodewhispererAutomatedTriggerType | undefined - const changedSource = new DefaultDocumentChangedType(event.contentChanges).checkChangeSource() - - switch (changedSource) { - case DocumentChangedSource.EnterKey: { - triggerType = 'Enter' - break - } - case DocumentChangedSource.SpecialCharsKey: { - triggerType = 'SpecialCharacters' - break - } - case DocumentChangedSource.RegularKey: { - triggerType = ClassifierTrigger.instance.shouldTriggerFromClassifier(event, editor, triggerType) - ? 'Classifier' - : undefined - break - } - default: { - break - } - } - - if (triggerType) { - await this.invokeAutomatedTrigger(triggerType, editor, client, config, event) - } - } catch (error) { - getLogger().verbose(`Automated Trigger Exception : ${error}`) - } - } - - async invokeAutomatedTrigger( - autoTriggerType: CodewhispererAutomatedTriggerType, - editor: vscode.TextEditor, - client: DefaultCodeWhispererClient, - config: ConfigurationEntry, - event: vscode.TextDocumentChangeEvent - ): Promise { - if (!editor) { - return - } - - // RecommendationHandler.instance.reportUserDecisionOfRecommendation(editor, -1) - await RecommendationService.instance.generateRecommendation( - client, - editor, - 'AutoTrigger', - config, - autoTriggerType - ) - } -} - -export abstract class DocumentChangedType { - constructor(protected readonly contentChanges: ReadonlyArray) { - this.contentChanges = contentChanges - } - - abstract checkChangeSource(): DocumentChangedSource - - // Enter key should always start with ONE '\n' or '\r\n' and potentially following spaces due to IDE reformat - protected isEnterKey(str: string): boolean { - if (str.length === 0) { - return false - } - return ( - (str.startsWith('\r\n') && str.substring(2).trim() === '') || - (str[0] === '\n' && str.substring(1).trim() === '') - ) - } - - // Tab should consist of space char only ' ' and the length % tabSize should be 0 - protected isTabKey(str: string): boolean { - const tabSize = getTabSizeSetting() - if (str.length % tabSize === 0 && str.trim() === '') { - return true - } - return false - } - - protected isUserTypingSpecialChar(str: string): boolean { - return ['(', '()', '[', '[]', '{', '{}', ':'].includes(str) - } - - protected isSingleLine(str: string): boolean { - let newLineCounts = 0 - for (const ch of str) { - if (ch === '\n') { - newLineCounts += 1 - } - } - - // since pressing Enter key possibly will generate string like '\n ' due to indention - if (this.isEnterKey(str)) { - return true - } - if (newLineCounts >= 1) { - return false - } - return true - } -} - -export class DefaultDocumentChangedType extends DocumentChangedType { - constructor(contentChanges: ReadonlyArray) { - super(contentChanges) - } - - checkChangeSource(): DocumentChangedSource { - if (this.contentChanges.length === 0) { - return DocumentChangedSource.Unknown - } - - // event.contentChanges.length will be 2 when user press Enter key multiple times - if (this.contentChanges.length > 2) { - return DocumentChangedSource.Reformatting - } - - // Case when event.contentChanges.length === 1 - const changedText = this.contentChanges[0].text - - if (this.isSingleLine(changedText)) { - if (changedText === '') { - return DocumentChangedSource.Deletion - } else if (this.isEnterKey(changedText)) { - return DocumentChangedSource.EnterKey - } else if (this.isTabKey(changedText)) { - return DocumentChangedSource.TabKey - } else if (this.isUserTypingSpecialChar(changedText)) { - return DocumentChangedSource.SpecialCharsKey - } else if (changedText.length === 1) { - return DocumentChangedSource.RegularKey - } else if (new RegExp('^[ ]+$').test(changedText)) { - // single line && single place reformat should consist of space chars only - return DocumentChangedSource.Reformatting - } else { - return DocumentChangedSource.Unknown - } - } - - // Won't trigger cwspr on multi-line changes - return DocumentChangedSource.Unknown - } -} - -export enum DocumentChangedSource { - SpecialCharsKey = 'SpecialCharsKey', - RegularKey = 'RegularKey', - TabKey = 'TabKey', - EnterKey = 'EnterKey', - Reformatting = 'Reformatting', - Deletion = 'Deletion', - Unknown = 'Unknown', -} diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts deleted file mode 100644 index 8ab491b32e0..00000000000 --- a/packages/core/src/codewhisperer/service/recommendationHandler.ts +++ /dev/null @@ -1,724 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { extensionVersion } from '../../shared/vscode/env' -import { RecommendationsList, DefaultCodeWhispererClient, CognitoCredentialsError } from '../client/codewhisperer' -import * as EditorContext from '../util/editorContext' -import * as CodeWhispererConstants from '../models/constants' -import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { AWSError } from 'aws-sdk' -import { isAwsError } from '../../shared/errors' -import { TelemetryHelper } from '../util/telemetryHelper' -import { getLogger } from '../../shared/logger/logger' -import { hasVendedIamCredentials } from '../../auth/auth' -import { - asyncCallWithTimeout, - isInlineCompletionEnabled, - isVscHavingRegressionInlineCompletionApi, -} from '../util/commonUtil' -import { showTimedMessage } from '../../shared/utilities/messages' -import { - CodewhispererAutomatedTriggerType, - CodewhispererCompletionType, - CodewhispererGettingStartedTask, - CodewhispererTriggerType, - telemetry, -} from '../../shared/telemetry/telemetry' -import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' -import { invalidCustomizationMessage } from '../models/constants' -import { getSelectedCustomization, switchToBaseCustomizationAndNotify } from '../util/customizationUtil' -import { session } from '../util/codeWhispererSession' -import { Commands } from '../../shared/vscode/commands2' -import globals from '../../shared/extensionGlobals' -import { noSuggestions, updateInlineLockKey } from '../models/constants' -import AsyncLock from 'async-lock' -import { AuthUtil } from '../util/authUtil' -import { CWInlineCompletionItemProvider } from './inlineCompletionItemProvider' -import { application } from '../util/codeWhispererApplication' -import { openUrl } from '../../shared/utilities/vsCodeUtils' -import { indent } from '../../shared/utilities/textUtilities' -import path from 'path' -import { isIamConnection } from '../../auth/connection' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' - -/** - * This class is for getRecommendation/listRecommendation API calls and its states - * It does not contain UI/UX related logic - */ - -/** - * Commands as a level of indirection so that declare doesn't intercept any registrations for the - * language server implementation. - * - * Otherwise you'll get: - * "Unable to launch amazonq language server: Command "aws.amazonq.rejectCodeSuggestion" has already been declared by the Toolkit" - */ -function createCommands() { - // below commands override VS Code inline completion commands - const prevCommand = Commands.declare('editor.action.inlineSuggest.showPrevious', () => async () => { - await RecommendationHandler.instance.showRecommendation(-1) - }) - const nextCommand = Commands.declare('editor.action.inlineSuggest.showNext', () => async () => { - await RecommendationHandler.instance.showRecommendation(1) - }) - - const rejectCommand = Commands.declare('aws.amazonq.rejectCodeSuggestion', () => async () => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - RecommendationHandler.instance.reportUserDecisions(-1) - await Commands.tryExecute('aws.amazonq.refreshAnnotation') - }) - - return { - prevCommand, - nextCommand, - rejectCommand, - } -} - -const lock = new AsyncLock({ maxPending: 1 }) - -export class RecommendationHandler { - public lastInvocationTime: number - // TODO: remove this requestId - public requestId: string - private nextToken: string - private cancellationToken: vscode.CancellationTokenSource - private _onDidReceiveRecommendation: vscode.EventEmitter = new vscode.EventEmitter() - public readonly onDidReceiveRecommendation: vscode.Event = this._onDidReceiveRecommendation.event - private inlineCompletionProvider?: CWInlineCompletionItemProvider - private inlineCompletionProviderDisposable?: vscode.Disposable - private reject: vscode.Disposable - private next: vscode.Disposable - private prev: vscode.Disposable - private _timer?: NodeJS.Timer - documentUri: vscode.Uri | undefined = undefined - - constructor() { - this.requestId = '' - this.nextToken = '' - this.lastInvocationTime = performance.now() - CodeWhispererConstants.invocationTimeIntervalThreshold * 1000 - this.cancellationToken = new vscode.CancellationTokenSource() - this.prev = new vscode.Disposable(() => {}) - this.next = new vscode.Disposable(() => {}) - this.reject = new vscode.Disposable(() => {}) - } - - static #instance: RecommendationHandler - - public static get instance() { - return (this.#instance ??= new this()) - } - - isValidResponse(): boolean { - return session.recommendations.some((r) => r.content.trim() !== '') - } - - async getServerResponse( - triggerType: CodewhispererTriggerType, - isManualTriggerOn: boolean, - promise: Promise - ): Promise { - const timeoutMessage = hasVendedIamCredentials() - ? 'Generate recommendation timeout.' - : 'List recommendation timeout' - if (isManualTriggerOn && triggerType === 'OnDemand' && hasVendedIamCredentials()) { - return vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: CodeWhispererConstants.pendingResponse, - cancellable: false, - }, - async () => { - return await asyncCallWithTimeout( - promise, - timeoutMessage, - CodeWhispererConstants.promiseTimeoutLimit * 1000 - ) - } - ) - } - return await asyncCallWithTimeout(promise, timeoutMessage, CodeWhispererConstants.promiseTimeoutLimit * 1000) - } - - async getTaskTypeFromEditorFileName(filePath: string): Promise { - if (filePath.includes('CodeWhisperer_generate_suggestion')) { - return 'autoTrigger' - } else if (filePath.includes('CodeWhisperer_manual_invoke')) { - return 'manualTrigger' - } else if (filePath.includes('CodeWhisperer_use_comments')) { - return 'commentAsPrompt' - } else if (filePath.includes('CodeWhisperer_navigate_suggestions')) { - return 'navigation' - } else if (filePath.includes('Generate_unit_tests')) { - return 'unitTest' - } else { - return undefined - } - } - - async getRecommendations( - client: DefaultCodeWhispererClient, - editor: vscode.TextEditor, - triggerType: CodewhispererTriggerType, - config: ConfigurationEntry, - autoTriggerType?: CodewhispererAutomatedTriggerType, - pagination: boolean = true, - page: number = 0, - generate: boolean = isIamConnection(AuthUtil.instance.conn) - ): Promise { - let invocationResult: 'Succeeded' | 'Failed' = 'Failed' - let errorMessage: string | undefined = undefined - let errorCode: string | undefined = undefined - - if (!editor) { - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: 0, - }) - } - let recommendations: RecommendationsList = [] - let requestId = '' - let sessionId = '' - let reason = '' - let startTime = 0 - let latency = 0 - let nextToken = '' - let shouldRecordServiceInvocation = true - session.language = runtimeLanguageContext.getLanguageContext( - editor.document.languageId, - path.extname(editor.document.fileName) - ).language - session.taskType = await this.getTaskTypeFromEditorFileName(editor.document.fileName) - - if (pagination && !generate) { - if (page === 0) { - session.requestContext = await EditorContext.buildListRecommendationRequest( - editor as vscode.TextEditor, - this.nextToken, - config.isSuggestionsWithCodeReferencesEnabled - ) - } else { - session.requestContext = { - request: { - ...session.requestContext.request, - // Putting nextToken assignment in the end so it overwrites the existing nextToken - nextToken: this.nextToken, - }, - supplementalMetadata: session.requestContext.supplementalMetadata, - } - } - } else { - session.requestContext = await EditorContext.buildGenerateRecommendationRequest(editor as vscode.TextEditor) - } - const request = session.requestContext.request - // record preprocessing end time - TelemetryHelper.instance.setPreprocessEndTime() - - // set start pos for non pagination call or first pagination call - if (!pagination || (pagination && page === 0)) { - session.startPos = editor.selection.active - session.startCursorOffset = editor.document.offsetAt(session.startPos) - session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line) - session.triggerType = triggerType - session.autoTriggerType = autoTriggerType - - /** - * Validate request - */ - if (!EditorContext.validateRequest(request)) { - getLogger().verbose('Invalid Request: %O', request) - const languageName = request.fileContext.programmingLanguage.languageName - if (!runtimeLanguageContext.isLanguageSupported(languageName)) { - errorMessage = `${languageName} is currently not supported by Amazon Q inline suggestions` - } - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: 0, - }) - } - } - - try { - startTime = performance.now() - this.lastInvocationTime = startTime - const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request) - const codewhispererPromise = - pagination && !generate - ? client.listRecommendations(mappedReq) - : client.generateRecommendations(mappedReq) - const resp = await this.getServerResponse(triggerType, config.isManualTriggerEnabled, codewhispererPromise) - TelemetryHelper.instance.setSdkApiCallEndTime() - latency = startTime !== 0 ? performance.now() - startTime : 0 - if ('recommendations' in resp) { - recommendations = (resp && resp.recommendations) || [] - } else { - recommendations = (resp && resp.completions) || [] - } - invocationResult = 'Succeeded' - requestId = resp?.$response && resp?.$response?.requestId - nextToken = resp?.nextToken ? resp?.nextToken : '' - sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] - TelemetryHelper.instance.setFirstResponseRequestId(requestId) - if (page === 0) { - session.setTimeToFirstRecommendation(performance.now()) - } - if (nextToken === '') { - TelemetryHelper.instance.setAllPaginationEndTime() - } - } catch (error) { - if (error instanceof CognitoCredentialsError) { - shouldRecordServiceInvocation = false - } - if (latency === 0) { - latency = startTime !== 0 ? performance.now() - startTime : 0 - } - getLogger().error('amazonq inline-suggest: Invocation Exception : %s', (error as Error).message) - if (isAwsError(error)) { - errorMessage = error.message - requestId = error.requestId || '' - errorCode = error.code - reason = `CodeWhisperer Invocation Exception: ${error?.code ?? error?.name ?? 'unknown'}` - await this.onThrottlingException(error, triggerType) - - if (error?.code === 'AccessDeniedException' && errorMessage?.includes('no identity-based policy')) { - getLogger().error('amazonq inline-suggest: AccessDeniedException : %s', (error as Error).message) - void vscode.window - .showErrorMessage(`CodeWhisperer: ${error?.message}`, CodeWhispererConstants.settingsLearnMore) - .then(async (resp) => { - if (resp === CodeWhispererConstants.settingsLearnMore) { - void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUri)) - } - }) - await vscode.commands.executeCommand('aws.amazonq.enableCodeSuggestions', false) - } - } else { - errorMessage = error instanceof Error ? error.message : String(error) - reason = error ? String(error) : 'unknown' - } - } finally { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone - - let msg = indent( - `codewhisperer: request-id: ${requestId}, - timestamp(epoch): ${Date.now()}, - timezone: ${timezone}, - datetime: ${new Date().toLocaleString([], { timeZone: timezone })}, - vscode version: '${vscode.version}', - extension version: '${extensionVersion}', - filename: '${EditorContext.getFileName(editor)}', - left context of line: '${session.leftContextOfCurrentLine}', - line number: ${session.startPos.line}, - character location: ${session.startPos.character}, - latency: ${latency} ms. - Recommendations:`, - 4, - true - ).trimStart() - for (const [index, item] of recommendations.entries()) { - msg += `\n ${index.toString().padStart(2, '0')}: ${indent(item.content, 8, true).trim()}` - session.requestIdList.push(requestId) - } - getLogger('nextEditPrediction').debug(`codeWhisper request ${requestId}`) - if (invocationResult === 'Succeeded') { - CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() - UserWrittenCodeTracker.instance.onQFeatureInvoked() - } else { - if ( - (errorMessage?.includes(invalidCustomizationMessage) && errorCode === 'AccessDeniedException') || - errorCode === 'ResourceNotFoundException' - ) { - getLogger() - .debug(`The selected customization is no longer available. Retrying with the default model. - Failed request id: ${requestId}`) - await switchToBaseCustomizationAndNotify() - await this.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - pagination, - page, - true - ) - } - } - - if (shouldRecordServiceInvocation) { - TelemetryHelper.instance.recordServiceInvocationTelemetry( - requestId, - sessionId, - session.recommendations.length + recommendations.length - 1, - invocationResult, - latency, - session.language, - session.taskType, - reason, - session.requestContext.supplementalMetadata - ) - } - } - - if (this.isCancellationRequested()) { - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: session.recommendations.length, - }) - } - - const typedPrefix = editor.document - .getText(new vscode.Range(session.startPos, editor.selection.active)) - .replace('\r\n', '\n') - if (recommendations.length > 0) { - TelemetryHelper.instance.setTypeAheadLength(typedPrefix.length) - // mark suggestions that does not match typeahead when arrival as Discard - // these suggestions can be marked as Showed if typeahead can be removed with new inline API - for (const [i, r] of recommendations.entries()) { - const recommendationIndex = i + session.recommendations.length - if ( - !r.content.startsWith(typedPrefix) && - session.getSuggestionState(recommendationIndex) === undefined - ) { - session.setSuggestionState(recommendationIndex, 'Discard') - } - session.setCompletionType(recommendationIndex, r) - } - session.recommendations = pagination ? session.recommendations.concat(recommendations) : recommendations - if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix)) { - this._onDidReceiveRecommendation.fire() - } - } - - this.requestId = requestId - session.sessionId = sessionId - this.nextToken = nextToken - - // send Empty userDecision event if user receives no recommendations in this session at all. - if (invocationResult === 'Succeeded' && nextToken === '') { - // case 1: empty list of suggestion [] - if (session.recommendations.length === 0) { - session.requestIdList.push(requestId) - // Received an empty list of recommendations - TelemetryHelper.instance.recordUserDecisionTelemetryForEmptyList( - session.requestIdList, - sessionId, - page, - runtimeLanguageContext.getLanguageContext( - editor.document.languageId, - path.extname(editor.document.fileName) - ).language, - session.requestContext.supplementalMetadata - ) - } - // case 2: non empty list of suggestion but with (a) empty content or (b) non-matching typeahead - else if (!this.hasAtLeastOneValidSuggestion(typedPrefix)) { - this.reportUserDecisions(-1) - } - } - return Promise.resolve({ - result: invocationResult, - errorMessage: errorMessage, - recommendationCount: session.recommendations.length, - }) - } - - hasAtLeastOneValidSuggestion(typedPrefix: string): boolean { - return session.recommendations.some((r) => r.content.trim() !== '' && r.content.startsWith(typedPrefix)) - } - - cancelPaginatedRequest() { - this.nextToken = '' - this.cancellationToken.cancel() - } - - isCancellationRequested() { - return this.cancellationToken.token.isCancellationRequested - } - - checkAndResetCancellationTokens() { - if (this.isCancellationRequested()) { - this.cancellationToken.dispose() - this.cancellationToken = new vscode.CancellationTokenSource() - this.nextToken = '' - return true - } - return false - } - /** - * Clear recommendation state - */ - clearRecommendations() { - session.requestIdList = [] - session.recommendations = [] - session.suggestionStates = new Map() - session.completionTypes = new Map() - this.requestId = '' - session.sessionId = '' - this.nextToken = '' - session.requestContext.supplementalMetadata = undefined - } - - async clearInlineCompletionStates() { - try { - vsCodeState.isCodeWhispererEditing = false - application()._clearCodeWhispererUIListener.fire() - this.cancelPaginatedRequest() - this.clearRecommendations() - this.disposeInlineCompletion() - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - // fix a regression that requires user to hit Esc twice to clear inline ghost text - // because disposing a provider does not clear the UX - if (isVscHavingRegressionInlineCompletionApi()) { - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - } - } finally { - this.clearRejectionTimer() - } - } - - reportDiscardedUserDecisions() { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - this.reportUserDecisions(-1) - } - - /** - * Emits telemetry reflecting user decision for current recommendation. - */ - reportUserDecisions(acceptIndex: number) { - if (session.sessionId === '' || this.requestId === '') { - return - } - TelemetryHelper.instance.recordUserDecisionTelemetry( - session.requestIdList, - session.sessionId, - session.recommendations, - acceptIndex, - session.recommendations.length, - session.completionTypes, - session.suggestionStates, - session.requestContext.supplementalMetadata - ) - if (isInlineCompletionEnabled()) { - this.clearInlineCompletionStates().catch((e) => { - getLogger().error('clearInlineCompletionStates failed: %s', (e as Error).message) - }) - } - } - - hasNextToken(): boolean { - return this.nextToken !== '' - } - - canShowRecommendationInIntelliSense( - editor: vscode.TextEditor, - showPrompt: boolean = false, - response: GetRecommendationsResponse - ): boolean { - const reject = () => { - this.reportUserDecisions(-1) - } - if (!this.isValidResponse()) { - if (showPrompt) { - void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 3000) - } - reject() - return false - } - // do not show recommendation if cursor is before invocation position - // also mark as Discard - if (editor.selection.active.isBefore(session.startPos)) { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - reject() - return false - } - - // do not show recommendation if typeahead does not match - // also mark as Discard - const typedPrefix = editor.document.getText( - new vscode.Range( - session.startPos.line, - session.startPos.character, - editor.selection.active.line, - editor.selection.active.character - ) - ) - if (!session.recommendations[0].content.startsWith(typedPrefix.trimStart())) { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - reject() - return false - } - return true - } - - async onThrottlingException(awsError: AWSError, triggerType: CodewhispererTriggerType) { - if ( - awsError.code === 'ThrottlingException' && - awsError.message.includes(CodeWhispererConstants.throttlingMessage) - ) { - if (triggerType === 'OnDemand') { - void vscode.window.showErrorMessage(CodeWhispererConstants.freeTierLimitReached) - } - vsCodeState.isFreeTierLimitReached = true - } - } - - public disposeInlineCompletion() { - this.inlineCompletionProviderDisposable?.dispose() - this.inlineCompletionProvider = undefined - } - - private disposeCommandOverrides() { - this.prev.dispose() - this.reject.dispose() - this.next.dispose() - } - - // These commands override the vs code inline completion commands - // They are subscribed when suggestion starts and disposed when suggestion is accepted/rejected - // to avoid impacting other plugins or user who uses this API - private registerCommandOverrides() { - const { prevCommand, nextCommand, rejectCommand } = createCommands() - this.prev = prevCommand.register() - this.next = nextCommand.register() - this.reject = rejectCommand.register() - } - - subscribeSuggestionCommands() { - this.disposeCommandOverrides() - this.registerCommandOverrides() - globals.context.subscriptions.push(this.prev) - globals.context.subscriptions.push(this.next) - globals.context.subscriptions.push(this.reject) - } - - async showRecommendation(indexShift: number, noSuggestionVisible: boolean = false) { - await lock.acquire(updateInlineLockKey, async () => { - if (!vscode.window.state.focused) { - this.reportDiscardedUserDecisions() - return - } - const inlineCompletionProvider = new CWInlineCompletionItemProvider( - this.inlineCompletionProvider?.getActiveItemIndex, - indexShift, - session.recommendations, - this.requestId, - session.startPos, - this.nextToken - ) - this.inlineCompletionProviderDisposable?.dispose() - // when suggestion is active, registering a new provider will let VS Code invoke inline API automatically - this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( - Object.assign([], CodeWhispererConstants.platformLanguageIds), - inlineCompletionProvider - ) - this.inlineCompletionProvider = inlineCompletionProvider - - if (isVscHavingRegressionInlineCompletionApi() && !noSuggestionVisible) { - // fix a regression in new VS Code when disposing and re-registering - // a new provider does not auto refresh the inline suggestion widget - // by manually refresh it - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - } - if (noSuggestionVisible) { - await vscode.commands.executeCommand(`editor.action.inlineSuggest.trigger`) - this.sendPerceivedLatencyTelemetry() - } - }) - } - - async onEditorChange() { - this.reportUserDecisions(-1) - } - - async onFocusChange() { - this.reportUserDecisions(-1) - } - - async onCursorChange(e: vscode.TextEditorSelectionChangeEvent) { - // we do not want to reset the states for keyboard events because they can be typeahead - if ( - e.kind !== vscode.TextEditorSelectionChangeKind.Keyboard && - vscode.window.activeTextEditor === e.textEditor - ) { - application()._clearCodeWhispererUIListener.fire() - // when cursor change due to mouse movement we need to reset the active item index for inline - if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { - this.inlineCompletionProvider?.clearActiveItemIndex() - } - } - } - - isSuggestionVisible(): boolean { - return this.inlineCompletionProvider?.getActiveItemIndex !== undefined - } - - async tryShowRecommendation() { - const editor = vscode.window.activeTextEditor - if (editor === undefined) { - return - } - if (this.isSuggestionVisible()) { - // do not force refresh the tooltip to avoid suggestion "flashing" - return - } - if ( - editor.selection.active.isBefore(session.startPos) || - editor.document.uri.fsPath !== this.documentUri?.fsPath - ) { - for (const [i, _] of session.recommendations.entries()) { - session.setSuggestionState(i, 'Discard') - } - this.reportUserDecisions(-1) - } else if (session.recommendations.length > 0) { - await this.showRecommendation(0, true) - } - } - - private clearRejectionTimer() { - if (this._timer !== undefined) { - clearInterval(this._timer) - this._timer = undefined - } - } - - private sendPerceivedLatencyTelemetry() { - if (vscode.window.activeTextEditor) { - const languageContext = runtimeLanguageContext.getLanguageContext( - vscode.window.activeTextEditor.document.languageId, - vscode.window.activeTextEditor.document.fileName.substring( - vscode.window.activeTextEditor.document.fileName.lastIndexOf('.') + 1 - ) - ) - telemetry.codewhisperer_perceivedLatency.emit({ - codewhispererRequestId: this.requestId, - codewhispererSessionId: session.sessionId, - codewhispererTriggerType: session.triggerType, - codewhispererCompletionType: session.getCompletionType(0), - codewhispererCustomizationArn: getSelectedCustomization().arn, - codewhispererLanguage: languageContext.language, - duration: performance.now() - this.lastInvocationTime, - passive: true, - credentialStartUrl: AuthUtil.instance.startUrl, - result: 'Succeeded', - }) - } - } -} diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts deleted file mode 100644 index de78b435913..00000000000 --- a/packages/core/src/codewhisperer/service/recommendationService.ts +++ /dev/null @@ -1,122 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import { ConfigurationEntry, GetRecommendationsResponse } from '../models/model' -import { isInlineCompletionEnabled } from '../util/commonUtil' -import { - CodewhispererAutomatedTriggerType, - CodewhispererTriggerType, - telemetry, -} from '../../shared/telemetry/telemetry' -import { InlineCompletionService } from '../service/inlineCompletionService' -import { ClassifierTrigger } from './classifierTrigger' -import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { randomUUID } from '../../shared/crypto' -import { TelemetryHelper } from '../util/telemetryHelper' -import { AuthUtil } from '../util/authUtil' - -export interface SuggestionActionEvent { - readonly editor: vscode.TextEditor | undefined - readonly isRunning: boolean - readonly triggerType: CodewhispererTriggerType - readonly response: GetRecommendationsResponse | undefined -} - -export class RecommendationService { - static #instance: RecommendationService - - private _isRunning: boolean = false - get isRunning() { - return this._isRunning - } - - private _onSuggestionActionEvent = new vscode.EventEmitter() - get suggestionActionEvent(): vscode.Event { - return this._onSuggestionActionEvent.event - } - - private _acceptedSuggestionCount: number = 0 - get acceptedSuggestionCount() { - return this._acceptedSuggestionCount - } - - private _totalValidTriggerCount: number = 0 - get totalValidTriggerCount() { - return this._totalValidTriggerCount - } - - public static get instance() { - return (this.#instance ??= new RecommendationService()) - } - - incrementAcceptedCount() { - this._acceptedSuggestionCount++ - } - - incrementValidTriggerCount() { - this._totalValidTriggerCount++ - } - - async generateRecommendation( - client: DefaultCodeWhispererClient, - editor: vscode.TextEditor, - triggerType: CodewhispererTriggerType, - config: ConfigurationEntry, - autoTriggerType?: CodewhispererAutomatedTriggerType, - event?: vscode.TextDocumentChangeEvent - ) { - // TODO: should move all downstream auth check(inlineCompletionService, recommendationHandler etc) to here(upstream) instead of spreading everywhere - if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { - return - } - - if (this._isRunning) { - return - } - - /** - * Use an existing trace ID if invoked through a command (e.g., manual invocation), - * otherwise generate a new trace ID - */ - const traceId = telemetry.attributes?.traceId ?? randomUUID() - TelemetryHelper.instance.setTraceId(traceId) - await telemetry.withTraceId(async () => { - if (isInlineCompletionEnabled()) { - if (triggerType === 'OnDemand') { - ClassifierTrigger.instance.recordClassifierResultForManualTrigger(editor) - } - - this._isRunning = true - let response: GetRecommendationsResponse | undefined = undefined - - try { - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: true, - triggerType: triggerType, - response: undefined, - }) - - response = await InlineCompletionService.instance.getPaginatedRecommendation( - client, - editor, - triggerType, - config, - autoTriggerType, - event - ) - } finally { - this._isRunning = false - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: false, - triggerType: triggerType, - response: response, - }) - } - } - }, traceId) - } -} diff --git a/packages/core/src/codewhisperer/views/activeStateController.ts b/packages/core/src/codewhisperer/views/activeStateController.ts index b3c991a9d38..6994ef8af9a 100644 --- a/packages/core/src/codewhisperer/views/activeStateController.ts +++ b/packages/core/src/codewhisperer/views/activeStateController.ts @@ -6,13 +6,9 @@ import * as vscode from 'vscode' import { LineSelection, LinesChangeEvent } from '../tracker/lineTracker' import { isTextEditor } from '../../shared/utilities/editorUtilities' -import { RecommendationService, SuggestionActionEvent } from '../service/recommendationService' import { subscribeOnce } from '../../shared/utilities/vsCodeUtils' import { Container } from '../service/serviceContainer' -import { RecommendationHandler } from '../service/recommendationHandler' import { cancellableDebounce } from '../../shared/utilities/functionUtils' -import { telemetry } from '../../shared/telemetry/telemetry' -import { TelemetryHelper } from '../util/telemetryHelper' export class ActiveStateController implements vscode.Disposable { private readonly _disposable: vscode.Disposable @@ -34,14 +30,14 @@ export class ActiveStateController implements vscode.Disposable { constructor(private readonly container: Container) { this._disposable = vscode.Disposable.from( - RecommendationService.instance.suggestionActionEvent(async (e) => { - await telemetry.withTraceId(async () => { - await this.onSuggestionActionEvent(e) - }, TelemetryHelper.instance.traceId) - }), - RecommendationHandler.instance.onDidReceiveRecommendation(async (_) => { - await this.onDidReceiveRecommendation() - }), + // RecommendationService.instance.suggestionActionEvent(async (e) => { + // await telemetry.withTraceId(async () => { + // await this.onSuggestionActionEvent(e) + // }, TelemetryHelper.instance.traceId) + // }), + // RecommendationHandler.instance.onDidReceiveRecommendation(async (_) => { + // await this.onDidReceiveRecommendation() + // }), this.container.lineTracker.onDidChangeActiveLines(async (e) => { await this.onActiveLinesChanged(e) }), @@ -70,32 +66,32 @@ export class ActiveStateController implements vscode.Disposable { await this._refresh(vscode.window.activeTextEditor) } - private async onSuggestionActionEvent(e: SuggestionActionEvent) { - if (!this._isReady) { - return - } - - this.clear(e.editor) // do we need this? - if (e.triggerType === 'OnDemand' && e.isRunning) { - // if user triggers on demand, immediately update the UI and cancel the previous debounced update if there is one - this.refreshDebounced.cancel() - await this._refresh(this._editor) - } else { - await this.refreshDebounced.promise(e.editor) - } - } - - private async onDidReceiveRecommendation() { - if (!this._isReady) { - return - } - - if (this._editor && this._editor === vscode.window.activeTextEditor) { - // receives recommendation, immediately update the UI and cacnel the debounced update if there is one - this.refreshDebounced.cancel() - await this._refresh(this._editor, false) - } - } + // private async onSuggestionActionEvent(e: SuggestionActionEvent) { + // if (!this._isReady) { + // return + // } + + // this.clear(e.editor) // do we need this? + // if (e.triggerType === 'OnDemand' && e.isRunning) { + // // if user triggers on demand, immediately update the UI and cancel the previous debounced update if there is one + // this.refreshDebounced.cancel() + // await this._refresh(this._editor) + // } else { + // await this.refreshDebounced.promise(e.editor) + // } + // } + + // private async onDidReceiveRecommendation() { + // if (!this._isReady) { + // return + // } + + // if (this._editor && this._editor === vscode.window.activeTextEditor) { + // // receives recommendation, immediately update the UI and cacnel the debounced update if there is one + // this.refreshDebounced.cancel() + // await this._refresh(this._editor, false) + // } + // } private async onActiveLinesChanged(e: LinesChangeEvent) { if (!this._isReady) { @@ -147,7 +143,7 @@ export class ActiveStateController implements vscode.Disposable { if (shouldDisplay !== undefined) { await this.updateDecorations(editor, selections, shouldDisplay) } else { - await this.updateDecorations(editor, selections, RecommendationService.instance.isRunning) + await this.updateDecorations(editor, selections, true) } } diff --git a/packages/core/src/codewhisperer/views/lineAnnotationController.ts b/packages/core/src/codewhisperer/views/lineAnnotationController.ts index 8b1d38ed7ae..ae853a25739 100644 --- a/packages/core/src/codewhisperer/views/lineAnnotationController.ts +++ b/packages/core/src/codewhisperer/views/lineAnnotationController.ts @@ -9,18 +9,17 @@ import { LineSelection, LinesChangeEvent } from '../tracker/lineTracker' import { isTextEditor } from '../../shared/utilities/editorUtilities' import { cancellableDebounce } from '../../shared/utilities/functionUtils' import { subscribeOnce } from '../../shared/utilities/vsCodeUtils' -import { RecommendationService } from '../service/recommendationService' +// import { RecommendationService } from '../service/recommendationService' import { AnnotationChangeSource, inlinehintKey } from '../models/constants' import globals from '../../shared/extensionGlobals' import { Container } from '../service/serviceContainer' import { telemetry } from '../../shared/telemetry/telemetry' import { getLogger } from '../../shared/logger/logger' import { Commands } from '../../shared/vscode/commands2' -import { session } from '../util/codeWhispererSession' -import { RecommendationHandler } from '../service/recommendationHandler' +// import { session } from '../util/codeWhispererSession' +// import { RecommendationHandler } from '../service/recommendationHandler' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { setContext } from '../../shared/vscode/setContext' -import { TelemetryHelper } from '../util/telemetryHelper' const case3TimeWindow = 30000 // 30 seconds @@ -75,13 +74,14 @@ export class AutotriggerState implements AnnotationState { static acceptedCount = 0 updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { - if (AutotriggerState.acceptedCount < RecommendationService.instance.acceptedSuggestionCount) { - return new ManualtriggerState() - } else if (session.recommendations.length > 0 && RecommendationHandler.instance.isSuggestionVisible()) { - return new PressTabState() - } else { - return this - } + // if (AutotriggerState.acceptedCount < RecommendationService.instance.acceptedSuggestionCount) { + // return new ManualtriggerState() + // } else if (session.recommendations.length > 0 && RecommendationHandler.instance.isSuggestionVisible()) { + // return new PressTabState() + // } else { + // return this + // } + return undefined } isNextState(state: AnnotationState | undefined): boolean { @@ -268,28 +268,28 @@ export class LineAnnotationController implements vscode.Disposable { subscribeOnce(this.container.lineTracker.onReady)(async (_) => { await this.onReady() }), - RecommendationService.instance.suggestionActionEvent(async (e) => { - await telemetry.withTraceId(async () => { - if (!this._isReady) { - return - } - - if (this._currentState instanceof ManualtriggerState) { - if (e.triggerType === 'OnDemand' && this._currentState.hasManualTrigger === false) { - this._currentState.hasManualTrigger = true - } - if ( - e.response?.recommendationCount !== undefined && - e.response?.recommendationCount > 0 && - this._currentState.hasValidResponse === false - ) { - this._currentState.hasValidResponse = true - } - } - - await this.refresh(e.editor, 'codewhisperer') - }, TelemetryHelper.instance.traceId) - }), + // RecommendationService.instance.suggestionActionEvent(async (e) => { + // await telemetry.withTraceId(async () => { + // if (!this._isReady) { + // return + // } + + // if (this._currentState instanceof ManualtriggerState) { + // if (e.triggerType === 'OnDemand' && this._currentState.hasManualTrigger === false) { + // this._currentState.hasManualTrigger = true + // } + // if ( + // e.response?.recommendationCount !== undefined && + // e.response?.recommendationCount > 0 && + // this._currentState.hasValidResponse === false + // ) { + // this._currentState.hasValidResponse = true + // } + // } + + // await this.refresh(e.editor, 'codewhisperer') + // }, TelemetryHelper.instance.traceId) + // }), this.container.lineTracker.onDidChangeActiveLines(async (e) => { await this.onActiveLinesChanged(e) }), @@ -484,7 +484,7 @@ export class LineAnnotationController implements vscode.Disposable { source: AnnotationChangeSource, force?: boolean ): Partial | undefined { - const isCWRunning = RecommendationService.instance.isRunning + const isCWRunning = true const textOptions: vscode.ThemableDecorationAttachmentRenderOptions = { contentText: '', @@ -517,9 +517,9 @@ export class LineAnnotationController implements vscode.Disposable { this._currentState = updatedState // take snapshot of accepted session so that we can compre if there is delta -> users accept 1 suggestion after seeing this state - AutotriggerState.acceptedCount = RecommendationService.instance.acceptedSuggestionCount + AutotriggerState.acceptedCount = 0 // take snapshot of total trigger count so that we can compare if there is delta -> users accept/reject suggestions after seeing this state - TryMoreExState.triggerCount = RecommendationService.instance.totalValidTriggerCount + TryMoreExState.triggerCount = 0 textOptions.contentText = this._currentState.text() diff --git a/packages/core/src/test/codewhisperer/testUtil.ts b/packages/core/src/test/codewhisperer/testUtil.ts index f3b82fd3850..b0c66d6552f 100644 --- a/packages/core/src/test/codewhisperer/testUtil.ts +++ b/packages/core/src/test/codewhisperer/testUtil.ts @@ -23,7 +23,6 @@ import { HttpResponse, Service } from 'aws-sdk' import userApiConfig = require('./../../codewhisperer/client/user-service-2.json') import CodeWhispererUserClient = require('../../codewhisperer/client/codewhispereruserclient') import { codeWhispererClient } from '../../codewhisperer/client/codewhisperer' -import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' import * as model from '../../codewhisperer/models/model' import { stub } from '../utilities/stubber' import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports @@ -36,7 +35,6 @@ export async function resetCodeWhispererGlobalVariables() { session.reset() await globals.globalState.clear() await CodeSuggestionsState.instance.setSuggestionsEnabled(true) - await RecommendationHandler.instance.clearInlineCompletionStates() } export function createMockDocument( diff --git a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts deleted file mode 100644 index 0038795ad89..00000000000 --- a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' -import { ConfigurationEntry } from '../../codewhisperer/models/model' -import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' -import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' -import { createMockTextEditor, resetCodeWhispererGlobalVariables } from '../../test/codewhisperer/testUtil' -import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' -import { session } from '../../codewhisperer/util/codeWhispererSession' - -/* -New model deployment may impact references returned. - -These tests: - 1) are not required for github approval flow - 2) will be auto-skipped until fix for manual runs is posted. -*/ - -const leftContext = `InAuto.GetContent( - InAuto.servers.auto, "vendors.json", - function (data) { - let block = ''; - for(let i = 0; i < data.length; i++) { - block += '' + cars[i].title + ''; - } - $('#cars').html(block); - });` - -describe('CodeWhisperer service invocation', async function () { - let validConnection: boolean - const client = new codewhispererClient.DefaultCodeWhispererClient() - const configWithRefs: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - const configWithNoRefs: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: false, - } - - before(async function () { - validConnection = await setValidConnection() - }) - - beforeEach(function () { - void resetCodeWhispererGlobalVariables() - RecommendationHandler.instance.clearRecommendations() - // TODO: remove this line (this.skip()) when these tests no longer auto-skipped - this.skip() - // valid connection required to run tests - skipTestIfNoValidConn(validConnection, this) - }) - - it('trigger known to return recs with references returns rec with reference', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const doc = leftContext + rightContext - const filename = 'test.js' - const language = 'javascript' - const line = 5 - const character = 39 - const mockEditor = createMockTextEditor(doc, filename, language, line, character) - - await invokeRecommendation(mockEditor, client, configWithRefs) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - const references = session.recommendations[0].references - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - assert.ok(validRecs) - assert.ok(references !== undefined) - // TODO: uncomment this assert when this test is no longer auto-skipped - // assert.ok(references.length > 0) - }) - - // This test will fail if user is logged in with IAM identity center - it('trigger known to return rec with references does not return rec with references when reference tracker setting is off', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const doc = leftContext + rightContext - const filename = 'test.js' - const language = 'javascript' - const line = 5 - const character = 39 - const mockEditor = createMockTextEditor(doc, filename, language, line, character) - - await invokeRecommendation(mockEditor, client, configWithNoRefs) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - // no recs returned because example request returns 1 rec with reference, so no recs returned when references off - assert.ok(!validRecs) - }) -}) diff --git a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts deleted file mode 100644 index d4265d13982..00000000000 --- a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as vscode from 'vscode' -import * as path from 'path' -import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' -import { ConfigurationEntry } from '../../codewhisperer/models/model' -import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' -import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' -import { - createMockTextEditor, - createTextDocumentChangeEvent, - resetCodeWhispererGlobalVariables, -} from '../../test/codewhisperer/testUtil' -import { KeyStrokeHandler } from '../../codewhisperer/service/keyStrokeHandler' -import { sleep } from '../../shared/utilities/timeoutUtils' -import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' -import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' -import { session } from '../../codewhisperer/util/codeWhispererSession' - -describe('CodeWhisperer service invocation', async function () { - let validConnection: boolean - const client = new codewhispererClient.DefaultCodeWhispererClient() - const config: ConfigurationEntry = { - isShowMethodsEnabled: true, - isManualTriggerEnabled: true, - isAutomatedTriggerEnabled: true, - isSuggestionsWithCodeReferencesEnabled: true, - } - - before(async function () { - validConnection = await setValidConnection() - }) - - beforeEach(function () { - void resetCodeWhispererGlobalVariables() - RecommendationHandler.instance.clearRecommendations() - // valid connection required to run tests - skipTestIfNoValidConn(validConnection, this) - }) - - it('manual trigger returns valid recommendation response', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const mockEditor = createMockTextEditor() - await invokeRecommendation(mockEditor, client, config) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - assert.ok(validRecs) - }) - - it('auto trigger returns valid recommendation response', async function () { - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const mockEditor = createMockTextEditor() - - const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( - mockEditor.document, - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), - '\n' - ) - - await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, client, config) - // wait for 5 seconds to allow time for response to be generated - await sleep(5000) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length > 0) - assert.ok(sessionId.length > 0) - assert.ok(validRecs) - }) - - it('invocation in unsupported language does not generate a request', async function () { - const workspaceFolder = getTestWorkspaceFolder() - const appRoot = path.join(workspaceFolder, 'go1-plain-sam-app') - const appCodePath = path.join(appRoot, 'hello-world', 'main.go') - - // check that handler is empty before invocation - const requestIdBefore = RecommendationHandler.instance.requestId - const sessionIdBefore = session.sessionId - const validRecsBefore = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestIdBefore.length === 0) - assert.ok(sessionIdBefore.length === 0) - assert.ok(!validRecsBefore) - - const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(appCodePath)) - const editor = await vscode.window.showTextDocument(doc) - await invokeRecommendation(editor, client, config) - - const requestId = RecommendationHandler.instance.requestId - const sessionId = session.sessionId - const validRecs = RecommendationHandler.instance.isValidResponse() - - assert.ok(requestId.length === 0) - assert.ok(sessionId.length === 0) - assert.ok(!validRecs) - }) -}) From f11489404385c457ebb7b9b793c069c39b0b54b2 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Tue, 6 May 2025 17:46:16 -0400 Subject: [PATCH 04/48] refactor(amazonq): deprecate codewhisperer context utils (#7241) ## Problem deprecation for inline ## Solution deprecates: 1. supplemental context + its tests, since that was only passed into codewhisperer 2. codewhisperer coverage tracker, since thats [already in flare](https://github.com/aws/language-servers/blob/main/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts) 3. [classifier trigger](https://github.com/aws/language-servers/blob/main/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/coefficients.json), since that's in flare 4. inlineCompletionProvider is no longer used and should have been in my other PR 5. editorContext utils, since that was used with supplemental context --- - 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/app/inline/activation.ts | 2 - .../codewhispererCodeCoverageTracker.test.ts | 560 ---------------- .../test/unit/codewhisperer/util/bm25.test.ts | 117 ---- .../util/closingBracketUtil.test.ts | 389 ----------- .../util/codeParsingUtil.test.ts | 327 ---------- .../codewhisperer/util/commonUtil.test.ts | 81 --- .../util/crossFileContextUtil.test.ts | 454 ------------- .../codewhisperer/util/editorContext.test.ts | 170 ----- .../util/globalStateUtil.test.ts | 42 -- .../util/supplemetalContextUtil.test.ts | 265 -------- .../unit/codewhisperer/util/utgUtils.test.ts | 63 -- packages/core/src/codewhisperer/activation.ts | 6 - packages/core/src/codewhisperer/index.ts | 14 +- .../service/classifierTrigger.ts | 609 ------------------ .../service/inlineCompletionItemProvider.ts | 194 ------ .../codewhispererCodeCoverageTracker.ts | 319 --------- .../codewhisperer/util/closingBracketUtil.ts | 262 -------- .../core/src/codewhisperer/util/commonUtil.ts | 62 -- .../src/codewhisperer/util/editorContext.ts | 289 --------- .../src/codewhisperer/util/globalStateUtil.ts | 23 - .../supplementalContext/codeParsingUtil.ts | 130 ---- .../crossFileContextUtil.ts | 395 ------------ .../util/supplementalContext/rankBm25.ts | 137 ---- .../supplementalContextUtil.ts | 137 ---- .../util/supplementalContext/utgUtils.ts | 229 ------- .../core/src/test/codewhisperer/testUtil.ts | 2 - 26 files changed, 1 insertion(+), 5277 deletions(-) delete mode 100644 packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/closingBracketUtil.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts delete mode 100644 packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts delete mode 100644 packages/core/src/codewhisperer/service/classifierTrigger.ts delete mode 100644 packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts delete mode 100644 packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts delete mode 100644 packages/core/src/codewhisperer/util/closingBracketUtil.ts delete mode 100644 packages/core/src/codewhisperer/util/editorContext.ts delete mode 100644 packages/core/src/codewhisperer/util/globalStateUtil.ts delete mode 100644 packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts delete mode 100644 packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts delete mode 100644 packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts delete mode 100644 packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts delete mode 100644 packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts diff --git a/packages/amazonq/src/app/inline/activation.ts b/packages/amazonq/src/app/inline/activation.ts index f1a45c9158c..69515127441 100644 --- a/packages/amazonq/src/app/inline/activation.ts +++ b/packages/amazonq/src/app/inline/activation.ts @@ -6,7 +6,6 @@ import vscode from 'vscode' import { AuthUtil, - CodeWhispererCodeCoverageTracker, CodeWhispererConstants, isInlineCompletionEnabled, runtimeLanguageContext, @@ -39,7 +38,6 @@ export async function activate() { return } - CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) UserWrittenCodeTracker.instance.onTextDocumentChange(e) /** * Handle this keystroke event only when diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts deleted file mode 100644 index ee001b3328d..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts +++ /dev/null @@ -1,560 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as sinon from 'sinon' -import * as vscode from 'vscode' -import { - CodeWhispererCodeCoverageTracker, - vsCodeState, - TelemetryHelper, - AuthUtil, - getUnmodifiedAcceptedTokens, -} from 'aws-core-vscode/codewhisperer' -import { createMockDocument, createMockTextEditor, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' -import { globals } from 'aws-core-vscode/shared' -import { assertTelemetryCurried } from 'aws-core-vscode/test' - -describe('codewhispererCodecoverageTracker', function () { - const language = 'python' - - describe('test getTracker', function () { - afterEach(async function () { - await resetCodeWhispererGlobalVariables() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('unsupported language', function () { - assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('vb'), undefined) - assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('ipynb'), undefined) - }) - - it('supported language', function () { - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('python'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascriptreact'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('java'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascript'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('cpp'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('ruby'), undefined) - assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('go'), undefined) - }) - - it('supported language and should return singleton object per language', function () { - let instance1: CodeWhispererCodeCoverageTracker | undefined - let instance2: CodeWhispererCodeCoverageTracker | undefined - instance1 = CodeWhispererCodeCoverageTracker.getTracker('java') - instance2 = CodeWhispererCodeCoverageTracker.getTracker('java') - assert.notStrictEqual(instance1, undefined) - assert.strictEqual(Object.is(instance1, instance2), true) - - instance1 = CodeWhispererCodeCoverageTracker.getTracker('python') - instance2 = CodeWhispererCodeCoverageTracker.getTracker('python') - assert.notStrictEqual(instance1, undefined) - assert.strictEqual(Object.is(instance1, instance2), true) - - instance1 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') - instance2 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') - assert.notStrictEqual(instance1, undefined) - assert.strictEqual(Object.is(instance1, instance2), true) - }) - }) - - describe('test isActive', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - afterEach(async function () { - await resetCodeWhispererGlobalVariables() - CodeWhispererCodeCoverageTracker.instances.clear() - sinon.restore() - }) - - it('inactive case: telemetryEnable = true, isConnected = false', function () { - sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) - sinon.stub(AuthUtil.instance, 'isConnected').returns(false) - - tracker = CodeWhispererCodeCoverageTracker.getTracker('python') - if (!tracker) { - assert.fail() - } - - assert.strictEqual(tracker.isActive(), false) - }) - - it('inactive case: telemetryEnabled = false, isConnected = false', function () { - sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(false) - sinon.stub(AuthUtil.instance, 'isConnected').returns(false) - - tracker = CodeWhispererCodeCoverageTracker.getTracker('java') - if (!tracker) { - assert.fail() - } - - assert.strictEqual(tracker.isActive(), false) - }) - - it('active case: telemetryEnabled = true, isConnected = true', function () { - sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) - sinon.stub(AuthUtil.instance, 'isConnected').returns(true) - - tracker = CodeWhispererCodeCoverageTracker.getTracker('javascript') - if (!tracker) { - assert.fail() - } - assert.strictEqual(tracker.isActive(), true) - }) - }) - - describe('updateAcceptedTokensCount', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should compute edit distance to update the accepted tokens', function () { - if (!tracker) { - assert.fail() - } - const editor = createMockTextEditor('def addTwoNumbers(a, b):\n') - - tracker.addAcceptedTokens(editor.document.fileName, { - range: new vscode.Range(0, 0, 0, 25), - text: `def addTwoNumbers(x, y):\n`, - accepted: 25, - }) - tracker.addTotalTokens(editor.document.fileName, 100) - tracker.updateAcceptedTokensCount(editor) - assert.strictEqual(tracker?.acceptedTokens[editor.document.fileName][0].accepted, 23) - }) - }) - - describe('getUnmodifiedAcceptedTokens', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should return correct unmodified accepted tokens count', function () { - assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fou'), 2) - assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'f11111oo'), 3) - assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fo'), 2) - assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'HelloWorld'), 8) - assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'World'), 4) - assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CODE'), 1) - assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CodeWhispererGood'), 13) - }) - }) - - describe('countAcceptedTokens', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should skip when tracker is not active', function () { - if (!tracker) { - assert.fail() - } - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') - const spy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addAcceptedTokens') - assert.ok(!spy.called) - }) - - it('Should increase AcceptedTokens', function () { - if (!tracker) { - assert.fail() - } - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') - assert.deepStrictEqual(tracker.acceptedTokens['test.py'][0], { - range: new vscode.Range(0, 0, 0, 1), - text: 'a', - accepted: 1, - }) - }) - it('Should increase TotalTokens', function () { - if (!tracker) { - assert.fail() - } - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') - tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'b', 'test.py') - assert.deepStrictEqual(tracker.totalTokens['test.py'], 2) - }) - }) - - describe('countTotalTokens', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should skip when content change size is more than 50', function () { - if (!tracker) { - assert.fail() - } - tracker.countTotalTokens({ - reason: undefined, - document: createMockDocument(), - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 600), - rangeOffset: 0, - rangeLength: 600, - text: 'def twoSum(nums, target):\nfor '.repeat(20), - }, - ], - }) - assert.strictEqual(Object.keys(tracker.totalTokens).length, 0) - }) - - it('Should not skip when content change size is less than 50', function () { - if (!tracker) { - assert.fail() - } - tracker.countTotalTokens({ - reason: undefined, - document: createMockDocument(), - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 49), - rangeOffset: 0, - rangeLength: 49, - text: 'a = 123'.repeat(7), - }, - ], - }) - assert.strictEqual(Object.keys(tracker.totalTokens).length, 1) - assert.strictEqual(Object.values(tracker.totalTokens)[0], 49) - }) - - it('Should skip when CodeWhisperer is editing', function () { - if (!tracker) { - assert.fail() - } - vsCodeState.isCodeWhispererEditing = true - tracker.countTotalTokens({ - reason: undefined, - document: createMockDocument(), - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 30), - rangeOffset: 0, - rangeLength: 30, - text: 'def twoSum(nums, target):\nfor', - }, - ], - }) - const startedSpy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addTotalTokens') - assert.ok(!startedSpy.called) - }) - - it('Should not reduce tokens when delete', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('import math', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 0, - rangeLength: 0, - text: 'a', - }, - ], - }) - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 0, - rangeLength: 0, - text: 'b', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 1, - rangeLength: 1, - text: '', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - }) - - it('Should add tokens when type', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('import math', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 1), - rangeOffset: 0, - rangeLength: 0, - text: 'a', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when hitting enter with indentation', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('def h():', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '\n ', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when hitting enter with indentation in Windows', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('def h():', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '\r\n ', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when hitting enter with indentation in Java', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('class A() {', 'test.java', 'java') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 11), - rangeOffset: 0, - rangeLength: 0, - text: '', - }, - { - range: new vscode.Range(0, 0, 0, 11), - rangeOffset: 0, - rangeLength: 0, - text: '\n\t\t', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) - }) - - it('Should add tokens when inserting closing brackets', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('a=', 'test.py', 'python') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 3), - rangeOffset: 0, - rangeLength: 0, - text: '[]', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - }) - - it('Should add tokens when inserting closing brackets in Java', function () { - if (!tracker) { - assert.fail() - } - const doc = createMockDocument('class A ', 'test.java', 'java') - tracker.countTotalTokens({ - reason: undefined, - document: doc, - contentChanges: [ - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '{}', - }, - { - range: new vscode.Range(0, 0, 0, 8), - rangeOffset: 0, - rangeLength: 0, - text: '', - }, - ], - }) - assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) - }) - }) - - describe('flush', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('Should not send codecoverage telemetry if tracker is not active', function () { - if (!tracker) { - assert.fail() - } - sinon.restore() - sinon.stub(tracker, 'isActive').returns(false) - - tracker.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) - tracker.addTotalTokens(`test.py`, 100) - tracker.flush() - const data = globals.telemetry.logger.query({ - metricName: 'codewhisperer_codePercentage', - excludeKeys: ['awsAccount'], - }) - assert.strictEqual(data.length, 0) - }) - }) - - describe('emitCodeWhispererCodeContribution', function () { - let tracker: CodeWhispererCodeCoverageTracker | undefined - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - if (tracker) { - sinon.stub(tracker, 'isActive').returns(true) - } - }) - - afterEach(function () { - sinon.restore() - CodeWhispererCodeCoverageTracker.instances.clear() - }) - - it('should emit correct code coverage telemetry in python file', async function () { - const tracker = CodeWhispererCodeCoverageTracker.getTracker(language) - - const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') - tracker?.incrementServiceInvocationCount() - tracker?.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) - tracker?.addTotalTokens(`test.py`, 100) - tracker?.emitCodeWhispererCodeContribution() - assertTelemetry({ - codewhispererTotalTokens: 100, - codewhispererLanguage: language, - codewhispererAcceptedTokens: 7, - codewhispererSuggestedTokens: 7, - codewhispererPercentage: 7, - successCount: 1, - }) - }) - - it('should emit correct code coverage telemetry when success count = 0', async function () { - const tracker = CodeWhispererCodeCoverageTracker.getTracker('java') - - const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') - tracker?.addAcceptedTokens(`test.java`, { - range: new vscode.Range(0, 0, 0, 18), - text: `public static main`, - accepted: 18, - }) - tracker?.incrementServiceInvocationCount() - tracker?.incrementServiceInvocationCount() - tracker?.addTotalTokens(`test.java`, 30) - tracker?.emitCodeWhispererCodeContribution() - assertTelemetry({ - codewhispererTotalTokens: 30, - codewhispererLanguage: 'java', - codewhispererAcceptedTokens: 18, - codewhispererSuggestedTokens: 18, - codewhispererPercentage: 60, - successCount: 2, - }) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts b/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts deleted file mode 100644 index 0a3c4b17d60..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import { BM25Okapi } from 'aws-core-vscode/codewhisperer' - -describe('bm25', function () { - it('simple case 1', function () { - const query = 'windy London' - const corpus = ['Hello there good man!', 'It is quite windy in London', 'How is the weather today?'] - - const sut = new BM25Okapi(corpus) - const actual = sut.score(query) - - assert.deepStrictEqual(actual, [ - { - content: 'Hello there good man!', - index: 0, - score: 0, - }, - { - content: 'It is quite windy in London', - index: 1, - score: 0.937294722506405, - }, - { - content: 'How is the weather today?', - index: 2, - score: 0, - }, - ]) - - assert.deepStrictEqual(sut.topN(query, 1), [ - { - content: 'It is quite windy in London', - index: 1, - score: 0.937294722506405, - }, - ]) - }) - - it('simple case 2', function () { - const query = 'codewhisperer is a machine learning powered code generator' - const corpus = [ - 'codewhisperer goes GA at April 2023', - 'machine learning tool is the trending topic!!! :)', - 'codewhisperer is good =))))', - 'codewhisperer vs. copilot, which code generator better?', - 'copilot is a AI code generator too', - 'it is so amazing!!', - ] - - const sut = new BM25Okapi(corpus) - const actual = sut.score(query) - - assert.deepStrictEqual(actual, [ - { - content: 'codewhisperer goes GA at April 2023', - index: 0, - score: 0, - }, - { - content: 'machine learning tool is the trending topic!!! :)', - index: 1, - score: 2.597224531416621, - }, - { - content: 'codewhisperer is good =))))', - index: 2, - score: 0.3471790843435529, - }, - { - content: 'codewhisperer vs. copilot, which code generator better?', - index: 3, - score: 1.063018436525109, - }, - { - content: 'copilot is a AI code generator too', - index: 4, - score: 2.485359418462239, - }, - { - content: 'it is so amazing!!', - index: 5, - score: 0.3154033715392277, - }, - ]) - - assert.deepStrictEqual(sut.topN(query, 1), [ - { - content: 'machine learning tool is the trending topic!!! :)', - index: 1, - score: 2.597224531416621, - }, - ]) - - assert.deepStrictEqual(sut.topN(query, 3), [ - { - content: 'machine learning tool is the trending topic!!! :)', - index: 1, - score: 2.597224531416621, - }, - { - content: 'copilot is a AI code generator too', - index: 4, - score: 2.485359418462239, - }, - { - content: 'codewhisperer vs. copilot, which code generator better?', - index: 3, - score: 1.063018436525109, - }, - ]) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/closingBracketUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/closingBracketUtil.test.ts deleted file mode 100644 index bfdf9dc3d29..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/closingBracketUtil.test.ts +++ /dev/null @@ -1,389 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import assert from 'assert' -import { handleExtraBrackets } from 'aws-core-vscode/codewhisperer' -import { toTextEditor } from 'aws-core-vscode/test' - -describe('closingBracketUtil', function () { - /** - * leftContext + recommendation + rightContext - * startStart start end endEnd - */ - describe('handleExtraBrackets', function () { - async function assertClosingSymbolsHandler( - leftContext: string, - rightContext: string, - recommendation: string, - expected: string - ) { - const editor = await toTextEditor(leftContext + recommendation + rightContext, 'test.txt') - const document = editor.document - - const startStart = document.positionAt(0) - const endEnd = document.positionAt(editor.document.getText().length) - const start = document.positionAt(leftContext.length) - const end = document.positionAt(leftContext.length + recommendation.length) - - const left = document.getText(new vscode.Range(startStart, start)) - const right = document.getText(new vscode.Range(end, endEnd)) - const reco = document.getText(new vscode.Range(start, end)) - - assert.strictEqual(left, leftContext) - assert.strictEqual(right, rightContext) - assert.strictEqual(reco, recommendation) - - await handleExtraBrackets(editor, end, start) - - assert.strictEqual(editor.document.getText(), expected) - } - - it('should remove extra closing symbol', async function () { - /** - * public static void mergeSort(int[|] nums) { - * mergeSort(nums, 0, nums.length - 1); - * }|]) - */ - await assertClosingSymbolsHandler( - String.raw`public static void mergeSort(int[`, - String.raw`])`, - String.raw`] nums) { - mergeSort(nums, 0, nums.length - 1); -}`, - String.raw`public static void mergeSort(int[] nums) { - mergeSort(nums, 0, nums.length - 1); -}` - ) - - /** - * fun genericFunction<|T>(value: T): T { - * return value - * }|> - */ - await assertClosingSymbolsHandler( - String.raw`fun genericFunction<`, - String.raw`>`, - String.raw`T>(value: T): T { - return value -}`, - String.raw`fun genericFunction(value: T): T { - return value -}` - ) - - /** - * function getProperty(obj: T, key: K) {|> - */ - await assertClosingSymbolsHandler( - String.raw`function getProperty`, - String.raw`K extends keyof T>(obj: T, key: K) {`, - String.raw`function getProperty(obj: T, key: K) {` - ) - - /** - * public class Main { - * public static void main(|args: String[]) { - * System.out.println("Hello World"); - * }|) - * } - */ - await assertClosingSymbolsHandler( - String.raw`public class Main { - public static void main(`, - String.raw`) -}`, - String.raw`args: String[]) { - System.out.println("Hello World"); - }`, - String.raw`public class Main { - public static void main(args: String[]) { - System.out.println("Hello World"); - } -}` - ) - - /** - * function add2Numbers(a: number: b: number) { - * return a + b - * }) - */ - await assertClosingSymbolsHandler( - 'function add2Numbers(', - ')', - 'a: number, b: number) {\n return a + b\n}', - `function add2Numbers(a: number, b: number) {\n return a + b\n}` - ) - - /** - * function sum(a: number, b: number, c: number) { - * return a + b + c - * }) - */ - await assertClosingSymbolsHandler( - 'function sum(a: number, b: number, ', - ')', - 'c: number) {\n return a + b + c\n}', - `function sum(a: number, b: number, c: number) {\n return a + b + c\n}` - ) - - /** - * const aString = "hello world";" - */ - await assertClosingSymbolsHandler( - 'const aString = "', - '"', - 'hello world";', - `const aString = "hello world";` - ) - - /** - * { - * "userName": "john", - * "department": "codewhisperer"", - * } - */ - await assertClosingSymbolsHandler( - '{\n\t"userName": "john",\n\t"', - '"\n}', - 'department": "codewhisperer",', - '{\n\t"userName": "john",\n\t"department": "codewhisperer",\n}' - ) - - /** - * const someArray = [|"element1", "element2"];|] - */ - await assertClosingSymbolsHandler( - 'const anArray = [', - ']', - '"element1", "element2"];', - `const anArray = ["element1", "element2"];` - ) - - /** - * export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - * lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - * lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - * lt3: { |launchTemplateId: "lt-678919", launchTemplateName: "foobar" },| - * }; - */ - await assertClosingSymbolsHandler( - String.raw`export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - lt3: { `, - String.raw` - };`, - String.raw`launchTemplateId: "lt-678919", launchTemplateName: "foobar" },`, - String.raw`export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - lt3: { launchTemplateId: "lt-678919", launchTemplateName: "foobar" }, - };` - ) - - /** - * genericFunction<|T>|> () { - * if (T isInstanceOf string) { - * console.log(T) - * } else { - * // Do nothing - * } - * } - */ - await assertClosingSymbolsHandler( - String.raw`genericFunction<`, - String.raw`> () { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}`, - 'T>', - String.raw`genericFunction () { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}` - ) - - /** - * const rawStr = "|Foo";|" - * const anotherStr = "Bar" - */ - await assertClosingSymbolsHandler( - 'const rawStr = "', - '\nconst anotherStr = "Bar";', - 'Foo";', - String.raw`const rawStr = "Foo"; -const anotherStr = "Bar";` - ) - }) - - it('should not remove extra closing symbol', async function () { - /** - * describe('Foo', () => { - * describe('Bar', function () => { - * it('Boo', |() => { - * expect(true).toBe(true) - * }|) - * }) - * }) - */ - await assertClosingSymbolsHandler( - String.raw`describe('Foo', () => { - describe('Bar', function () { - it('Boo', `, - String.raw`) - }) -})`, - String.raw`() => { - expect(true).toBe(true) - }`, - String.raw`describe('Foo', () => { - describe('Bar', function () { - it('Boo', () => { - expect(true).toBe(true) - }) - }) -})` - ) - - /** - * function add2Numbers(|a: nuumber, b: number) { - * return a + b; - * }| - */ - await assertClosingSymbolsHandler( - 'function add2Numbers(', - '', - 'a: number, b: number) {\n return a + b;\n}', - `function add2Numbers(a: number, b: number) {\n return a + b;\n}` - ) - - /** - * export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - * lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - * lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - * lt3: |{ launchTemplateId: "lt-3456", launchTemplateName: "baz" },| - * } - */ - await assertClosingSymbolsHandler( - 'export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n lt3: ', - '\n};', - '{ launchTemplateId: "lt-3456", launchTemplateName: "baz" },', - `export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },\n};` - ) - - /** - * export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = { - * lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" }, - * lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" }, - * |lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },| - * } - */ - await assertClosingSymbolsHandler( - 'export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n ', - '\n};', - 'lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },', - 'export const launchTemplates: { [key: string]: AmazonEC2.LaunchTemplate } = {\n lt1: { launchTemplateId: "lt-1", launchTemplateName: "foo" },\n lt2: { launchTemplateId: "lt-2345", launchTemplateName: "bar" },\n lt3: { launchTemplateId: "lt-3456", launchTemplateName: "baz" },\n};' - ) - - /** - * const aString = "|hello world";| - */ - await assertClosingSymbolsHandler( - 'const aString = "', - '', - 'hello world";', - 'const aString = "hello world";' - ) - - /** genericFunction<|T> ()|> { - * if (T isInstanceOf string) { - * console.log(T) - * } else { - * // Do nothing - * } - * } - */ - await assertClosingSymbolsHandler( - 'genericFunction<', - String.raw` { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}`, - 'T> ()', - String.raw`genericFunction () { - if (T isInstanceOf string) { - console.log(T) - } else { - // Do nothing - } -}` - ) - - /** - * const rawStr = "|Foo";| - * const anotherStr = "Bar" - */ - await assertClosingSymbolsHandler( - 'const rawStr = "', - String.raw` -const anotherStr = "Bar";`, - 'Foo";', - String.raw`const rawStr = "Foo"; -const anotherStr = "Bar";` - ) - - /** - * function shouldReturnAhtmlDiv( { name } : Props) { - * if (!name) { - * return undefined - * } - * - * return ( - *
- * { name } - *
- * |) - * } - */ - await assertClosingSymbolsHandler( - String.raw`function shouldReturnAhtmlDiv( { name } : Props) { - if (!name) { - return undefined - } - - return ( -
- { name } -
`, - String.raw`function shouldReturnAhtmlDiv( { name } : Props) { - if (!name) { - return undefined - } - - return ( -
- { name } -
- ) -}` - ) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts deleted file mode 100644 index 2a2ad8bb34e..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - PlatformLanguageId, - extractClasses, - extractFunctions, - isTestFile, - utgLanguageConfigs, -} from 'aws-core-vscode/codewhisperer' -import assert from 'assert' -import { createTestWorkspaceFolder, toTextDocument } from 'aws-core-vscode/test' - -describe('RegexValidationForPython', () => { - it('should extract all function names from a python file content', () => { - // TODO: Replace this variable based testing to read content from File. - // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; - // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); - // const regex = /function\s+(\w+)/g; - - const result = extractFunctions(pythonFileContent, utgLanguageConfigs['python'].functionExtractionPattern) - assert.strictEqual(result.length, 13) - assert.deepStrictEqual(result, [ - 'hello_world', - 'add_numbers', - 'multiply_numbers', - 'sum_numbers', - 'divide_numbers', - '__init__', - 'add', - 'multiply', - 'square', - 'from_sum', - '__init__', - 'triple', - 'main', - ]) - }) - - it('should extract all class names from a file content', () => { - const result = extractClasses(pythonFileContent, utgLanguageConfigs['python'].classExtractionPattern) - assert.deepStrictEqual(result, ['Calculator']) - }) -}) - -describe('RegexValidationForJava', () => { - it('should extract all function names from a java file content', () => { - // TODO: Replace this variable based testing to read content from File. - // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; - // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); - // const regex = /function\s+(\w+)/g; - - const result = extractFunctions(javaFileContent, utgLanguageConfigs['java'].functionExtractionPattern) - assert.strictEqual(result.length, 5) - assert.deepStrictEqual(result, ['sayHello', 'doSomething', 'square', 'manager', 'ABCFUNCTION']) - }) - - it('should extract all class names from a java file content', () => { - const result = extractClasses(javaFileContent, utgLanguageConfigs['java'].classExtractionPattern) - assert.deepStrictEqual(result, ['Test']) - }) -}) - -describe('isTestFile', () => { - let testWsFolder: string - beforeEach(async function () { - testWsFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - it('validate by file path', async function () { - const langs = new Map([ - ['java', '.java'], - ['python', '.py'], - ['typescript', '.ts'], - ['javascript', '.js'], - ['typescriptreact', '.tsx'], - ['javascriptreact', '.jsx'], - ]) - const testFilePathsWithoutExt = [ - '/test/MyClass', - '/test/my_class', - '/tst/MyClass', - '/tst/my_class', - '/tests/MyClass', - '/tests/my_class', - ] - - const srcFilePathsWithoutExt = [ - '/src/MyClass', - 'MyClass', - 'foo/bar/MyClass', - 'foo/my_class', - 'my_class', - 'anyFolderOtherThanTest/foo/myClass', - ] - - for (const [languageId, ext] of langs) { - const testFilePaths = testFilePathsWithoutExt.map((it) => it + ext) - for (const testFilePath of testFilePaths) { - const actual = await isTestFile(testFilePath, { languageId: languageId }) - assert.strictEqual(actual, true) - } - - const srcFilePaths = srcFilePathsWithoutExt.map((it) => it + ext) - for (const srcFilePath of srcFilePaths) { - const actual = await isTestFile(srcFilePath, { languageId: languageId }) - assert.strictEqual(actual, false) - } - } - }) - - async function assertIsTestFile( - fileNames: string[], - config: { languageId: PlatformLanguageId }, - expected: boolean - ) { - for (const fileName of fileNames) { - const document = await toTextDocument('', fileName, testWsFolder) - const actual = await isTestFile(document.uri.fsPath, { languageId: config.languageId }) - assert.strictEqual(actual, expected) - } - } - - it('validate by file name', async function () { - const camelCaseSrc = ['Foo.java', 'Bar.java', 'Baz.java'] - await assertIsTestFile(camelCaseSrc, { languageId: 'java' }, false) - - const camelCaseTst = ['FooTest.java', 'BarTests.java'] - await assertIsTestFile(camelCaseTst, { languageId: 'java' }, true) - - const snakeCaseSrc = ['foo.py', 'bar.py'] - await assertIsTestFile(snakeCaseSrc, { languageId: 'python' }, false) - - const snakeCaseTst = ['test_foo.py', 'bar_test.py'] - await assertIsTestFile(snakeCaseTst, { languageId: 'python' }, true) - - const javascriptSrc = ['Foo.js', 'bar.js'] - await assertIsTestFile(javascriptSrc, { languageId: 'javascript' }, false) - - const javascriptTst = ['Foo.test.js', 'Bar.spec.js'] - await assertIsTestFile(javascriptTst, { languageId: 'javascript' }, true) - - const typescriptSrc = ['Foo.ts', 'bar.ts'] - await assertIsTestFile(typescriptSrc, { languageId: 'typescript' }, false) - - const typescriptTst = ['Foo.test.ts', 'Bar.spec.ts'] - await assertIsTestFile(typescriptTst, { languageId: 'typescript' }, true) - - const jsxSrc = ['Foo.jsx', 'Bar.jsx'] - await assertIsTestFile(jsxSrc, { languageId: 'javascriptreact' }, false) - - const jsxTst = ['Foo.test.jsx', 'Bar.spec.jsx'] - await assertIsTestFile(jsxTst, { languageId: 'javascriptreact' }, true) - }) - - it('should return true if the file name matches the test filename pattern - Java', async () => { - const filePaths = ['/path/to/MyClassTest.java', '/path/to/TestMyClass.java', '/path/to/MyClassTests.java'] - const language = 'java' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, true) - } - }) - - it('should return false if the file name does not match the test filename pattern - Java', async () => { - const filePaths = ['/path/to/MyClass.java', '/path/to/MyClass_test.java', '/path/to/test_MyClass.java'] - const language = 'java' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, false) - } - }) - - it('should return true if the file name does not match the test filename pattern - Python', async () => { - const filePaths = ['/path/to/util_test.py', '/path/to/test_util.py'] - const language = 'python' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, true) - } - }) - - it('should return false if the file name does not match the test filename pattern - Python', async () => { - const filePaths = ['/path/to/util.py', '/path/to/utilTest.java', '/path/to/Testutil.java'] - const language = 'python' - - for (const filePath of filePaths) { - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, false) - } - }) - - it('should return false if the language is not supported', async () => { - const filePath = '/path/to/MyClass.cpp' - const language = 'c++' - const result = await isTestFile(filePath, { languageId: language }) - assert.strictEqual(result, false) - }) -}) - -const pythonFileContent = ` -# Single-line import statements -import os -import numpy as np -from typing import List, Tuple - -# Multi-line import statements -from collections import ( - defaultdict, - Counter -) - -# Relative imports -from . import module1 -from ..subpackage import module2 - -# Wildcard imports -from mypackage import * -from mypackage.module import * - -# Aliased imports -import pandas as pd -from mypackage import module1 as m1, module2 as m2 - -def hello_world(): - print("Hello, world!") - -def add_numbers(x, y): - return x + y - -def multiply_numbers(x=1, y=1): - return x * y - -def sum_numbers(*args): - total = 0 - for num in args: - total += num - return total - -def divide_numbers(x, y=1, *args, **kwargs): - result = x / y - for arg in args: - result /= arg - for _, value in kwargs.items(): - result /= value - return result - -class Calculator: - def __init__(self, x, y): - self.x = x - self.y = y - - def add(self): - return self.x + self.y - - def multiply(self): - return self.x * self.y - - @staticmethod - def square(x): - return x ** 2 - - @classmethod - def from_sum(cls, x, y): - return cls(x+y, 0) - - class InnerClass: - def __init__(self, z): - self.z = z - - def triple(self): - return self.z * 3 - -def main(): - print(hello_world()) - print(add_numbers(3, 5)) - print(multiply_numbers(3, 5)) - print(sum_numbers(1, 2, 3, 4, 5)) - print(divide_numbers(10, 2, 5, 2, a=2, b=3)) - - calc = Calculator(3, 5) - print(calc.add()) - print(calc.multiply()) - print(Calculator.square(3)) - print(Calculator.from_sum(2, 3).add()) - - inner = Calculator.InnerClass(5) - print(inner.triple()) - -if __name__ == "__main__": - main() -` - -const javaFileContent = ` -@Annotation -public class Test { - Test() { - // Do something here - } - - //Additional commenting - public static void sayHello() { - System.out.println("Hello, World!"); - } - - private void doSomething(int x, int y) throws Exception { - int z = x + y; - System.out.println("The sum of " + x + " and " + y + " is " + z); - } - - protected static int square(int x) { - return x * x; - } - - private static void manager(int a, int b) { - return a+b; - } - - public int ABCFUNCTION( int ABC, int PQR) { - return ABC + PQR; - } -}` diff --git a/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts deleted file mode 100644 index 5694b33365d..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import { - JsonConfigFileNamingConvention, - checkLeftContextKeywordsForJson, - getPrefixSuffixOverlap, -} from 'aws-core-vscode/codewhisperer' - -describe('commonUtil', function () { - describe('getPrefixSuffixOverlap', function () { - it('Should return correct overlap', async function () { - assert.strictEqual(getPrefixSuffixOverlap('32rasdgvdsg', 'sg462ydfgbs'), `sg`) - assert.strictEqual(getPrefixSuffixOverlap('32rasdgbreh', 'brehsega'), `breh`) - assert.strictEqual(getPrefixSuffixOverlap('42y24hsd', '42y24hsdzqq23'), `42y24hsd`) - assert.strictEqual(getPrefixSuffixOverlap('ge23yt1', 'ge23yt1'), `ge23yt1`) - assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', 'a1sgdbsfbwsergs'), `a`) - assert.strictEqual(getPrefixSuffixOverlap('xxa', 'xa'), `xa`) - }) - - it('Should return empty overlap for prefix suffix not matching cases', async function () { - assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', '1sgdbsfbwsergs'), ``) - assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsab', '1sgdbsfbwsergs'), ``) - assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'v2135t12'), ``) - assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'zv2135t12'), ``) - assert.strictEqual(getPrefixSuffixOverlap('xa', 'xxa'), ``) - }) - - it('Should return empty overlap for empty string input', async function () { - assert.strictEqual(getPrefixSuffixOverlap('ergwsghws', ''), ``) - assert.strictEqual(getPrefixSuffixOverlap('', 'asfegw4eh'), ``) - }) - }) - - describe('checkLeftContextKeywordsForJson', function () { - it('Should return true for valid left context keywords', async function () { - assert.strictEqual( - checkLeftContextKeywordsForJson('foo.json', 'Create an S3 Bucket named CodeWhisperer', 'json'), - true - ) - }) - it('Should return false for invalid left context keywords', async function () { - assert.strictEqual( - checkLeftContextKeywordsForJson( - 'foo.json', - 'Create an S3 Bucket named CodeWhisperer in Cloudformation', - 'json' - ), - false - ) - }) - - for (const jsonConfigFile of JsonConfigFileNamingConvention) { - it(`should evalute by filename ${jsonConfigFile}`, function () { - assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile, 'foo', 'json'), false) - - assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'bar', 'json'), false) - - assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'baz', 'json'), false) - }) - - const upperCaseFilename = jsonConfigFile.toUpperCase() - it(`should evalute by filename and case insensitive ${upperCaseFilename}`, function () { - assert.strictEqual(checkLeftContextKeywordsForJson(upperCaseFilename, 'foo', 'json'), false) - - assert.strictEqual( - checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'bar', 'json'), - false - ) - - assert.strictEqual( - checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'baz', 'json'), - false - ) - }) - } - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts deleted file mode 100644 index 91e26e36111..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts +++ /dev/null @@ -1,454 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as FakeTimers from '@sinonjs/fake-timers' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as crossFile from 'aws-core-vscode/codewhisperer' -import { - aLongStringWithLineCount, - aStringWithLineCount, - createMockTextEditor, - installFakeClock, -} from 'aws-core-vscode/test' -import { FeatureConfigProvider, crossFileContextConfig } from 'aws-core-vscode/codewhisperer' -import { - assertTabCount, - closeAllEditors, - createTestWorkspaceFolder, - toTextEditor, - shuffleList, - toFile, -} from 'aws-core-vscode/test' -import { areEqual, normalize } from 'aws-core-vscode/shared' -import * as path from 'path' -import { LspController } from 'aws-core-vscode/amazonq' - -let tempFolder: string - -describe('crossFileContextUtil', function () { - const fakeCancellationToken: vscode.CancellationToken = { - isCancellationRequested: false, - onCancellationRequested: sinon.spy(), - } - - let mockEditor: vscode.TextEditor - let clock: FakeTimers.InstalledClock - - before(function () { - clock = installFakeClock() - }) - - after(function () { - clock.uninstall() - }) - - afterEach(function () { - sinon.restore() - }) - - describe('fetchSupplementalContextForSrc', function () { - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - sinon.restore() - }) - - it.skip('for control group, should return opentabs context where there will be 3 chunks and each chunk should contains 50 lines', async function () { - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') - await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) - const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { - preview: false, - }) - - await assertTabCount(2) - - const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) - assert.ok(actual) - assert.strictEqual(actual.supplementalContextItems.length, 3) - assert.strictEqual(actual.supplementalContextItems[0].content.split('\n').length, 50) - assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) - assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) - }) - - it('for t1 group, should return repomap + opentabs context, should not exceed 20k total length', async function () { - await toTextEditor(aLongStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) - const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { - preview: false, - }) - - await assertTabCount(2) - - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t1') - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'codemap') - .resolves([ - { - content: 'foo'.repeat(3000), - score: 0, - filePath: 'q-inline', - }, - ]) - - const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) - assert.ok(actual) - assert.strictEqual(actual.supplementalContextItems.length, 3) - assert.strictEqual(actual?.strategy, 'codemap') - assert.deepEqual(actual?.supplementalContextItems[0], { - content: 'foo'.repeat(3000), - score: 0, - filePath: 'q-inline', - }) - assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) - assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) - }) - - it.skip('for t2 group, should return global bm25 context and no repomap', async function () { - await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) - const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { - preview: false, - }) - - await assertTabCount(2) - - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t2') - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'bm25') - .resolves([ - { - content: 'foo', - score: 5, - filePath: 'foo.java', - }, - { - content: 'bar', - score: 4, - filePath: 'bar.java', - }, - { - content: 'baz', - score: 3, - filePath: 'baz.java', - }, - { - content: 'qux', - score: 2, - filePath: 'qux.java', - }, - { - content: 'quux', - score: 1, - filePath: 'quux.java', - }, - ]) - - const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) - assert.ok(actual) - assert.strictEqual(actual.supplementalContextItems.length, 5) - assert.strictEqual(actual?.strategy, 'bm25') - - assert.deepEqual(actual?.supplementalContextItems[0], { - content: 'foo', - score: 5, - filePath: 'foo.java', - }) - - assert.deepEqual(actual?.supplementalContextItems[1], { - content: 'bar', - score: 4, - filePath: 'bar.java', - }) - assert.deepEqual(actual?.supplementalContextItems[2], { - content: 'baz', - score: 3, - filePath: 'baz.java', - }) - - assert.deepEqual(actual?.supplementalContextItems[3], { - content: 'qux', - score: 2, - filePath: 'qux.java', - }) - - assert.deepEqual(actual?.supplementalContextItems[4], { - content: 'quux', - score: 1, - filePath: 'quux.java', - }) - }) - }) - - describe('non supported language should return undefined', function () { - it('c++', async function () { - mockEditor = createMockTextEditor('content', 'fileName', 'cpp') - const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) - assert.strictEqual(actual, undefined) - }) - - it('ruby', async function () { - mockEditor = createMockTextEditor('content', 'fileName', 'ruby') - - const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) - - assert.strictEqual(actual, undefined) - }) - }) - - describe('getCrossFileCandidate', function () { - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - await closeAllEditors() - }) - - it('should return opened files, exclude test files and sorted ascendingly by file distance', async function () { - const targetFile = path.join('src', 'service', 'microService', 'CodeWhispererFileContextProvider.java') - const fileWithDistance3 = path.join('src', 'service', 'CodewhispererRecommendationService.java') - const fileWithDistance5 = path.join('src', 'util', 'CodeWhispererConstants.java') - const fileWithDistance6 = path.join('src', 'ui', 'popup', 'CodeWhispererPopupManager.java') - const fileWithDistance7 = path.join('src', 'ui', 'popup', 'components', 'CodeWhispererPopup.java') - const fileWithDistance8 = path.join( - 'src', - 'ui', - 'popup', - 'components', - 'actions', - 'AcceptRecommendationAction.java' - ) - const testFile1 = path.join('test', 'service', 'CodeWhispererFileContextProviderTest.java') - const testFile2 = path.join('test', 'ui', 'CodeWhispererPopupManagerTest.java') - - const expectedFilePaths = [ - fileWithDistance3, - fileWithDistance5, - fileWithDistance6, - fileWithDistance7, - fileWithDistance8, - ] - - const shuffledFilePaths = shuffleList(expectedFilePaths) - - for (const filePath of shuffledFilePaths) { - await toTextEditor('', filePath, tempFolder, { preview: false }) - } - - await toTextEditor('', testFile1, tempFolder, { preview: false }) - await toTextEditor('', testFile2, tempFolder, { preview: false }) - const editor = await toTextEditor('', targetFile, tempFolder, { preview: false }) - - await assertTabCount(shuffledFilePaths.length + 3) - - const actual = await crossFile.getCrossFileCandidates(editor) - - assert.ok(actual.length === 5) - for (const [index, actualFile] of actual.entries()) { - const expectedFile = path.join(tempFolder, expectedFilePaths[index]) - assert.strictEqual(normalize(expectedFile), normalize(actualFile)) - assert.ok(areEqual(tempFolder, actualFile, expectedFile)) - } - }) - }) - - describe.skip('partial support - control group', function () { - const fileExtLists: string[] = [] - - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - await closeAllEditors() - }) - - for (const fileExt of fileExtLists) { - it('should be empty if userGroup is control', async function () { - const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) - await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) - - assert.ok(actual && actual.supplementalContextItems.length === 0) - }) - } - }) - - describe.skip('partial support - crossfile group', function () { - const fileExtLists: string[] = [] - - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - await closeAllEditors() - }) - - for (const fileExt of fileExtLists) { - it('should be non empty if usergroup is Crossfile', async function () { - const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) - await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) - - assert.ok(actual && actual.supplementalContextItems.length !== 0) - }) - } - }) - - describe('full support', function () { - const fileExtLists = ['java', 'js', 'ts', 'py', 'tsx', 'jsx'] - - before(async function () { - this.timeout(60000) - }) - - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - afterEach(async function () { - sinon.restore() - await closeAllEditors() - }) - - for (const fileExt of fileExtLists) { - it(`supplemental context for file ${fileExt} should be non empty`, async function () { - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'codemap') - .resolves([ - { - content: 'foo', - score: 0, - filePath: 'q-inline', - }, - ]) - const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) - await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) - await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) - - assert.ok(actual && actual.supplementalContextItems.length !== 0) - }) - } - }) - - describe('splitFileToChunks', function () { - beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - it('should split file to a chunk of 2 lines', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, 2) - - assert.strictEqual(chunks.length, 4) - assert.strictEqual(chunks[0].content, 'line_1\nline_2') - assert.strictEqual(chunks[1].content, 'line_3\nline_4') - assert.strictEqual(chunks[2].content, 'line_5\nline_6') - assert.strictEqual(chunks[3].content, 'line_7') - }) - - it('should split file to a chunk of 5 lines', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, 5) - - assert.strictEqual(chunks.length, 2) - assert.strictEqual(chunks[0].content, 'line_1\nline_2\nline_3\nline_4\nline_5') - assert.strictEqual(chunks[1].content, 'line_6\nline_7') - }) - - it('codewhisperer crossfile config should use 50 lines', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile(aStringWithLineCount(210), filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) - - // (210 / 50) + 1 - assert.strictEqual(chunks.length, 5) - // line0 -> line49 - assert.strictEqual(chunks[0].content, aStringWithLineCount(50, 0)) - // line50 -> line99 - assert.strictEqual(chunks[1].content, aStringWithLineCount(50, 50)) - // line100 -> line149 - assert.strictEqual(chunks[2].content, aStringWithLineCount(50, 100)) - // line150 -> line199 - assert.strictEqual(chunks[3].content, aStringWithLineCount(50, 150)) - // line 200 -> line209 - assert.strictEqual(chunks[4].content, aStringWithLineCount(10, 200)) - }) - - it('linkChunks should add another chunk which will link to the first chunk and chunk.nextContent should reflect correct value', async function () { - const filePath = path.join(tempFolder, 'file.txt') - await toFile(aStringWithLineCount(210), filePath) - - const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) - const linkedChunks = crossFile.linkChunks(chunks) - - // 210 / 50 + 2 - assert.strictEqual(linkedChunks.length, 6) - - // 0th - assert.strictEqual(linkedChunks[0].content, aStringWithLineCount(3, 0)) - assert.strictEqual(linkedChunks[0].nextContent, aStringWithLineCount(50, 0)) - - // 1st - assert.strictEqual(linkedChunks[1].content, aStringWithLineCount(50, 0)) - assert.strictEqual(linkedChunks[1].nextContent, aStringWithLineCount(50, 50)) - - // 2nd - assert.strictEqual(linkedChunks[2].content, aStringWithLineCount(50, 50)) - assert.strictEqual(linkedChunks[2].nextContent, aStringWithLineCount(50, 100)) - - // 3rd - assert.strictEqual(linkedChunks[3].content, aStringWithLineCount(50, 100)) - assert.strictEqual(linkedChunks[3].nextContent, aStringWithLineCount(50, 150)) - - // 4th - assert.strictEqual(linkedChunks[4].content, aStringWithLineCount(50, 150)) - assert.strictEqual(linkedChunks[4].nextContent, aStringWithLineCount(10, 200)) - - // 5th - assert.strictEqual(linkedChunks[5].content, aStringWithLineCount(10, 200)) - assert.strictEqual(linkedChunks[5].nextContent, aStringWithLineCount(10, 200)) - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts b/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts deleted file mode 100644 index d5085e4db0c..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import assert from 'assert' -import * as codewhispererClient from 'aws-core-vscode/codewhisperer' -import * as EditorContext from 'aws-core-vscode/codewhisperer' -import { - createMockTextEditor, - createMockClientRequest, - resetCodeWhispererGlobalVariables, - toTextEditor, - createTestWorkspaceFolder, - closeAllEditors, -} from 'aws-core-vscode/test' -import { globals } from 'aws-core-vscode/shared' -import { GenerateCompletionsRequest } from 'aws-core-vscode/codewhisperer' - -describe('editorContext', function () { - let telemetryEnabledDefault: boolean - let tempFolder: string - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - telemetryEnabledDefault = globals.telemetry.telemetryEnabled - }) - - afterEach(async function () { - await globals.telemetry.setTelemetryEnabled(telemetryEnabledDefault) - }) - - describe('extractContextForCodeWhisperer', function () { - it('Should return expected context', function () { - const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) - const actual = EditorContext.extractContextForCodeWhisperer(editor) - const expected: codewhispererClient.FileContext = { - filename: 'test.py', - programmingLanguage: { - languageName: 'python', - }, - leftFileContent: 'import math\ndef two_sum(nums,', - rightFileContent: ' target):\n', - } - assert.deepStrictEqual(actual, expected) - }) - - it('Should return expected context within max char limit', function () { - const editor = createMockTextEditor( - 'import math\ndef ' + 'a'.repeat(10340) + 'two_sum(nums, target):\n', - 'test.py', - 'python', - 1, - 17 - ) - const actual = EditorContext.extractContextForCodeWhisperer(editor) - const expected: codewhispererClient.FileContext = { - filename: 'test.py', - programmingLanguage: { - languageName: 'python', - }, - leftFileContent: 'import math\ndef aaaaaaaaaaaaa', - rightFileContent: 'a'.repeat(10240), - } - assert.deepStrictEqual(actual, expected) - }) - }) - - describe('getFileName', function () { - it('Should return expected filename given a document reading test.py', function () { - const editor = createMockTextEditor('', 'test.py', 'python', 1, 17) - const actual = EditorContext.getFileName(editor) - const expected = 'test.py' - assert.strictEqual(actual, expected) - }) - - it('Should return expected filename for a long filename', async function () { - const editor = createMockTextEditor('', 'a'.repeat(1500), 'python', 1, 17) - const actual = EditorContext.getFileName(editor) - const expected = 'a'.repeat(1024) - assert.strictEqual(actual, expected) - }) - }) - - describe('getFileRelativePath', function () { - this.beforeEach(async function () { - tempFolder = (await createTestWorkspaceFolder()).uri.fsPath - }) - - it('Should return a new filename with correct extension given a .ipynb file', function () { - const languageToExtension = new Map([ - ['python', 'py'], - ['rust', 'rs'], - ['javascript', 'js'], - ['typescript', 'ts'], - ['c', 'c'], - ]) - - for (const [language, extension] of languageToExtension.entries()) { - const editor = createMockTextEditor('', 'test.ipynb', language, 1, 17) - const actual = EditorContext.getFileRelativePath(editor) - const expected = 'test.' + extension - assert.strictEqual(actual, expected) - } - }) - - it('Should return relative path', async function () { - const editor = await toTextEditor('tttt', 'test.py', tempFolder) - const actual = EditorContext.getFileRelativePath(editor) - const expected = 'test.py' - assert.strictEqual(actual, expected) - }) - - afterEach(async function () { - await closeAllEditors() - }) - }) - - describe('validateRequest', function () { - it('Should return false if request filename.length is invalid', function () { - const req = createMockClientRequest() - req.fileContext.filename = '' - assert.ok(!EditorContext.validateRequest(req)) - }) - - it('Should return false if request programming language is invalid', function () { - const req = createMockClientRequest() - req.fileContext.programmingLanguage.languageName = '' - assert.ok(!EditorContext.validateRequest(req)) - req.fileContext.programmingLanguage.languageName = 'a'.repeat(200) - assert.ok(!EditorContext.validateRequest(req)) - }) - - it('Should return false if request left or right context exceeds max length', function () { - const req = createMockClientRequest() - req.fileContext.leftFileContent = 'a'.repeat(256000) - assert.ok(!EditorContext.validateRequest(req)) - req.fileContext.leftFileContent = 'a' - req.fileContext.rightFileContent = 'a'.repeat(256000) - assert.ok(!EditorContext.validateRequest(req)) - }) - - it('Should return true if above conditions are not met', function () { - const req = createMockClientRequest() - assert.ok(EditorContext.validateRequest(req)) - }) - }) - - describe('getLeftContext', function () { - it('Should return expected left context', function () { - const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) - const actual = EditorContext.getLeftContext(editor, 1) - const expected = '...wo_sum(nums, target)' - assert.strictEqual(actual, expected) - }) - }) - - describe('buildListRecommendationRequest', function () { - it('Should return expected fields for optOut, nextToken and reference config', async function () { - const nextToken = 'testToken' - const optOutPreference = false - await globals.telemetry.setTelemetryEnabled(false) - const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) - const actual = await EditorContext.buildListRecommendationRequest(editor, nextToken, optOutPreference) - - assert.strictEqual(actual.request.nextToken, nextToken) - assert.strictEqual((actual.request as GenerateCompletionsRequest).optOutPreference, 'OPTOUT') - assert.strictEqual(actual.request.referenceTrackerConfiguration?.recommendationsWithReferences, 'BLOCK') - }) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts deleted file mode 100644 index 24062a81b7c..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as sinon from 'sinon' -import { resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' -import { getLogger } from 'aws-core-vscode/shared' -import { resetIntelliSenseState, vsCodeState } from 'aws-core-vscode/codewhisperer' - -describe('globalStateUtil', function () { - let loggerSpy: sinon.SinonSpy - - beforeEach(async function () { - await resetCodeWhispererGlobalVariables() - vsCodeState.isIntelliSenseActive = true - loggerSpy = sinon.spy(getLogger(), 'info') - }) - - this.afterEach(function () { - sinon.restore() - }) - - it('Should skip when CodeWhisperer is turned off', async function () { - const isManualTriggerEnabled = false - const isAutomatedTriggerEnabled = false - resetIntelliSenseState(isManualTriggerEnabled, isAutomatedTriggerEnabled, true) - assert.ok(!loggerSpy.called) - }) - - it('Should skip when invocationContext is not active', async function () { - vsCodeState.isIntelliSenseActive = false - resetIntelliSenseState(false, false, true) - assert.ok(!loggerSpy.called) - }) - - it('Should skip when no valid recommendations', async function () { - resetIntelliSenseState(true, true, false) - assert.ok(!loggerSpy.called) - }) -}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts deleted file mode 100644 index a42b0aa6158..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as FakeTimers from '@sinonjs/fake-timers' -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import * as os from 'os' -import * as crossFile from 'aws-core-vscode/codewhisperer' -import { TestFolder, assertTabCount, installFakeClock } from 'aws-core-vscode/test' -import { CodeWhispererSupplementalContext, FeatureConfigProvider } from 'aws-core-vscode/codewhisperer' -import { toTextEditor } from 'aws-core-vscode/test' -import { LspController } from 'aws-core-vscode/amazonq' - -const newLine = os.EOL - -describe('supplementalContextUtil', function () { - let testFolder: TestFolder - let clock: FakeTimers.InstalledClock - - const fakeCancellationToken: vscode.CancellationToken = { - isCancellationRequested: false, - onCancellationRequested: sinon.spy(), - } - - before(function () { - clock = installFakeClock() - }) - - after(function () { - clock.uninstall() - }) - - beforeEach(async function () { - testFolder = await TestFolder.create() - sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') - }) - - afterEach(function () { - sinon.restore() - }) - - describe('fetchSupplementalContext', function () { - describe('openTabsContext', function () { - it('opentabContext should include chunks if non empty', async function () { - sinon - .stub(LspController.instance, 'queryInlineProjectContext') - .withArgs(sinon.match.any, sinon.match.any, 'codemap') - .resolves([ - { - content: 'foo', - score: 0, - filePath: 'q-inline', - }, - ]) - await toTextEditor('class Foo', 'Foo.java', testFolder.path, { preview: false }) - await toTextEditor('class Bar', 'Bar.java', testFolder.path, { preview: false }) - await toTextEditor('class Baz', 'Baz.java', testFolder.path, { preview: false }) - - const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { - preview: false, - }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) - assert.ok(actual?.supplementalContextItems.length === 4) - }) - - it('opentabsContext should filter out empty chunks', async function () { - // open 3 files as supplemental context candidate files but none of them have contents - await toTextEditor('', 'Foo.java', testFolder.path, { preview: false }) - await toTextEditor('', 'Bar.java', testFolder.path, { preview: false }) - await toTextEditor('', 'Baz.java', testFolder.path, { preview: false }) - - const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { - preview: false, - }) - - await assertTabCount(4) - - const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) - assert.ok(actual?.supplementalContextItems.length === 0) - }) - }) - }) - - describe('truncation', function () { - it('truncate context should do nothing if everything fits in constraint', function () { - const chunkA: crossFile.CodeWhispererSupplementalContextItem = { - content: 'a', - filePath: 'a.java', - score: 0, - } - const chunkB: crossFile.CodeWhispererSupplementalContextItem = { - content: 'b', - filePath: 'b.java', - score: 1, - } - const chunks = [chunkA, chunkB] - - const supplementalContext: CodeWhispererSupplementalContext = { - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: chunks, - contentsLength: 25000, - latency: 0, - strategy: 'codemap', - } - - const actual = crossFile.truncateSuppelementalContext(supplementalContext) - assert.strictEqual(actual.supplementalContextItems.length, 2) - assert.strictEqual(actual.supplementalContextItems[0].content, 'a') - assert.strictEqual(actual.supplementalContextItems[1].content, 'b') - }) - - it('truncateLineByLine should drop the last line if max length is greater than threshold', function () { - const input = - repeatString('a', 11) + - newLine + - repeatString('b', 11) + - newLine + - repeatString('c', 11) + - newLine + - repeatString('d', 11) + - newLine + - repeatString('e', 11) - - assert.ok(input.length > 50) - const actual = crossFile.truncateLineByLine(input, 50) - assert.ok(actual.length <= 50) - - const input2 = repeatString(`b${newLine}`, 10) - const actual2 = crossFile.truncateLineByLine(input2, 8) - assert.ok(actual2.length <= 8) - }) - - it('truncation context should make context length per item lte 10240 cap', function () { - const chunkA: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`a${newLine}`, 4000), - filePath: 'a.java', - score: 0, - } - const chunkB: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`b${newLine}`, 6000), - filePath: 'b.java', - score: 1, - } - const chunkC: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`c${newLine}`, 1000), - filePath: 'c.java', - score: 2, - } - const chunkD: crossFile.CodeWhispererSupplementalContextItem = { - content: repeatString(`d${newLine}`, 1500), - filePath: 'd.java', - score: 3, - } - - assert.ok( - chunkA.content.length + chunkB.content.length + chunkC.content.length + chunkD.content.length > 20480 - ) - - const supplementalContext: CodeWhispererSupplementalContext = { - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: [chunkA, chunkB, chunkC, chunkD], - contentsLength: 25000, - latency: 0, - strategy: 'codemap', - } - - const actual = crossFile.truncateSuppelementalContext(supplementalContext) - assert.strictEqual(actual.supplementalContextItems.length, 3) - assert.ok(actual.contentsLength <= 20480) - assert.strictEqual(actual.strategy, 'codemap') - }) - - it('truncate context should make context items lte 5', function () { - const chunkA: crossFile.CodeWhispererSupplementalContextItem = { - content: 'a', - filePath: 'a.java', - score: 0, - } - const chunkB: crossFile.CodeWhispererSupplementalContextItem = { - content: 'b', - filePath: 'b.java', - score: 1, - } - const chunkC: crossFile.CodeWhispererSupplementalContextItem = { - content: 'c', - filePath: 'c.java', - score: 2, - } - const chunkD: crossFile.CodeWhispererSupplementalContextItem = { - content: 'd', - filePath: 'd.java', - score: 3, - } - const chunkE: crossFile.CodeWhispererSupplementalContextItem = { - content: 'e', - filePath: 'e.java', - score: 4, - } - const chunkF: crossFile.CodeWhispererSupplementalContextItem = { - content: 'f', - filePath: 'f.java', - score: 5, - } - const chunkG: crossFile.CodeWhispererSupplementalContextItem = { - content: 'g', - filePath: 'g.java', - score: 6, - } - const chunks = [chunkA, chunkB, chunkC, chunkD, chunkE, chunkF, chunkG] - - assert.strictEqual(chunks.length, 7) - - const supplementalContext: CodeWhispererSupplementalContext = { - isUtg: false, - isProcessTimeout: false, - supplementalContextItems: chunks, - contentsLength: 25000, - latency: 0, - strategy: 'codemap', - } - - const actual = crossFile.truncateSuppelementalContext(supplementalContext) - assert.strictEqual(actual.supplementalContextItems.length, 5) - }) - - describe('truncate line by line', function () { - it('should return empty if empty string is provided', function () { - const input = '' - const actual = crossFile.truncateLineByLine(input, 50) - assert.strictEqual(actual, '') - }) - - it('should return empty if 0 max length is provided', function () { - const input = 'aaaaa' - const actual = crossFile.truncateLineByLine(input, 0) - assert.strictEqual(actual, '') - }) - - it('should flip the value if negative max length is provided', function () { - const input = `aaaaa${newLine}bbbbb` - const actual = crossFile.truncateLineByLine(input, -6) - const expected = crossFile.truncateLineByLine(input, 6) - assert.strictEqual(actual, expected) - assert.strictEqual(actual, 'aaaaa') - }) - }) - }) -}) - -function repeatString(s: string, n: number): string { - let output = '' - for (let i = 0; i < n; i++) { - output += s - } - - return output -} diff --git a/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts b/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts deleted file mode 100644 index 67359b8a6fc..00000000000 --- a/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import * as utgUtils from 'aws-core-vscode/codewhisperer' - -describe('shouldFetchUtgContext', () => { - it('fully supported language', function () { - assert.ok(utgUtils.shouldFetchUtgContext('java')) - }) - - it('partially supported language', () => { - assert.strictEqual(utgUtils.shouldFetchUtgContext('python'), false) - }) - - it('not supported language', () => { - assert.strictEqual(utgUtils.shouldFetchUtgContext('typescript'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('javascript'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('javascriptreact'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('typescriptreact'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('scala'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('shellscript'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('csharp'), undefined) - - assert.strictEqual(utgUtils.shouldFetchUtgContext('c'), undefined) - }) -}) - -describe('guessSrcFileName', function () { - it('should return undefined if no matching regex', function () { - assert.strictEqual(utgUtils.guessSrcFileName('Foo.java', 'java'), undefined) - assert.strictEqual(utgUtils.guessSrcFileName('folder1/foo.py', 'python'), undefined) - assert.strictEqual(utgUtils.guessSrcFileName('Bar.js', 'javascript'), undefined) - }) - - it('java', function () { - assert.strictEqual(utgUtils.guessSrcFileName('FooTest.java', 'java'), 'Foo.java') - assert.strictEqual(utgUtils.guessSrcFileName('FooTests.java', 'java'), 'Foo.java') - }) - - it('python', function () { - assert.strictEqual(utgUtils.guessSrcFileName('test_foo.py', 'python'), 'foo.py') - assert.strictEqual(utgUtils.guessSrcFileName('foo_test.py', 'python'), 'foo.py') - }) - - it('typescript', function () { - assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.ts', 'typescript'), 'Foo.ts') - assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.ts', 'typescript'), 'Foo.ts') - }) - - it('javascript', function () { - assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.js', 'javascript'), 'Foo.js') - assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.js', 'javascript'), 'Foo.js') - }) -}) diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index ec49efcedaa..83875b9fe58 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -5,8 +5,6 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' -import { getTabSizeSetting } from '../shared/utilities/editorUtilities' -import * as EditorContext from './util/editorContext' import * as CodeWhispererConstants from './models/constants' import { CodeSuggestionsState, @@ -143,10 +141,6 @@ export async function activate(context: ExtContext): Promise { * Configuration change */ vscode.workspace.onDidChangeConfiguration(async (configurationChangeEvent) => { - if (configurationChangeEvent.affectsConfiguration('editor.tabSize')) { - EditorContext.updateTabSize(getTabSizeSetting()) - } - if (configurationChangeEvent.affectsConfiguration('amazonQ.showCodeWithReferences')) { ReferenceLogViewProvider.instance.update() if (auth.isEnterpriseSsoInUse()) { diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 12c6783526f..05813444bd3 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -36,7 +36,7 @@ export { codeWhispererClient, } from './client/codewhisperer' export { listCodeWhispererCommands, listCodeWhispererCommandsId } from './ui/statusBarMenu' -export { refreshStatusBar, CodeWhispererStatusBar, InlineCompletionService } from './service/inlineCompletionService' +export { refreshStatusBar, CodeWhispererStatusBar } from './service/inlineCompletionService' export { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' export { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' export { @@ -53,34 +53,22 @@ export { stopTransformByQ } from './commands/startTransformByQ' export { featureDefinitions, FeatureConfigProvider } from '../shared/featureConfig' export { ReferenceInlineProvider } from './service/referenceInlineProvider' export { ReferenceHoverProvider } from './service/referenceHoverProvider' -export { CWInlineCompletionItemProvider } from './service/inlineCompletionItemProvider' -export { ClassifierTrigger } from './service/classifierTrigger' 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' -export { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' -export { BM25Okapi } from './util/supplementalContext/rankBm25' -export { handleExtraBrackets } from './util/closingBracketUtil' export { runtimeLanguageContext, RuntimeLanguageContext } from './util/runtimeLanguageContext' export * as startSecurityScan from './commands/startSecurityScan' -export * from './util/supplementalContext/utgUtils' -export * from './util/supplementalContext/crossFileContextUtil' -export * from './util/editorContext' export * from './util/showSsoPrompt' export * from './util/securityScanLanguageContext' export * from './util/importAdderUtil' -export * from './util/globalStateUtil' export * from './util/zipUtil' export * from './util/diagnosticsUtil' export * from './util/commonUtil' -export * from './util/supplementalContext/codeParsingUtil' -export * from './util/supplementalContext/supplementalContextUtil' export * from './util/codewhispererSettings' -export * as supplementalContextUtil from './util/supplementalContext/supplementalContextUtil' export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' export * from './ui/codeWhispererNodes' diff --git a/packages/core/src/codewhisperer/service/classifierTrigger.ts b/packages/core/src/codewhisperer/service/classifierTrigger.ts deleted file mode 100644 index 842d5312e68..00000000000 --- a/packages/core/src/codewhisperer/service/classifierTrigger.ts +++ /dev/null @@ -1,609 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import os from 'os' -import * as vscode from 'vscode' -import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' -import { extractContextForCodeWhisperer } from '../util/editorContext' -import { TelemetryHelper } from '../util/telemetryHelper' -import { ProgrammingLanguage } from '../client/codewhispereruserclient' - -interface normalizedCoefficients { - readonly lineNum: number - readonly lenLeftCur: number - readonly lenLeftPrev: number - readonly lenRight: number -} -/* - uses ML classifier to determine if user input should trigger CWSPR service - */ -export class ClassifierTrigger { - static #instance: ClassifierTrigger - - public static get instance() { - return (this.#instance ??= new this()) - } - - // ML classifier trigger threshold - private triggerThreshold = 0.43 - - // ML classifier coefficients - // os coefficient - private osCoefficientMap: Readonly> = { - 'Mac OS X': -0.1552, - 'Windows 10': -0.0238, - Windows: 0.0412, - win32: -0.0559, - } - - // trigger type coefficient - private triggerTypeCoefficientMap: Readonly> = { - SpecialCharacters: 0.0209, - Enter: 0.2853, - } - - private languageCoefficientMap: Readonly> = { - java: -0.4622, - javascript: -0.4688, - python: -0.3052, - typescript: -0.6084, - tsx: -0.6084, - jsx: -0.4688, - shell: -0.4718, - ruby: -0.7356, - sql: -0.4937, - rust: -0.4309, - kotlin: -0.4739, - php: -0.3917, - csharp: -0.3475, - go: -0.3504, - scala: -0.534, - cpp: -0.1734, - json: 0, - yaml: -0.3, - tf: -0.55, - } - - // other metadata coefficient - private lineNumCoefficient = -0.0416 - private lengthOfLeftCurrentCoefficient = -1.1747 - private lengthOfLeftPrevCoefficient = 0.4033 - private lengthOfRightCoefficient = -0.3321 - private prevDecisionAcceptCoefficient = 0.5397 - private prevDecisionRejectCoefficient = -0.1656 - private prevDecisionOtherCoefficient = 0 - private ideVscode = -0.1905 - private lengthLeft0To5 = -0.8756 - private lengthLeft5To10 = -0.5463 - private lengthLeft10To20 = -0.4081 - private lengthLeft20To30 = -0.3272 - private lengthLeft30To40 = -0.2442 - private lengthLeft40To50 = -0.1471 - - // intercept of logistic regression classifier - private intercept = 0.3738713 - - private maxx: normalizedCoefficients = { - lineNum: 4631.0, - lenLeftCur: 157.0, - lenLeftPrev: 176.0, - lenRight: 10239.0, - } - - private minn: normalizedCoefficients = { - lineNum: 0.0, - lenLeftCur: 0.0, - lenLeftPrev: 0.0, - lenRight: 0.0, - } - - // character and keywords coefficient - private charCoefficient: Readonly> = { - throw: 1.5868, - ';': -1.268, - any: -1.1565, - '7': -1.1347, - false: -1.1307, - nil: -1.0653, - elif: 1.0122, - '9': -1.0098, - pass: -1.0058, - True: -1.0002, - False: -0.9434, - '6': -0.9222, - true: -0.9142, - None: -0.9027, - '8': -0.9013, - break: -0.8475, - '}': -0.847, - '5': -0.8414, - '4': -0.8197, - '1': -0.8085, - '\\': -0.8019, - static: -0.7748, - '0': -0.77, - end: -0.7617, - '(': 0.7239, - '/': -0.7104, - where: -0.6981, - readonly: -0.6741, - async: -0.6723, - '3': -0.654, - continue: -0.6413, - struct: -0.64, - try: -0.6369, - float: -0.6341, - using: 0.6079, - '@': 0.6016, - '|': 0.5993, - impl: 0.5808, - private: -0.5746, - for: 0.5741, - '2': -0.5634, - let: -0.5187, - foreach: 0.5186, - select: -0.5148, - export: -0.5, - mut: -0.4921, - ')': -0.463, - ']': -0.4611, - when: 0.4602, - virtual: -0.4583, - extern: -0.4465, - catch: 0.4446, - new: 0.4394, - val: -0.4339, - map: 0.4284, - case: 0.4271, - throws: 0.4221, - null: -0.4197, - protected: -0.4133, - q: 0.4125, - except: 0.4115, - ': ': 0.4072, - '^': -0.407, - ' ': 0.4066, - $: 0.3981, - this: 0.3962, - switch: 0.3947, - '*': -0.3931, - module: 0.3912, - array: 0.385, - '=': 0.3828, - p: 0.3728, - ON: 0.3708, - '`': 0.3693, - u: 0.3658, - a: 0.3654, - require: 0.3646, - '>': -0.3644, - const: -0.3476, - o: 0.3423, - sizeof: 0.3416, - object: 0.3362, - w: 0.3345, - print: 0.3344, - range: 0.3336, - if: 0.3324, - abstract: -0.3293, - var: -0.3239, - i: 0.321, - while: 0.3138, - J: 0.3137, - c: 0.3118, - await: -0.3072, - from: 0.3057, - f: 0.302, - echo: 0.2995, - '#': 0.2984, - e: 0.2962, - r: 0.2925, - mod: 0.2893, - loop: 0.2874, - t: 0.2832, - '~': 0.282, - final: -0.2816, - del: 0.2785, - override: -0.2746, - ref: -0.2737, - h: 0.2693, - m: 0.2681, - '{': 0.2674, - implements: 0.2672, - inline: -0.2642, - match: 0.2613, - with: -0.261, - x: 0.2597, - namespace: -0.2596, - operator: 0.2573, - double: -0.2563, - source: -0.2482, - import: -0.2419, - NULL: -0.2399, - l: 0.239, - or: 0.2378, - s: 0.2366, - then: 0.2354, - W: 0.2354, - y: 0.2333, - local: 0.2288, - is: 0.2282, - n: 0.2254, - '+': -0.2251, - G: 0.223, - public: -0.2229, - WHERE: 0.2224, - list: 0.2204, - Q: 0.2204, - '[': 0.2136, - VALUES: 0.2134, - H: 0.2105, - g: 0.2094, - else: -0.208, - bool: -0.2066, - long: -0.2059, - R: 0.2025, - S: 0.2021, - d: 0.2003, - V: 0.1974, - K: -0.1961, - '<': 0.1958, - debugger: -0.1929, - NOT: -0.1911, - b: 0.1907, - boolean: -0.1891, - z: -0.1866, - LIKE: -0.1793, - raise: 0.1782, - L: 0.1768, - fn: 0.176, - delete: 0.1714, - unsigned: -0.1675, - auto: -0.1648, - finally: 0.1616, - k: 0.1599, - as: 0.156, - instanceof: 0.1558, - '&': 0.1554, - E: 0.1551, - M: 0.1542, - I: 0.1503, - Y: 0.1493, - typeof: 0.1475, - j: 0.1445, - INTO: 0.1442, - IF: 0.1437, - next: 0.1433, - undef: -0.1427, - THEN: -0.1416, - v: 0.1415, - C: 0.1383, - P: 0.1353, - AND: -0.1345, - constructor: 0.1337, - void: -0.1336, - class: -0.1328, - defer: 0.1316, - begin: 0.1306, - FROM: -0.1304, - SET: 0.1291, - decimal: -0.1278, - friend: 0.1277, - SELECT: -0.1265, - event: 0.1259, - lambda: 0.1253, - enum: 0.1215, - A: 0.121, - lock: 0.1187, - ensure: 0.1184, - '%': 0.1177, - isset: 0.1175, - O: 0.1174, - '.': 0.1146, - UNION: -0.1145, - alias: -0.1129, - template: -0.1102, - WHEN: 0.1093, - rescue: 0.1083, - DISTINCT: -0.1074, - trait: -0.1073, - D: 0.1062, - in: 0.1045, - internal: -0.1029, - ',': 0.1027, - static_cast: 0.1016, - do: -0.1005, - OR: 0.1003, - AS: -0.1001, - interface: 0.0996, - super: 0.0989, - B: 0.0963, - U: 0.0962, - T: 0.0943, - CALL: -0.0918, - BETWEEN: -0.0915, - N: 0.0897, - yield: 0.0867, - done: -0.0857, - string: -0.0837, - out: -0.0831, - volatile: -0.0819, - retry: 0.0816, - '?': -0.0796, - number: -0.0791, - short: 0.0787, - sealed: -0.0776, - package: 0.0765, - OPEN: -0.0756, - base: 0.0735, - and: 0.0729, - exit: 0.0726, - _: 0.0721, - keyof: -0.072, - def: 0.0713, - crate: -0.0706, - '-': -0.07, - FUNCTION: 0.0692, - declare: -0.0678, - include: 0.0671, - COUNT: -0.0669, - INDEX: -0.0666, - CLOSE: -0.0651, - fi: -0.0644, - uint: 0.0624, - params: 0.0575, - HAVING: 0.0575, - byte: -0.0575, - clone: -0.0552, - char: -0.054, - func: 0.0538, - never: -0.053, - unset: -0.0524, - unless: -0.051, - esac: -0.0509, - shift: -0.0507, - require_once: 0.0486, - ELSE: -0.0477, - extends: 0.0461, - elseif: 0.0452, - mutable: -0.0451, - asm: 0.0449, - '!': 0.0446, - LIMIT: 0.0444, - ushort: -0.0438, - '"': -0.0433, - Z: 0.0431, - exec: -0.0431, - IS: -0.0429, - DECLARE: -0.0425, - __LINE__: -0.0424, - BEGIN: -0.0418, - typedef: 0.0414, - EXIT: -0.0412, - "'": 0.041, - function: -0.0393, - dyn: -0.039, - wchar_t: -0.0388, - unique: -0.0383, - include_once: 0.0367, - stackalloc: 0.0359, - RETURN: -0.0356, - const_cast: 0.035, - MAX: 0.0341, - assert: -0.0331, - JOIN: -0.0328, - use: 0.0318, - GET: 0.0317, - VIEW: 0.0314, - move: 0.0308, - typename: 0.0308, - die: 0.0305, - asserts: -0.0304, - reinterpret_cast: -0.0302, - USING: -0.0289, - elsif: -0.0285, - FIRST: -0.028, - self: -0.0278, - RETURNING: -0.0278, - symbol: -0.0273, - OFFSET: 0.0263, - bigint: 0.0253, - register: -0.0237, - union: -0.0227, - return: -0.0227, - until: -0.0224, - endfor: -0.0213, - implicit: -0.021, - LOOP: 0.0195, - pub: 0.0182, - global: 0.0179, - EXCEPTION: 0.0175, - delegate: 0.0173, - signed: -0.0163, - FOR: 0.0156, - unsafe: 0.014, - NEXT: -0.0133, - IN: 0.0129, - MIN: -0.0123, - go: -0.0112, - type: -0.0109, - explicit: -0.0107, - eval: -0.0104, - int: -0.0099, - CASE: -0.0096, - END: 0.0084, - UPDATE: 0.0074, - default: 0.0072, - chan: 0.0068, - fixed: 0.0066, - not: -0.0052, - X: -0.0047, - endforeach: 0.0031, - goto: 0.0028, - empty: 0.0022, - checked: 0.0012, - F: -0.001, - } - - public getThreshold() { - return this.triggerThreshold - } - - public recordClassifierResultForManualTrigger(editor: vscode.TextEditor) { - this.shouldTriggerFromClassifier(undefined, editor, undefined, true) - } - - public recordClassifierResultForAutoTrigger( - editor: vscode.TextEditor, - triggerType?: CodewhispererAutomatedTriggerType, - event?: vscode.TextDocumentChangeEvent - ) { - if (!triggerType) { - return - } - this.shouldTriggerFromClassifier(event, editor, triggerType, true) - } - - public shouldTriggerFromClassifier( - event: vscode.TextDocumentChangeEvent | undefined, - editor: vscode.TextEditor, - autoTriggerType: string | undefined, - shouldRecordResult: boolean = false - ): boolean { - const fileContext = extractContextForCodeWhisperer(editor) - const osPlatform = this.normalizeOsName(os.platform(), os.version()) - const char = event ? event.contentChanges[0].text : '' - const lineNum = editor.selection.active.line - const classifierResult = this.getClassifierResult( - fileContext.leftFileContent, - fileContext.rightFileContent, - osPlatform, - autoTriggerType, - char, - lineNum, - fileContext.programmingLanguage - ) - - const threshold = this.getThreshold() - - const shouldTrigger = classifierResult > threshold - if (shouldRecordResult) { - TelemetryHelper.instance.setClassifierResult(classifierResult) - TelemetryHelper.instance.setClassifierThreshold(threshold) - } - return shouldTrigger - } - - private getClassifierResult( - leftContext: string, - rightContext: string, - os: string, - triggerType: string | undefined, - char: string, - lineNum: number, - language: ProgrammingLanguage - ): number { - const leftContextLines = leftContext.split(/\r?\n/) - const leftContextAtCurrentLine = leftContextLines[leftContextLines.length - 1] - const tokens = leftContextAtCurrentLine.trim().split(' ') - let keyword = '' - const lastToken = tokens[tokens.length - 1] - if (lastToken && lastToken.length > 1) { - keyword = lastToken - } - const lengthOfLeftCurrent = leftContextLines[leftContextLines.length - 1].length - const lengthOfLeftPrev = leftContextLines[leftContextLines.length - 2]?.length ?? 0 - const lengthOfRight = rightContext.trim().length - - const triggerTypeCoefficient: number = this.triggerTypeCoefficientMap[triggerType || ''] ?? 0 - const osCoefficient: number = this.osCoefficientMap[os] ?? 0 - const charCoefficient: number = this.charCoefficient[char] ?? 0 - const keyWordCoefficient: number = this.charCoefficient[keyword] ?? 0 - const ideCoefficient = this.ideVscode - - const previousDecision = TelemetryHelper.instance.getLastTriggerDecisionForClassifier() - const languageCoefficients = Object.values(this.languageCoefficientMap) - const avrgCoefficient = - languageCoefficients.length > 0 - ? languageCoefficients.reduce((a, b) => a + b) / languageCoefficients.length - : 0 - const languageCoefficient = this.languageCoefficientMap[language.languageName] ?? avrgCoefficient - - let previousDecisionCoefficient = 0 - if (previousDecision === 'Accept') { - previousDecisionCoefficient = this.prevDecisionAcceptCoefficient - } else if (previousDecision === 'Reject') { - previousDecisionCoefficient = this.prevDecisionRejectCoefficient - } else if (previousDecision === 'Discard' || previousDecision === 'Empty') { - previousDecisionCoefficient = this.prevDecisionOtherCoefficient - } - - let leftContextLengthCoefficient = 0 - if (leftContext.length >= 0 && leftContext.length < 5) { - leftContextLengthCoefficient = this.lengthLeft0To5 - } else if (leftContext.length >= 5 && leftContext.length < 10) { - leftContextLengthCoefficient = this.lengthLeft5To10 - } else if (leftContext.length >= 10 && leftContext.length < 20) { - leftContextLengthCoefficient = this.lengthLeft10To20 - } else if (leftContext.length >= 20 && leftContext.length < 30) { - leftContextLengthCoefficient = this.lengthLeft20To30 - } else if (leftContext.length >= 30 && leftContext.length < 40) { - leftContextLengthCoefficient = this.lengthLeft30To40 - } else if (leftContext.length >= 40 && leftContext.length < 50) { - leftContextLengthCoefficient = this.lengthLeft40To50 - } - - const result = - (this.lengthOfRightCoefficient * (lengthOfRight - this.minn.lenRight)) / - (this.maxx.lenRight - this.minn.lenRight) + - (this.lengthOfLeftCurrentCoefficient * (lengthOfLeftCurrent - this.minn.lenLeftCur)) / - (this.maxx.lenLeftCur - this.minn.lenLeftCur) + - (this.lengthOfLeftPrevCoefficient * (lengthOfLeftPrev - this.minn.lenLeftPrev)) / - (this.maxx.lenLeftPrev - this.minn.lenLeftPrev) + - (this.lineNumCoefficient * (lineNum - this.minn.lineNum)) / (this.maxx.lineNum - this.minn.lineNum) + - osCoefficient + - triggerTypeCoefficient + - charCoefficient + - keyWordCoefficient + - ideCoefficient + - this.intercept + - previousDecisionCoefficient + - languageCoefficient + - leftContextLengthCoefficient - - return sigmoid(result) - } - - private normalizeOsName(name: string, version: string | undefined): string { - const lowercaseName = name.toLowerCase() - if (lowercaseName.includes('windows')) { - if (!version) { - return 'Windows' - } else if (version.includes('Windows NT 10') || version.startsWith('10')) { - return 'Windows 10' - } else if (version.includes('6.1')) { - return 'Windows 7' - } else if (version.includes('6.3')) { - return 'Windows 8.1' - } else { - return 'Windows' - } - } else if ( - lowercaseName.includes('macos') || - lowercaseName.includes('mac os') || - lowercaseName.includes('darwin') - ) { - return 'Mac OS X' - } else if (lowercaseName.includes('linux')) { - return 'Linux' - } else { - return name - } - } -} - -const sigmoid = (x: number) => { - return 1 / (1 + Math.exp(-x)) -} diff --git a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts deleted file mode 100644 index a6c424c321d..00000000000 --- a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts +++ /dev/null @@ -1,194 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import vscode, { Position } from 'vscode' -import { getPrefixSuffixOverlap } from '../util/commonUtil' -import { Recommendation } from '../client/codewhisperer' -import { session } from '../util/codeWhispererSession' -import { TelemetryHelper } from '../util/telemetryHelper' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { ReferenceInlineProvider } from './referenceInlineProvider' -import { ImportAdderProvider } from './importAdderProvider' -import { application } from '../util/codeWhispererApplication' -import path from 'path' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' - -export class CWInlineCompletionItemProvider implements vscode.InlineCompletionItemProvider { - private activeItemIndex: number | undefined - private nextMove: number - private recommendations: Recommendation[] - private requestId: string - private startPos: Position - private nextToken: string - - private _onDidShow: vscode.EventEmitter = new vscode.EventEmitter() - public readonly onDidShow: vscode.Event = this._onDidShow.event - - public constructor( - itemIndex: number | undefined, - firstMove: number, - recommendations: Recommendation[], - requestId: string, - startPos: Position, - nextToken: string - ) { - this.activeItemIndex = itemIndex - this.nextMove = firstMove - this.recommendations = recommendations - this.requestId = requestId - this.startPos = startPos - this.nextToken = nextToken - } - - get getActiveItemIndex() { - return this.activeItemIndex - } - - public clearActiveItemIndex() { - this.activeItemIndex = undefined - } - - // iterate suggestions and stop at index 0 or index len - 1 - private getIteratingIndexes() { - const len = this.recommendations.length - const startIndex = this.activeItemIndex ? this.activeItemIndex : 0 - const index = [] - if (this.nextMove === 0) { - for (let i = 0; i < len; i++) { - index.push((startIndex + i) % len) - } - } else if (this.nextMove === -1) { - for (let i = startIndex - 1; i >= 0; i--) { - index.push(i) - } - index.push(startIndex) - } else { - for (let i = startIndex + 1; i < len; i++) { - index.push(i) - } - index.push(startIndex) - } - return index - } - - truncateOverlapWithRightContext(document: vscode.TextDocument, suggestion: string, pos: vscode.Position): string { - const trimmedSuggestion = suggestion.trim() - // limit of 5000 for right context matching - const rightContext = document.getText(new vscode.Range(pos, document.positionAt(document.offsetAt(pos) + 5000))) - const overlap = getPrefixSuffixOverlap(trimmedSuggestion, rightContext) - const overlapIndex = suggestion.lastIndexOf(overlap) - if (overlapIndex >= 0) { - const truncated = suggestion.slice(0, overlapIndex) - return truncated.trim().length ? truncated : '' - } else { - return suggestion - } - } - - getInlineCompletionItem( - document: vscode.TextDocument, - r: Recommendation, - start: vscode.Position, - end: vscode.Position, - index: number, - prefix: string - ): vscode.InlineCompletionItem | undefined { - if (!r.content.startsWith(prefix)) { - return undefined - } - const effectiveStart = document.positionAt(document.offsetAt(start) + prefix.length) - const truncatedSuggestion = this.truncateOverlapWithRightContext(document, r.content, end) - if (truncatedSuggestion.length === 0) { - if (session.getSuggestionState(index) !== 'Showed') { - session.setSuggestionState(index, 'Discard') - } - return undefined - } - TelemetryHelper.instance.lastSuggestionInDisplay = truncatedSuggestion - return { - insertText: truncatedSuggestion, - range: new vscode.Range(start, end), - command: { - command: 'aws.amazonq.accept', - title: 'On acceptance', - arguments: [ - new vscode.Range(start, end), - new vscode.Range(effectiveStart, end), - index, - truncatedSuggestion, - this.requestId, - session.sessionId, - session.triggerType, - session.getCompletionType(index), - runtimeLanguageContext.getLanguageContext(document.languageId, path.extname(document.fileName)) - .language, - r.references, - ], - }, - } - } - - // the returned completion items will always only contain one valid item - // this is to trace the current index of visible completion item - // so that reference tracker can show - // This hack can be removed once inlineCompletionAdditions API becomes public - provideInlineCompletionItems( - document: vscode.TextDocument, - position: vscode.Position, - _context: vscode.InlineCompletionContext, - _token: vscode.CancellationToken - ): vscode.ProviderResult { - if (position.line < 0 || position.isBefore(this.startPos)) { - application()._clearCodeWhispererUIListener.fire() - this.activeItemIndex = undefined - return - } - - // There's a chance that the startPos is no longer valid in the current document (e.g. - // when CodeWhisperer got triggered by 'Enter', the original startPos is with indentation - // but then this indentation got removed by VSCode when another new line is inserted, - // before the code reaches here). In such case, we need to update the startPos to be a - // valid one. Otherwise, inline completion which utilizes this position will function - // improperly. - const start = document.validatePosition(this.startPos) - const end = position - const iteratingIndexes = this.getIteratingIndexes() - const prefix = document.getText(new vscode.Range(start, end)).replace(/\r\n/g, '\n') - const matchedCount = session.recommendations.filter( - (r) => r.content.length > 0 && r.content.startsWith(prefix) && r.content !== prefix - ).length - for (const i of iteratingIndexes) { - const r = session.recommendations[i] - const item = this.getInlineCompletionItem(document, r, start, end, i, prefix) - if (item === undefined) { - continue - } - this.activeItemIndex = i - session.setSuggestionState(i, 'Showed') - ReferenceInlineProvider.instance.setInlineReference(this.startPos.line, r.content, r.references) - ImportAdderProvider.instance.onShowRecommendation(document, this.startPos.line, r) - this.nextMove = 0 - TelemetryHelper.instance.setFirstSuggestionShowTime() - session.setPerceivedLatency() - UserWrittenCodeTracker.instance.onQStartsMakingEdits() - this._onDidShow.fire() - if (matchedCount >= 2 || this.nextToken !== '') { - const result = [item] - for (let j = 0; j < matchedCount - 1; j++) { - result.push({ - insertText: `${ - typeof item.insertText === 'string' ? item.insertText : item.insertText.value - }${j}`, - range: item.range, - }) - } - return result - } - return [item] - } - application()._clearCodeWhispererUIListener.fire() - this.activeItemIndex = undefined - return [] - } -} diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts deleted file mode 100644 index 0989f022245..00000000000 --- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts +++ /dev/null @@ -1,319 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { getLogger } from '../../shared/logger/logger' -import * as CodeWhispererConstants from '../models/constants' -import globals from '../../shared/extensionGlobals' -import { vsCodeState } from '../models/model' -import { CodewhispererLanguage, telemetry } from '../../shared/telemetry/telemetry' -import { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { TelemetryHelper } from '../util/telemetryHelper' -import { AuthUtil } from '../util/authUtil' -import { getSelectedCustomization } from '../util/customizationUtil' -import { codeWhispererClient as client } from '../client/codewhisperer' -import { isAwsError } from '../../shared/errors' -import { getUnmodifiedAcceptedTokens } from '../util/commonUtil' - -interface CodeWhispererToken { - range: vscode.Range - text: string - accepted: number -} - -const autoClosingKeystrokeInputs = ['[]', '{}', '()', '""', "''"] - -/** - * This singleton class is mainly used for calculating the code written by codeWhisperer - * TODO: Remove this tracker, uses user written code tracker instead. - * This is kept in codebase for server side backward compatibility until service fully switch to user written code - */ -export class CodeWhispererCodeCoverageTracker { - private _acceptedTokens: { [key: string]: CodeWhispererToken[] } - private _totalTokens: { [key: string]: number } - private _timer?: NodeJS.Timer - private _startTime: number - private _language: CodewhispererLanguage - private _serviceInvocationCount: number - - private constructor(language: CodewhispererLanguage) { - this._acceptedTokens = {} - this._totalTokens = {} - this._startTime = 0 - this._language = language - this._serviceInvocationCount = 0 - } - - public get serviceInvocationCount(): number { - return this._serviceInvocationCount - } - - public get acceptedTokens(): { [key: string]: CodeWhispererToken[] } { - return this._acceptedTokens - } - - public get totalTokens(): { [key: string]: number } { - return this._totalTokens - } - - public isActive(): boolean { - return TelemetryHelper.instance.isTelemetryEnabled() && AuthUtil.instance.isConnected() - } - - public incrementServiceInvocationCount() { - this._serviceInvocationCount += 1 - } - - public flush() { - if (!this.isActive()) { - this._totalTokens = {} - this._acceptedTokens = {} - this.closeTimer() - return - } - try { - this.emitCodeWhispererCodeContribution() - } catch (error) { - getLogger().error(`Encountered ${error} when emitting code contribution metric`) - } - } - - // TODO: Improve the range tracking of the accepted recommendation - // TODO: use the editor of the filename, not the current editor - public updateAcceptedTokensCount(editor: vscode.TextEditor) { - const filename = editor.document.fileName - if (filename in this._acceptedTokens) { - for (let i = 0; i < this._acceptedTokens[filename].length; i++) { - const oldText = this._acceptedTokens[filename][i].text - const newText = editor.document.getText(this._acceptedTokens[filename][i].range) - this._acceptedTokens[filename][i].accepted = getUnmodifiedAcceptedTokens(oldText, newText) - } - } - } - - public emitCodeWhispererCodeContribution() { - let totalTokens = 0 - for (const filename in this._totalTokens) { - totalTokens += this._totalTokens[filename] - } - if (vscode.window.activeTextEditor) { - this.updateAcceptedTokensCount(vscode.window.activeTextEditor) - } - // the accepted characters without counting user modification - let acceptedTokens = 0 - // the accepted characters after calculating user modification - let unmodifiedAcceptedTokens = 0 - for (const filename in this._acceptedTokens) { - for (const v of this._acceptedTokens[filename]) { - if (filename in this._totalTokens && this._totalTokens[filename] >= v.accepted) { - unmodifiedAcceptedTokens += v.accepted - acceptedTokens += v.text.length - } - } - } - const percentCount = ((acceptedTokens / totalTokens) * 100).toFixed(2) - const percentage = Math.round(parseInt(percentCount)) - const selectedCustomization = getSelectedCustomization() - if (this._serviceInvocationCount <= 0) { - getLogger().debug(`Skip emiting code contribution metric`) - return - } - telemetry.codewhisperer_codePercentage.emit({ - codewhispererTotalTokens: totalTokens, - codewhispererLanguage: this._language, - codewhispererAcceptedTokens: unmodifiedAcceptedTokens, - codewhispererSuggestedTokens: acceptedTokens, - codewhispererPercentage: percentage ? percentage : 0, - successCount: this._serviceInvocationCount, - codewhispererCustomizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, - credentialStartUrl: AuthUtil.instance.startUrl, - }) - - client - .sendTelemetryEvent({ - telemetryEvent: { - codeCoverageEvent: { - customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, - programmingLanguage: { - languageName: runtimeLanguageContext.toRuntimeLanguage(this._language), - }, - acceptedCharacterCount: acceptedTokens, - unmodifiedAcceptedCharacterCount: unmodifiedAcceptedTokens, - totalCharacterCount: totalTokens, - timestamp: new Date(Date.now()), - }, - }, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) - .then() - .catch((error) => { - let requestId: string | undefined - if (isAwsError(error)) { - requestId = error.requestId - } - - getLogger().debug( - `Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${ - error.message - }` - ) - }) - } - - private tryStartTimer() { - if (this._timer !== undefined) { - return - } - const currentDate = new globals.clock.Date() - this._startTime = currentDate.getTime() - this._timer = setTimeout(() => { - try { - const currentTime = new globals.clock.Date().getTime() - const delay: number = CodeWhispererConstants.defaultCheckPeriodMillis - const diffTime: number = this._startTime + delay - if (diffTime <= currentTime) { - let totalTokens = 0 - for (const filename in this._totalTokens) { - totalTokens += this._totalTokens[filename] - } - if (totalTokens > 0) { - this.flush() - } else { - getLogger().debug( - `CodeWhispererCodeCoverageTracker: skipped telemetry due to empty tokens array` - ) - } - } - } catch (e) { - getLogger().verbose(`Exception Thrown from CodeWhispererCodeCoverageTracker: ${e}`) - } finally { - this.resetTracker() - this.closeTimer() - } - }, CodeWhispererConstants.defaultCheckPeriodMillis) - } - - private resetTracker() { - this._totalTokens = {} - this._acceptedTokens = {} - this._startTime = 0 - this._serviceInvocationCount = 0 - } - - private closeTimer() { - if (this._timer !== undefined) { - clearTimeout(this._timer) - this._timer = undefined - } - } - - public addAcceptedTokens(filename: string, token: CodeWhispererToken) { - if (!(filename in this._acceptedTokens)) { - this._acceptedTokens[filename] = [] - } - this._acceptedTokens[filename].push(token) - } - - public addTotalTokens(filename: string, count: number) { - if (!(filename in this._totalTokens)) { - this._totalTokens[filename] = 0 - } - this._totalTokens[filename] += count - if (this._totalTokens[filename] < 0) { - this._totalTokens[filename] = 0 - } - } - - public countAcceptedTokens(range: vscode.Range, text: string, filename: string) { - if (!this.isActive()) { - return - } - // generate accepted recommendation token and stored in collection - this.addAcceptedTokens(filename, { range: range, text: text, accepted: text.length }) - this.addTotalTokens(filename, text.length) - } - - // For below 2 edge cases - // 1. newline character with indentation - // 2. 2 character insertion of closing brackets - public getCharacterCountFromComplexEvent(e: vscode.TextDocumentChangeEvent) { - function countChanges(cond: boolean, text: string): number { - if (!cond) { - return 0 - } - if ((text.startsWith('\n') || text.startsWith('\r\n')) && text.trim().length === 0) { - return 1 - } - if (autoClosingKeystrokeInputs.includes(text)) { - return 2 - } - return 0 - } - if (e.contentChanges.length === 2) { - const text1 = e.contentChanges[0].text - const text2 = e.contentChanges[1].text - const text2Count = countChanges(text1.length === 0, text2) - const text1Count = countChanges(text2.length === 0, text1) - return text2Count > 0 ? text2Count : text1Count - } else if (e.contentChanges.length === 1) { - return countChanges(true, e.contentChanges[0].text) - } - return 0 - } - - public isFromUserKeystroke(e: vscode.TextDocumentChangeEvent) { - return e.contentChanges.length === 1 && e.contentChanges[0].text.length === 1 - } - - public countTotalTokens(e: vscode.TextDocumentChangeEvent) { - // ignore no contentChanges. ignore contentChanges from other plugins (formatters) - // only include contentChanges from user keystroke input(one character input). - // Also ignore deletion events due to a known issue of tracking deleted CodeWhiperer tokens. - if (!runtimeLanguageContext.isLanguageSupported(e.document.languageId) || vsCodeState.isCodeWhispererEditing) { - return - } - // a user keystroke input can be - // 1. content change with 1 character insertion - // 2. newline character with indentation - // 3. 2 character insertion of closing brackets - if (this.isFromUserKeystroke(e)) { - this.tryStartTimer() - this.addTotalTokens(e.document.fileName, 1) - } else if (this.getCharacterCountFromComplexEvent(e) !== 0) { - this.tryStartTimer() - const characterIncrease = this.getCharacterCountFromComplexEvent(e) - this.addTotalTokens(e.document.fileName, characterIncrease) - } - // also include multi character input within 50 characters (not from CWSPR) - else if ( - e.contentChanges.length === 1 && - e.contentChanges[0].text.length > 1 && - TelemetryHelper.instance.lastSuggestionInDisplay !== e.contentChanges[0].text - ) { - const multiCharInputSize = e.contentChanges[0].text.length - - // select 50 as the cut-off threshold for counting user input. - // ignore all white space multi char input, this usually comes from reformat. - if (multiCharInputSize < 50 && e.contentChanges[0].text.trim().length > 0) { - this.addTotalTokens(e.document.fileName, multiCharInputSize) - } - } - } - - public static readonly instances = new Map() - - public static getTracker(language: string): CodeWhispererCodeCoverageTracker | undefined { - if (!runtimeLanguageContext.isLanguageSupported(language)) { - return undefined - } - const cwsprLanguage = runtimeLanguageContext.normalizeLanguage(language) - if (!cwsprLanguage) { - return undefined - } - const instance = this.instances.get(cwsprLanguage) ?? new this(cwsprLanguage) - this.instances.set(cwsprLanguage, instance) - return instance - } -} diff --git a/packages/core/src/codewhisperer/util/closingBracketUtil.ts b/packages/core/src/codewhisperer/util/closingBracketUtil.ts deleted file mode 100644 index 466ca31a0b9..00000000000 --- a/packages/core/src/codewhisperer/util/closingBracketUtil.ts +++ /dev/null @@ -1,262 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as CodeWhispererConstants from '../models/constants' - -interface bracketMapType { - [k: string]: string -} - -const quotes = ["'", '"', '`'] -const parenthesis = ['(', '[', '{', ')', ']', '}', '<', '>'] - -const closeToOpen: bracketMapType = { - ')': '(', - ']': '[', - '}': '{', - '>': '<', -} - -const openToClose: bracketMapType = { - '(': ')', - '[': ']', - '{': '}', - '<': '>', -} - -/** - * LeftContext | Recommendation | RightContext - * This function aims to resolve symbols which are redundant and need to be removed - * The high level logic is as followed - * 1. Pair non-paired closing symbols(parenthesis, brackets, quotes) existing in the "recommendation" with non-paired symbols existing in the "leftContext" - * 2. Remove non-paired closing symbols existing in the "rightContext" - * @param endPosition: end position of the effective recommendation written by CodeWhisperer - * @param startPosition: start position of the effective recommendation by CodeWhisperer - * - * for example given file context ('|' is where we trigger the service): - * anArray.pu| - * recommendation returned: "sh(element);" - * typeahead: "sh(" - * the effective recommendation written by CodeWhisperer: "element);" - */ -export async function handleExtraBrackets( - editor: vscode.TextEditor, - endPosition: vscode.Position, - startPosition: vscode.Position -) { - const recommendation = editor.document.getText(new vscode.Range(startPosition, endPosition)) - const endOffset = editor.document.offsetAt(endPosition) - const startOffset = editor.document.offsetAt(startPosition) - const leftContext = editor.document.getText( - new vscode.Range( - startPosition, - editor.document.positionAt(Math.max(startOffset - CodeWhispererConstants.charactersLimit, 0)) - ) - ) - - const rightContext = editor.document.getText( - new vscode.Range( - editor.document.positionAt(endOffset), - editor.document.positionAt(endOffset + CodeWhispererConstants.charactersLimit) - ) - ) - const bracketsToRemove = getBracketsToRemove( - editor, - recommendation, - leftContext, - rightContext, - endPosition, - startPosition - ) - - const quotesToRemove = getQuotesToRemove( - editor, - recommendation, - leftContext, - rightContext, - endPosition, - startPosition - ) - - const symbolsToRemove = [...bracketsToRemove, ...quotesToRemove] - - if (symbolsToRemove.length) { - await removeBracketsFromRightContext(editor, symbolsToRemove, endPosition) - } -} - -const removeBracketsFromRightContext = async ( - editor: vscode.TextEditor, - idxToRemove: number[], - endPosition: vscode.Position -) => { - const offset = editor.document.offsetAt(endPosition) - - await editor.edit( - (editBuilder) => { - for (const idx of idxToRemove) { - const range = new vscode.Range( - editor.document.positionAt(offset + idx), - editor.document.positionAt(offset + idx + 1) - ) - editBuilder.delete(range) - } - }, - { undoStopAfter: false, undoStopBefore: false } - ) -} - -function getBracketsToRemove( - editor: vscode.TextEditor, - recommendation: string, - leftContext: string, - rightContext: string, - end: vscode.Position, - start: vscode.Position -) { - const unpairedClosingsInReco = nonClosedClosingParen(recommendation) - const unpairedOpeningsInLeftContext = nonClosedOpneingParen(leftContext, unpairedClosingsInReco.length) - const unpairedClosingsInRightContext = nonClosedClosingParen(rightContext) - - const toRemove: number[] = [] - - let i = 0 - let j = 0 - let k = 0 - while (i < unpairedOpeningsInLeftContext.length && j < unpairedClosingsInReco.length) { - const opening = unpairedOpeningsInLeftContext[i] - const closing = unpairedClosingsInReco[j] - - const isPaired = closeToOpen[closing.char] === opening.char - const rightContextCharToDelete = unpairedClosingsInRightContext[k] - - if (isPaired) { - if (rightContextCharToDelete && rightContextCharToDelete.char === closing.char) { - const rightContextStart = editor.document.offsetAt(end) + 1 - const symbolPosition = editor.document.positionAt( - rightContextStart + rightContextCharToDelete.strOffset - ) - const lineCnt = recommendation.split('\n').length - 1 - const isSameline = symbolPosition.line - lineCnt === start.line - - if (isSameline) { - toRemove.push(rightContextCharToDelete.strOffset) - } - - k++ - } - } - - i++ - j++ - } - - return toRemove -} - -function getQuotesToRemove( - editor: vscode.TextEditor, - recommendation: string, - leftContext: string, - rightContext: string, - endPosition: vscode.Position, - startPosition: vscode.Position -) { - let leftQuote: string | undefined = undefined - let leftIndex: number | undefined = undefined - for (let i = leftContext.length - 1; i >= 0; i--) { - const char = leftContext[i] - if (quotes.includes(char)) { - leftQuote = char - leftIndex = leftContext.length - i - break - } - } - - let rightQuote: string | undefined = undefined - let rightIndex: number | undefined = undefined - for (let i = 0; i < rightContext.length; i++) { - const char = rightContext[i] - if (quotes.includes(char)) { - rightQuote = char - rightIndex = i - break - } - } - - let quoteCountInReco = 0 - if (leftQuote && rightQuote && leftQuote === rightQuote) { - for (const char of recommendation) { - if (quotes.includes(char) && char === leftQuote) { - quoteCountInReco++ - } - } - } - - if (leftIndex !== undefined && rightIndex !== undefined && quoteCountInReco % 2 !== 0) { - const p = editor.document.positionAt(editor.document.offsetAt(endPosition) + rightIndex) - - if (endPosition.line === startPosition.line && endPosition.line === p.line) { - return [rightIndex] - } - } - - return [] -} - -function nonClosedOpneingParen(str: string, cnt?: number): { char: string; strOffset: number }[] { - const resultSet: { char: string; strOffset: number }[] = [] - const stack: string[] = [] - - for (let i = str.length - 1; i >= 0; i--) { - const char = str[i] - if (char! in parenthesis) { - continue - } - - if (char in closeToOpen) { - stack.push(char) - if (cnt && cnt === resultSet.length) { - return resultSet - } - } else if (char in openToClose) { - if (stack.length !== 0 && stack[stack.length - 1] === openToClose[char]) { - stack.pop() - } else { - resultSet.push({ char: char, strOffset: i }) - } - } - } - - return resultSet -} - -function nonClosedClosingParen(str: string, cnt?: number): { char: string; strOffset: number }[] { - const resultSet: { char: string; strOffset: number }[] = [] - const stack: string[] = [] - - for (let i = 0; i < str.length; i++) { - const char = str[i] - if (char! in parenthesis) { - continue - } - - if (char in openToClose) { - stack.push(char) - if (cnt && cnt === resultSet.length) { - return resultSet - } - } else if (char in closeToOpen) { - if (stack.length !== 0 && stack[stack.length - 1] === closeToOpen[char]) { - stack.pop() - } else { - resultSet.push({ char: char, strOffset: i }) - } - } - } - - return resultSet -} diff --git a/packages/core/src/codewhisperer/util/commonUtil.ts b/packages/core/src/codewhisperer/util/commonUtil.ts index d2df78f1369..729d3b7ed12 100644 --- a/packages/core/src/codewhisperer/util/commonUtil.ts +++ b/packages/core/src/codewhisperer/util/commonUtil.ts @@ -3,80 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as vscode from 'vscode' -import * as semver from 'semver' import { distance } from 'fastest-levenshtein' import { getInlineSuggestEnabled } from '../../shared/utilities/editorUtilities' -import { - AWSTemplateCaseInsensitiveKeyWords, - AWSTemplateKeyWords, - JsonConfigFileNamingConvention, -} from '../models/constants' export function getLocalDatetime() { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone return new Date().toLocaleString([], { timeZone: timezone }) } -export function asyncCallWithTimeout(asyncPromise: Promise, message: string, timeLimit: number): Promise { - let timeoutHandle: NodeJS.Timeout - const timeoutPromise = new Promise((_resolve, reject) => { - timeoutHandle = setTimeout(() => reject(new Error(message)), timeLimit) - }) - return Promise.race([asyncPromise, timeoutPromise]).then((result) => { - clearTimeout(timeoutHandle) - return result as T - }) -} - export function isInlineCompletionEnabled() { return getInlineSuggestEnabled() } -// This is the VS Code version that started to have regressions in inline completion API -export function isVscHavingRegressionInlineCompletionApi() { - return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() -} - -export function getFileExt(languageId: string) { - switch (languageId) { - case 'java': - return '.java' - case 'python': - return '.py' - default: - break - } - return undefined -} - -/** - * Returns the longest overlap between the Suffix of firstString and Prefix of second string - * getPrefixSuffixOverlap("adwg31", "31ggrs") = "31" - */ -export function getPrefixSuffixOverlap(firstString: string, secondString: string) { - let i = Math.min(firstString.length, secondString.length) - while (i > 0) { - if (secondString.slice(0, i) === firstString.slice(-i)) { - break - } - i-- - } - return secondString.slice(0, i) -} - -export function checkLeftContextKeywordsForJson(fileName: string, leftFileContent: string, language: string): boolean { - if ( - language === 'json' && - !AWSTemplateKeyWords.some((substring) => leftFileContent.includes(substring)) && - !AWSTemplateCaseInsensitiveKeyWords.some((substring) => leftFileContent.toLowerCase().includes(substring)) && - !JsonConfigFileNamingConvention.has(fileName.toLowerCase()) - ) { - return true - } - return false -} - // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace), // and thus the unmodified part of recommendation length can be deducted/approximated // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3 diff --git a/packages/core/src/codewhisperer/util/editorContext.ts b/packages/core/src/codewhisperer/util/editorContext.ts deleted file mode 100644 index 0861b982d13..00000000000 --- a/packages/core/src/codewhisperer/util/editorContext.ts +++ /dev/null @@ -1,289 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as codewhispererClient from '../client/codewhisperer' -import * as path from 'path' -import * as CodeWhispererConstants from '../models/constants' -import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' -import { getLogger } from '../../shared/logger/logger' -import { runtimeLanguageContext } from './runtimeLanguageContext' -import { fetchSupplementalContext } from './supplementalContext/supplementalContextUtil' -import { supplementalContextTimeoutInMs } from '../models/constants' -import { getSelectedCustomization } from './customizationUtil' -import { selectFrom } from '../../shared/utilities/tsUtils' -import { checkLeftContextKeywordsForJson } from './commonUtil' -import { CodeWhispererSupplementalContext } from '../models/model' -import { getOptOutPreference } from '../../shared/telemetry/util' -import { indent } from '../../shared/utilities/textUtilities' -import { isInDirectory } from '../../shared/filesystemUtilities' -import { AuthUtil } from './authUtil' -import { predictionTracker } from '../nextEditPrediction/activation' - -let tabSize: number = getTabSizeSetting() - -export function extractContextForCodeWhisperer(editor: vscode.TextEditor): codewhispererClient.FileContext { - const document = editor.document - const curPos = editor.selection.active - const offset = document.offsetAt(curPos) - - const caretLeftFileContext = editor.document.getText( - new vscode.Range( - document.positionAt(offset - CodeWhispererConstants.charactersLimit), - document.positionAt(offset) - ) - ) - - const caretRightFileContext = editor.document.getText( - new vscode.Range( - document.positionAt(offset), - document.positionAt(offset + CodeWhispererConstants.charactersLimit) - ) - ) - let languageName = 'plaintext' - if (!checkLeftContextKeywordsForJson(document.fileName, caretLeftFileContext, editor.document.languageId)) { - languageName = - runtimeLanguageContext.normalizeLanguage(editor.document.languageId) ?? editor.document.languageId - } - return { - filename: getFileRelativePath(editor), - programmingLanguage: { - languageName: languageName, - }, - leftFileContent: caretLeftFileContext, - rightFileContent: caretRightFileContext, - } as codewhispererClient.FileContext -} - -export function getFileName(editor: vscode.TextEditor): string { - const fileName = path.basename(editor.document.fileName) - return fileName.substring(0, CodeWhispererConstants.filenameCharsLimit) -} - -export function getFileRelativePath(editor: vscode.TextEditor): string { - const fileName = path.basename(editor.document.fileName) - let relativePath = '' - const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri) - if (!workspaceFolder) { - relativePath = fileName - } else { - const workspacePath = workspaceFolder.uri.fsPath - const filePath = editor.document.uri.fsPath - relativePath = path.relative(workspacePath, filePath) - } - // For notebook files, we want to use the programming language for each cell for the code suggestions, so change - // the filename sent in the request to reflect that language - if (relativePath.endsWith('.ipynb')) { - const fileExtension = runtimeLanguageContext.getLanguageExtensionForNotebook(editor.document.languageId) - if (fileExtension !== undefined) { - const filenameWithNewExtension = relativePath.substring(0, relativePath.length - 5) + fileExtension - return filenameWithNewExtension.substring(0, CodeWhispererConstants.filenameCharsLimit) - } - } - return relativePath.substring(0, CodeWhispererConstants.filenameCharsLimit) -} - -async function getWorkspaceId(editor: vscode.TextEditor): Promise { - try { - const workspaceIds: { workspaces: { workspaceRoot: string; workspaceId: string }[] } = - await vscode.commands.executeCommand('aws.amazonq.getWorkspaceId') - for (const item of workspaceIds.workspaces) { - const path = vscode.Uri.parse(item.workspaceRoot).fsPath - if (isInDirectory(path, editor.document.uri.fsPath)) { - return item.workspaceId - } - } - } catch (err) { - getLogger().warn(`No workspace id found ${err}`) - } - return undefined -} - -export async function buildListRecommendationRequest( - editor: vscode.TextEditor, - nextToken: string, - allowCodeWithReference: boolean -): Promise<{ - request: codewhispererClient.ListRecommendationsRequest - supplementalMetadata: CodeWhispererSupplementalContext | undefined -}> { - const fileContext = extractContextForCodeWhisperer(editor) - - const tokenSource = new vscode.CancellationTokenSource() - setTimeout(() => { - tokenSource.cancel() - }, supplementalContextTimeoutInMs) - - const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token) - - logSupplementalContext(supplementalContexts) - - // Get predictionSupplementalContext from PredictionTracker - let predictionSupplementalContext: codewhispererClient.SupplementalContext[] = [] - if (predictionTracker) { - predictionSupplementalContext = await predictionTracker.generatePredictionSupplementalContext() - } - - const selectedCustomization = getSelectedCustomization() - const completionSupplementalContext: codewhispererClient.SupplementalContext[] = supplementalContexts - ? supplementalContexts.supplementalContextItems.map((v) => { - return selectFrom(v, 'content', 'filePath') - }) - : [] - - const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile - - const editorState = getEditorState(editor, fileContext) - - // Combine inline and prediction supplemental contexts - const finalSupplementalContext = completionSupplementalContext.concat(predictionSupplementalContext) - return { - request: { - fileContext: fileContext, - nextToken: nextToken, - referenceTrackerConfiguration: { - recommendationsWithReferences: allowCodeWithReference ? 'ALLOW' : 'BLOCK', - }, - supplementalContexts: finalSupplementalContext, - editorState: editorState, - maxResults: CodeWhispererConstants.maxRecommendations, - customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, - optOutPreference: getOptOutPreference(), - workspaceId: await getWorkspaceId(editor), - profileArn: profile?.arn, - }, - supplementalMetadata: supplementalContexts, - } -} - -export async function buildGenerateRecommendationRequest(editor: vscode.TextEditor): Promise<{ - request: codewhispererClient.GenerateRecommendationsRequest - supplementalMetadata: CodeWhispererSupplementalContext | undefined -}> { - const fileContext = extractContextForCodeWhisperer(editor) - - const tokenSource = new vscode.CancellationTokenSource() - // the supplement context fetch mechanisms each has a timeout of supplementalContextTimeoutInMs - // adding 10 ms for overall timeout as buffer - setTimeout(() => { - tokenSource.cancel() - }, supplementalContextTimeoutInMs + 10) - const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token) - - logSupplementalContext(supplementalContexts) - - return { - request: { - fileContext: fileContext, - maxResults: CodeWhispererConstants.maxRecommendations, - supplementalContexts: supplementalContexts?.supplementalContextItems ?? [], - }, - supplementalMetadata: supplementalContexts, - } -} - -export function validateRequest( - req: codewhispererClient.ListRecommendationsRequest | codewhispererClient.GenerateRecommendationsRequest -): boolean { - const isLanguageNameValid = - req.fileContext.programmingLanguage.languageName !== undefined && - req.fileContext.programmingLanguage.languageName.length >= 1 && - req.fileContext.programmingLanguage.languageName.length <= 128 && - (runtimeLanguageContext.isLanguageSupported(req.fileContext.programmingLanguage.languageName) || - runtimeLanguageContext.isFileFormatSupported( - req.fileContext.filename.substring(req.fileContext.filename.lastIndexOf('.') + 1) - )) - const isFileNameValid = !(req.fileContext.filename === undefined || req.fileContext.filename.length < 1) - const isFileContextValid = !( - req.fileContext.leftFileContent.length > CodeWhispererConstants.charactersLimit || - req.fileContext.rightFileContent.length > CodeWhispererConstants.charactersLimit - ) - if (isFileNameValid && isLanguageNameValid && isFileContextValid) { - return true - } - return false -} - -export function updateTabSize(val: number): void { - tabSize = val -} - -export function getTabSize(): number { - return tabSize -} - -export function getEditorState(editor: vscode.TextEditor, fileContext: codewhispererClient.FileContext): any { - try { - return { - document: { - programmingLanguage: { - languageName: fileContext.programmingLanguage.languageName, - }, - relativeFilePath: fileContext.filename, - text: editor.document.getText(), - }, - cursorState: { - position: { - line: editor.selection.active.line, - character: editor.selection.active.character, - }, - }, - } - } catch (error) { - getLogger().error(`Error generating editor state: ${error}`) - return undefined - } -} - -export function getLeftContext(editor: vscode.TextEditor, line: number): string { - let lineText = '' - try { - if (editor && editor.document.lineAt(line)) { - lineText = editor.document.lineAt(line).text - if (lineText.length > CodeWhispererConstants.contextPreviewLen) { - lineText = - '...' + - lineText.substring( - lineText.length - CodeWhispererConstants.contextPreviewLen - 1, - lineText.length - 1 - ) - } - } - } catch (error) { - getLogger().error(`Error when getting left context ${error}`) - } - - return lineText -} - -function logSupplementalContext(supplementalContext: CodeWhispererSupplementalContext | undefined) { - if (!supplementalContext) { - return - } - - let logString = indent( - `CodeWhispererSupplementalContext: - isUtg: ${supplementalContext.isUtg}, - isProcessTimeout: ${supplementalContext.isProcessTimeout}, - contentsLength: ${supplementalContext.contentsLength}, - latency: ${supplementalContext.latency} - strategy: ${supplementalContext.strategy}`, - 4, - true - ).trimStart() - - for (const [index, context] of supplementalContext.supplementalContextItems.entries()) { - logString += indent(`\nChunk ${index}:\n`, 4, true) - logString += indent( - `Path: ${context.filePath} - Length: ${context.content.length} - Score: ${context.score}`, - 8, - true - ) - } - - getLogger().debug(logString) -} diff --git a/packages/core/src/codewhisperer/util/globalStateUtil.ts b/packages/core/src/codewhisperer/util/globalStateUtil.ts deleted file mode 100644 index 55376a83546..00000000000 --- a/packages/core/src/codewhisperer/util/globalStateUtil.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { vsCodeState } from '../models/model' - -export function resetIntelliSenseState( - isManualTriggerEnabled: boolean, - isAutomatedTriggerEnabled: boolean, - hasResponse: boolean -) { - /** - * Skip when CodeWhisperer service is turned off - */ - if (!isManualTriggerEnabled && !isAutomatedTriggerEnabled) { - return - } - - if (vsCodeState.isIntelliSenseActive && hasResponse) { - vsCodeState.isIntelliSenseActive = false - } -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts deleted file mode 100644 index c73a2eebaa4..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts +++ /dev/null @@ -1,130 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import path = require('path') -import { normalize } from '../../../shared/utilities/pathUtils' - -// TODO: functionExtractionPattern, classExtractionPattern, imposrtStatementRegex are not scalable and we will deprecate and remove the usage in the near future -export interface utgLanguageConfig { - extension: string - testFilenamePattern: RegExp[] - functionExtractionPattern?: RegExp - classExtractionPattern?: RegExp - importStatementRegExp?: RegExp -} - -export const utgLanguageConfigs: Record = { - // Java regexes are not working efficiently for class or function extraction - java: { - extension: '.java', - testFilenamePattern: [/^(.+)Test(\.java)$/, /(.+)Tests(\.java)$/, /Test(.+)(\.java)$/], - functionExtractionPattern: - /(?:(?:public|private|protected)\s+)(?:static\s+)?(?:[\w<>]+\s+)?(\w+)\s*\([^)]*\)\s*(?:(?:throws\s+\w+)?\s*)[{;]/gm, // TODO: Doesn't work for generice T functions. - classExtractionPattern: /(?<=^|\n)\s*public\s+class\s+(\w+)/gm, // TODO: Verify these. - importStatementRegExp: /import .*\.([a-zA-Z0-9]+);/, - }, - python: { - extension: '.py', - testFilenamePattern: [/^test_(.+)(\.py)$/, /^(.+)_test(\.py)$/], - functionExtractionPattern: /def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, // Worked fine - classExtractionPattern: /^class\s+(\w+)\s*:/gm, - importStatementRegExp: /from (.*) import.*/, - }, - typescript: { - extension: '.ts', - testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], - }, - javascript: { - extension: '.js', - testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], - }, - typescriptreact: { - extension: '.tsx', - testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], - }, - javascriptreact: { - extension: '.jsx', - testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], - }, -} - -export function extractFunctions(fileContent: string, regex?: RegExp) { - if (!regex) { - return [] - } - const functionNames: string[] = [] - let match: RegExpExecArray | null - - while ((match = regex.exec(fileContent)) !== null) { - functionNames.push(match[1]) - } - return functionNames -} - -export function extractClasses(fileContent: string, regex?: RegExp) { - if (!regex) { - return [] - } - const classNames: string[] = [] - let match: RegExpExecArray | null - - while ((match = regex.exec(fileContent)) !== null) { - classNames.push(match[1]) - } - return classNames -} - -export function countSubstringMatches(arr1: string[], arr2: string[]): number { - let count = 0 - for (const str1 of arr1) { - for (const str2 of arr2) { - if (str2.toLowerCase().includes(str1.toLowerCase())) { - count++ - } - } - } - return count -} - -export async function isTestFile( - filePath: string, - languageConfig: { - languageId: vscode.TextDocument['languageId'] - fileContent?: string - } -): Promise { - const normalizedFilePath = normalize(filePath) - const pathContainsTest = - normalizedFilePath.includes('tests/') || - normalizedFilePath.includes('test/') || - normalizedFilePath.includes('tst/') - const fileNameMatchTestPatterns = isTestFileByName(normalizedFilePath, languageConfig.languageId) - - if (pathContainsTest || fileNameMatchTestPatterns) { - return true - } - - return false -} - -function isTestFileByName(filePath: string, language: vscode.TextDocument['languageId']): boolean { - const languageConfig = utgLanguageConfigs[language] - if (!languageConfig) { - // We have enabled the support only for python and Java for this check - // as we depend on Regex for this validation. - return false - } - const testFilenamePattern = languageConfig.testFilenamePattern - - const filename = path.basename(filePath) - for (const pattern of testFilenamePattern) { - if (pattern.test(filename)) { - return true - } - } - - return false -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts deleted file mode 100644 index db1d7f312b2..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts +++ /dev/null @@ -1,395 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import path = require('path') -import { BM25Document, BM25Okapi } from './rankBm25' -import { - crossFileContextConfig, - supplementalContextTimeoutInMs, - supplementalContextMaxTotalLength, -} from '../../models/constants' -import { isTestFile } from './codeParsingUtil' -import { getFileDistance } from '../../../shared/filesystemUtilities' -import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' -import { getLogger } from '../../../shared/logger/logger' -import { - CodeWhispererSupplementalContext, - CodeWhispererSupplementalContextItem, - SupplementalContextStrategy, -} from '../../models/model' -import { LspController } from '../../../amazonq/lsp/lspController' -import { waitUntil } from '../../../shared/utilities/timeoutUtils' -import { FeatureConfigProvider } from '../../../shared/featureConfig' -import fs from '../../../shared/fs/fs' - -type CrossFileSupportedLanguage = - | 'java' - | 'python' - | 'javascript' - | 'typescript' - | 'javascriptreact' - | 'typescriptreact' - -// TODO: ugly, can we make it prettier? like we have to manually type 'java', 'javascriptreact' which is error prone -// TODO: Move to another config file or constants file -// Supported language to its corresponding file ext -const supportedLanguageToDialects: Readonly>> = { - java: new Set(['.java']), - python: new Set(['.py']), - javascript: new Set(['.js', '.jsx']), - javascriptreact: new Set(['.js', '.jsx']), - typescript: new Set(['.ts', '.tsx']), - typescriptreact: new Set(['.ts', '.tsx']), -} - -function isCrossFileSupported(languageId: string): languageId is CrossFileSupportedLanguage { - return Object.keys(supportedLanguageToDialects).includes(languageId) -} - -interface Chunk { - fileName: string - content: string - nextContent: string - score?: number -} - -/** - * `none`: supplementalContext is not supported - * `opentabs`: opentabs_BM25 - * `codemap`: repomap + opentabs BM25 - * `bm25`: global_BM25 - * `default`: repomap + global_BM25 - */ -type SupplementalContextConfig = 'none' | 'opentabs' | 'codemap' | 'bm25' | 'default' - -export async function fetchSupplementalContextForSrc( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise | undefined> { - const supplementalContextConfig = getSupplementalContextConfig(editor.document.languageId) - - // not supported case - if (supplementalContextConfig === 'none') { - return undefined - } - - // fallback to opentabs if projectContext timeout - const opentabsContextPromise = waitUntil( - async function () { - return await fetchOpentabsContext(editor, cancellationToken) - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - // opentabs context will use bm25 and users' open tabs to fetch supplemental context - if (supplementalContextConfig === 'opentabs') { - const supContext = (await opentabsContextPromise) ?? [] - return { - supplementalContextItems: supContext, - strategy: supContext.length === 0 ? 'empty' : 'opentabs', - } - } - - // codemap will use opentabs context plus repomap if it's present - if (supplementalContextConfig === 'codemap') { - let strategy: SupplementalContextStrategy = 'empty' - let hasCodemap: boolean = false - let hasOpentabs: boolean = false - const opentabsContextAndCodemap = await waitUntil( - async function () { - const result: CodeWhispererSupplementalContextItem[] = [] - const opentabsContext = await fetchOpentabsContext(editor, cancellationToken) - const codemap = await fetchProjectContext(editor, 'codemap') - - function addToResult(items: CodeWhispererSupplementalContextItem[]) { - for (const item of items) { - const curLen = result.reduce((acc, i) => acc + i.content.length, 0) - if (curLen + item.content.length < supplementalContextMaxTotalLength) { - result.push(item) - } - } - } - - if (codemap && codemap.length > 0) { - addToResult(codemap) - hasCodemap = true - } - - if (opentabsContext && opentabsContext.length > 0) { - addToResult(opentabsContext) - hasOpentabs = true - } - - return result - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - if (hasCodemap) { - strategy = 'codemap' - } else if (hasOpentabs) { - strategy = 'opentabs' - } else { - strategy = 'empty' - } - - return { - supplementalContextItems: opentabsContextAndCodemap ?? [], - strategy: strategy, - } - } - - // global bm25 without repomap - if (supplementalContextConfig === 'bm25') { - const projectBM25Promise = waitUntil( - async function () { - return await fetchProjectContext(editor, 'bm25') - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - const [projectContext, opentabsContext] = await Promise.all([projectBM25Promise, opentabsContextPromise]) - if (projectContext && projectContext.length > 0) { - return { - supplementalContextItems: projectContext, - strategy: 'bm25', - } - } - - const supContext = opentabsContext ?? [] - return { - supplementalContextItems: supContext, - strategy: supContext.length === 0 ? 'empty' : 'opentabs', - } - } - - // global bm25 with repomap - const projectContextAndCodemapPromise = waitUntil( - async function () { - return await fetchProjectContext(editor, 'default') - }, - { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } - ) - - const [projectContext, opentabsContext] = await Promise.all([ - projectContextAndCodemapPromise, - opentabsContextPromise, - ]) - if (projectContext && projectContext.length > 0) { - return { - supplementalContextItems: projectContext, - strategy: 'default', - } - } - - return { - supplementalContextItems: opentabsContext ?? [], - strategy: 'opentabs', - } -} - -export async function fetchProjectContext( - editor: vscode.TextEditor, - target: 'default' | 'codemap' | 'bm25' -): Promise { - const inputChunkContent = getInputChunk(editor) - - const inlineProjectContext: { content: string; score: number; filePath: string }[] = - await LspController.instance.queryInlineProjectContext( - inputChunkContent.content, - editor.document.uri.fsPath, - target - ) - - return inlineProjectContext -} - -export async function fetchOpentabsContext( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise { - const codeChunksCalculated = crossFileContextConfig.numberOfChunkToFetch - - // Step 1: Get relevant cross files to refer - const relevantCrossFilePaths = await getCrossFileCandidates(editor) - - // Step 2: Split files to chunks with upper bound on chunkCount - // We restrict the total number of chunks to improve on latency. - // Chunk linking is required as we want to pass the next chunk value for matched chunk. - let chunkList: Chunk[] = [] - for (const relevantFile of relevantCrossFilePaths) { - const chunks: Chunk[] = await splitFileToChunks(relevantFile, crossFileContextConfig.numberOfLinesEachChunk) - const linkedChunks = linkChunks(chunks) - chunkList.push(...linkedChunks) - if (chunkList.length >= codeChunksCalculated) { - break - } - } - - // it's required since chunkList.push(...) is likely giving us a list of size > 60 - chunkList = chunkList.slice(0, codeChunksCalculated) - - // Step 3: Generate Input chunk (10 lines left of cursor position) - // and Find Best K chunks w.r.t input chunk using BM25 - const inputChunk: Chunk = getInputChunk(editor) - const bestChunks: Chunk[] = findBestKChunkMatches(inputChunk, chunkList, crossFileContextConfig.topK) - - // Step 4: Transform best chunks to supplemental contexts - const supplementalContexts: CodeWhispererSupplementalContextItem[] = [] - let totalLength = 0 - for (const chunk of bestChunks) { - totalLength += chunk.nextContent.length - - if (totalLength > crossFileContextConfig.maximumTotalLength) { - break - } - - supplementalContexts.push({ - filePath: chunk.fileName, - content: chunk.nextContent, - score: chunk.score, - }) - } - - // DO NOT send code chunk with empty content - getLogger().debug(`CodeWhisperer finished fetching crossfile context out of ${relevantCrossFilePaths.length} files`) - return supplementalContexts -} - -function findBestKChunkMatches(chunkInput: Chunk, chunkReferences: Chunk[], k: number): Chunk[] { - const chunkContentList = chunkReferences.map((chunk) => chunk.content) - - // performBM25Scoring returns the output in a sorted order (descending of scores) - const top3: BM25Document[] = new BM25Okapi(chunkContentList).topN(chunkInput.content, crossFileContextConfig.topK) - - return top3.map((doc) => { - // reference to the original metadata since BM25.top3 will sort the result - const chunkIndex = doc.index - const chunkReference = chunkReferences[chunkIndex] - return { - content: chunkReference.content, - fileName: chunkReference.fileName, - nextContent: chunkReference.nextContent, - score: doc.score, - } - }) -} - -/* This extract 10 lines to the left of the cursor from trigger file. - * This will be the inputquery to bm25 matching against list of cross-file chunks - */ -function getInputChunk(editor: vscode.TextEditor) { - const chunkSize = crossFileContextConfig.numberOfLinesEachChunk - const cursorPosition = editor.selection.active - const startLine = Math.max(cursorPosition.line - chunkSize, 0) - const endLine = Math.max(cursorPosition.line - 1, 0) - const inputChunkContent = editor.document.getText( - new vscode.Range(startLine, 0, endLine, editor.document.lineAt(endLine).text.length) - ) - const inputChunk: Chunk = { fileName: editor.document.fileName, content: inputChunkContent, nextContent: '' } - return inputChunk -} - -/** - * Util to decide if we need to fetch crossfile context since CodeWhisperer CrossFile Context feature is gated by userGroup and language level - * @param languageId: VSCode language Identifier - * @returns specifically returning undefined if the langueage is not supported, - * otherwise true/false depending on if the language is fully supported or not belonging to the user group - */ -function getSupplementalContextConfig(languageId: vscode.TextDocument['languageId']): SupplementalContextConfig { - if (!isCrossFileSupported(languageId)) { - return 'none' - } - - const group = FeatureConfigProvider.instance.getProjectContextGroup() - switch (group) { - default: - return 'codemap' - } -} - -/** - * This linking is required from science experimentations to pass the next contnet chunk - * when a given chunk context passes the match in BM25. - * Special handling is needed for last(its next points to its own) and first chunk - */ -export function linkChunks(chunks: Chunk[]) { - const updatedChunks: Chunk[] = [] - - // This additional chunk is needed to create a next pointer to chunk 0. - const firstChunk = chunks[0] - const firstChunkSubContent = firstChunk.content.split('\n').slice(0, 3).join('\n').trimEnd() - const newFirstChunk = { - fileName: firstChunk.fileName, - content: firstChunkSubContent, - nextContent: firstChunk.content, - } - updatedChunks.push(newFirstChunk) - - const n = chunks.length - for (let i = 0; i < n; i++) { - const chunk = chunks[i] - const nextChunk = i < n - 1 ? chunks[i + 1] : chunk - - chunk.nextContent = nextChunk.content - updatedChunks.push(chunk) - } - - return updatedChunks -} - -export async function splitFileToChunks(filePath: string, chunkSize: number): Promise { - const chunks: Chunk[] = [] - - const fileContent = (await fs.readFileText(filePath)).trimEnd() - const lines = fileContent.split('\n') - - for (let i = 0; i < lines.length; i += chunkSize) { - const chunkContent = lines.slice(i, Math.min(i + chunkSize, lines.length)).join('\n') - const chunk = { fileName: filePath, content: chunkContent.trimEnd(), nextContent: '' } - chunks.push(chunk) - } - return chunks -} - -/** - * This function will return relevant cross files sorted by file distance for the given editor file - * by referencing open files, imported files and same package files. - */ -export async function getCrossFileCandidates(editor: vscode.TextEditor): Promise { - const targetFile = editor.document.uri.fsPath - const language = editor.document.languageId as CrossFileSupportedLanguage - const dialects = supportedLanguageToDialects[language] - - /** - * Consider a file which - * 1. is different from the target - * 2. has the same file extension or it's one of the dialect of target file (e.g .js vs. .jsx) - * 3. is not a test file - */ - const unsortedCandidates = await getOpenFilesInWindow(async (candidateFile) => { - return ( - targetFile !== candidateFile && - (path.extname(targetFile) === path.extname(candidateFile) || - (dialects && dialects.has(path.extname(candidateFile)))) && - !(await isTestFile(candidateFile, { languageId: language })) - ) - }) - - return unsortedCandidates - .map((candidate) => { - return { - file: candidate, - fileDistance: getFileDistance(targetFile, candidate), - } - }) - .sort((file1, file2) => { - return file1.fileDistance - file2.fileDistance - }) - .map((fileToDistance) => { - return fileToDistance.file - }) -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts b/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts deleted file mode 100644 index a2c77e0b10f..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// Implementation inspired by https://github.com/dorianbrown/rank_bm25/blob/990470ebbe6b28c18216fd1a8b18fe7446237dd6/rank_bm25.py#L52 - -export interface BM25Document { - content: string - /** The score that the document receives. */ - score: number - - index: number -} - -export abstract class BM25 { - protected readonly corpusSize: number - protected readonly avgdl: number - protected readonly idf: Map = new Map() - protected readonly docLen: number[] = [] - protected readonly docFreqs: Map[] = [] - protected readonly nd: Map = new Map() - - constructor( - protected readonly corpus: string[], - protected readonly tokenizer: (str: string) => string[] = defaultTokenizer, - protected readonly k1: number, - protected readonly b: number, - protected readonly epsilon: number - ) { - this.corpusSize = corpus.length - - let numDoc = 0 - for (const document of corpus.map((document) => { - return tokenizer(document) - })) { - this.docLen.push(document.length) - numDoc += document.length - - const frequencies = new Map() - for (const word of document) { - frequencies.set(word, (frequencies.get(word) || 0) + 1) - } - this.docFreqs.push(frequencies) - - for (const [word, _] of frequencies.entries()) { - this.nd.set(word, (this.nd.get(word) || 0) + 1) - } - } - - this.avgdl = numDoc / this.corpusSize - - this.calIdf(this.nd) - } - - abstract calIdf(nd: Map): void - - abstract score(query: string): BM25Document[] - - topN(query: string, n: number): BM25Document[] { - const notSorted = this.score(query) - const sorted = notSorted.sort((a, b) => b.score - a.score) - return sorted.slice(0, Math.min(n, sorted.length)) - } -} - -export class BM25Okapi extends BM25 { - constructor(corpus: string[], tokenizer: (str: string) => string[] = defaultTokenizer) { - super(corpus, tokenizer, 1.5, 0.75, 0.25) - } - - calIdf(nd: Map): void { - let idfSum = 0 - - const negativeIdfs: string[] = [] - for (const [word, freq] of nd) { - const idf = Math.log(this.corpusSize - freq + 0.5) - Math.log(freq + 0.5) - this.idf.set(word, idf) - idfSum += idf - - if (idf < 0) { - negativeIdfs.push(word) - } - } - - const averageIdf = idfSum / this.idf.size - const eps = this.epsilon * averageIdf - for (const word of negativeIdfs) { - this.idf.set(word, eps) - } - } - - score(query: string): BM25Document[] { - const queryWords = defaultTokenizer(query) - return this.docFreqs.map((docFreq, index) => { - let score = 0 - for (const [_, queryWord] of queryWords.entries()) { - const queryWordFreqForDocument = docFreq.get(queryWord) || 0 - const numerator = (this.idf.get(queryWord) || 0.0) * queryWordFreqForDocument * (this.k1 + 1) - const denominator = - queryWordFreqForDocument + this.k1 * (1 - this.b + (this.b * this.docLen[index]) / this.avgdl) - - score += numerator / denominator - } - - return { - content: this.corpus[index], - score: score, - index: index, - } - }) - } -} - -// TODO: This is a very simple tokenizer, we want to replace this by more sophisticated one. -function defaultTokenizer(content: string): string[] { - const regex = /\w+/g - const words = content.split(' ') - const result = [] - for (const word of words) { - const wordList = findAll(word, regex) - result.push(...wordList) - } - - return result -} - -function findAll(str: string, re: RegExp): string[] { - let match: RegExpExecArray | null - const matches: string[] = [] - - while ((match = re.exec(str)) !== null) { - matches.push(match[0]) - } - - return matches -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts deleted file mode 100644 index bd214ace44e..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { fetchSupplementalContextForTest } from './utgUtils' -import { fetchSupplementalContextForSrc } from './crossFileContextUtil' -import { isTestFile } from './codeParsingUtil' -import * as vscode from 'vscode' -import { CancellationError } from '../../../shared/utilities/timeoutUtils' -import { ToolkitError } from '../../../shared/errors' -import { getLogger } from '../../../shared/logger/logger' -import { CodeWhispererSupplementalContext } from '../../models/model' -import * as os from 'os' -import { crossFileContextConfig } from '../../models/constants' - -export async function fetchSupplementalContext( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise { - const timesBeforeFetching = performance.now() - - const isUtg = await isTestFile(editor.document.uri.fsPath, { - languageId: editor.document.languageId, - fileContent: editor.document.getText(), - }) - - let supplementalContextPromise: Promise< - Pick | undefined - > - - if (isUtg) { - supplementalContextPromise = fetchSupplementalContextForTest(editor, cancellationToken) - } else { - supplementalContextPromise = fetchSupplementalContextForSrc(editor, cancellationToken) - } - - return supplementalContextPromise - .then((value) => { - if (value) { - const resBeforeTruncation = { - isUtg: isUtg, - isProcessTimeout: false, - supplementalContextItems: value.supplementalContextItems.filter( - (item) => item.content.trim().length !== 0 - ), - contentsLength: value.supplementalContextItems.reduce((acc, curr) => acc + curr.content.length, 0), - latency: performance.now() - timesBeforeFetching, - strategy: value.strategy, - } - - return truncateSuppelementalContext(resBeforeTruncation) - } else { - return undefined - } - }) - .catch((err) => { - if (err instanceof ToolkitError && err.cause instanceof CancellationError) { - return { - isUtg: isUtg, - isProcessTimeout: true, - supplementalContextItems: [], - contentsLength: 0, - latency: performance.now() - timesBeforeFetching, - strategy: 'empty', - } - } else { - getLogger().error( - `Fail to fetch supplemental context for target file ${editor.document.fileName}: ${err}` - ) - return undefined - } - }) -} - -/** - * Requirement - * - Maximum 5 supplemental context. - * - Each chunk can't exceed 10240 characters - * - Sum of all chunks can't exceed 20480 characters - */ -export function truncateSuppelementalContext( - context: CodeWhispererSupplementalContext -): CodeWhispererSupplementalContext { - let c = context.supplementalContextItems.map((item) => { - if (item.content.length > crossFileContextConfig.maxLengthEachChunk) { - return { - ...item, - content: truncateLineByLine(item.content, crossFileContextConfig.maxLengthEachChunk), - } - } else { - return item - } - }) - - if (c.length > crossFileContextConfig.maxContextCount) { - c = c.slice(0, crossFileContextConfig.maxContextCount) - } - - let curTotalLength = c.reduce((acc, cur) => { - return acc + cur.content.length - }, 0) - while (curTotalLength >= 20480 && c.length - 1 >= 0) { - const last = c[c.length - 1] - c = c.slice(0, -1) - curTotalLength -= last.content.length - } - - return { - ...context, - supplementalContextItems: c, - contentsLength: curTotalLength, - } -} - -export function truncateLineByLine(input: string, l: number): string { - const maxLength = l > 0 ? l : -1 * l - if (input.length === 0) { - return '' - } - - const shouldAddNewLineBack = input.endsWith(os.EOL) - let lines = input.trim().split(os.EOL) - let curLen = input.length - while (curLen > maxLength && lines.length - 1 >= 0) { - const last = lines[lines.length - 1] - lines = lines.slice(0, -1) - curLen -= last.length + 1 - } - - const r = lines.join(os.EOL) - if (shouldAddNewLineBack) { - return r + os.EOL - } else { - return r - } -} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts deleted file mode 100644 index 0d33969773e..00000000000 --- a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts +++ /dev/null @@ -1,229 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as path from 'path' -import { fs } from '../../../shared/fs/fs' -import * as vscode from 'vscode' -import { - countSubstringMatches, - extractClasses, - extractFunctions, - isTestFile, - utgLanguageConfig, - utgLanguageConfigs, -} from './codeParsingUtil' -import { ToolkitError } from '../../../shared/errors' -import { supplemetalContextFetchingTimeoutMsg } from '../../models/constants' -import { CancellationError } from '../../../shared/utilities/timeoutUtils' -import { utgConfig } from '../../models/constants' -import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' -import { getLogger } from '../../../shared/logger/logger' -import { CodeWhispererSupplementalContext, CodeWhispererSupplementalContextItem, UtgStrategy } from '../../models/model' - -const utgSupportedLanguages: vscode.TextDocument['languageId'][] = ['java', 'python'] - -type UtgSupportedLanguage = (typeof utgSupportedLanguages)[number] - -function isUtgSupportedLanguage(languageId: vscode.TextDocument['languageId']): languageId is UtgSupportedLanguage { - return utgSupportedLanguages.includes(languageId) -} - -export function shouldFetchUtgContext(languageId: vscode.TextDocument['languageId']): boolean | undefined { - if (!isUtgSupportedLanguage(languageId)) { - return undefined - } - - return languageId === 'java' -} - -/** - * This function attempts to find a focal file for the given trigger file. - * Attempt 1: If naming patterns followed correctly, source file can be found by name referencing. - * Attempt 2: Compare the function and class names of trigger file and all other open files in editor - * to find the closest match. - * Once found the focal file, we split it into multiple pieces as supplementalContext. - * @param editor - * @returns - */ -export async function fetchSupplementalContextForTest( - editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken -): Promise | undefined> { - const shouldProceed = shouldFetchUtgContext(editor.document.languageId) - - if (!shouldProceed) { - return shouldProceed === undefined ? undefined : { supplementalContextItems: [], strategy: 'empty' } - } - - const languageConfig = utgLanguageConfigs[editor.document.languageId] - - // TODO (Metrics): 1. Total number of calls to fetchSupplementalContextForTest - throwIfCancelled(cancellationToken) - - let crossSourceFile = await findSourceFileByName(editor, languageConfig, cancellationToken) - if (crossSourceFile) { - // TODO (Metrics): 2. Success count for fetchSourceFileByName (find source file by name) - getLogger().debug(`CodeWhisperer finished fetching utg context by file name`) - return { - supplementalContextItems: await generateSupplementalContextFromFocalFile( - crossSourceFile, - 'byName', - cancellationToken - ), - strategy: 'byName', - } - } - throwIfCancelled(cancellationToken) - - crossSourceFile = await findSourceFileByContent(editor, languageConfig, cancellationToken) - if (crossSourceFile) { - // TODO (Metrics): 3. Success count for fetchSourceFileByContent (find source file by content) - getLogger().debug(`CodeWhisperer finished fetching utg context by file content`) - return { - supplementalContextItems: await generateSupplementalContextFromFocalFile( - crossSourceFile, - 'byContent', - cancellationToken - ), - strategy: 'byContent', - } - } - - // TODO (Metrics): 4. Failure count - when unable to find focal file (supplemental context empty) - getLogger().debug(`CodeWhisperer failed to fetch utg context`) - return { - supplementalContextItems: [], - strategy: 'empty', - } -} - -async function generateSupplementalContextFromFocalFile( - filePath: string, - strategy: UtgStrategy, - cancellationToken: vscode.CancellationToken -): Promise { - const fileContent = await fs.readFileText(vscode.Uri.parse(filePath!).fsPath) - - // DO NOT send code chunk with empty content - if (fileContent.trim().length === 0) { - return [] - } - - return [ - { - filePath: filePath, - content: 'UTG\n' + fileContent.slice(0, Math.min(fileContent.length, utgConfig.maxSegmentSize)), - }, - ] -} - -async function findSourceFileByContent( - editor: vscode.TextEditor, - languageConfig: utgLanguageConfig, - cancellationToken: vscode.CancellationToken -): Promise { - const testFileContent = await fs.readFileText(editor.document.fileName) - const testElementList = extractFunctions(testFileContent, languageConfig.functionExtractionPattern) - - throwIfCancelled(cancellationToken) - - testElementList.push(...extractClasses(testFileContent, languageConfig.classExtractionPattern)) - - throwIfCancelled(cancellationToken) - - let sourceFilePath: string | undefined = undefined - let maxMatchCount = 0 - - if (testElementList.length === 0) { - // TODO: Add metrics here, as unable to parse test file using Regex. - return sourceFilePath - } - - const relevantFilePaths = await getRelevantUtgFiles(editor) - - throwIfCancelled(cancellationToken) - - // TODO (Metrics):Add metrics for relevantFilePaths length - for (const filePath of relevantFilePaths) { - throwIfCancelled(cancellationToken) - - const fileContent = await fs.readFileText(filePath) - const elementList = extractFunctions(fileContent, languageConfig.functionExtractionPattern) - elementList.push(...extractClasses(fileContent, languageConfig.classExtractionPattern)) - const matchCount = countSubstringMatches(elementList, testElementList) - if (matchCount > maxMatchCount) { - maxMatchCount = matchCount - sourceFilePath = filePath - } - } - return sourceFilePath -} - -async function getRelevantUtgFiles(editor: vscode.TextEditor): Promise { - const targetFile = editor.document.uri.fsPath - const language = editor.document.languageId - - return await getOpenFilesInWindow(async (candidateFile) => { - return ( - targetFile !== candidateFile && - path.extname(targetFile) === path.extname(candidateFile) && - !(await isTestFile(candidateFile, { languageId: language })) - ) - }) -} - -export function guessSrcFileName( - testFileName: string, - languageId: vscode.TextDocument['languageId'] -): string | undefined { - const languageConfig = utgLanguageConfigs[languageId] - if (!languageConfig) { - return undefined - } - - for (const pattern of languageConfig.testFilenamePattern) { - try { - const match = testFileName.match(pattern) - if (match) { - return match[1] + match[2] - } - } catch (err) { - if (err instanceof Error) { - getLogger().error( - `codewhisperer: error while guessing source file name from file ${testFileName} and pattern ${pattern}: ${err.message}` - ) - } - } - } - - return undefined -} - -async function findSourceFileByName( - editor: vscode.TextEditor, - languageConfig: utgLanguageConfig, - cancellationToken: vscode.CancellationToken -): Promise { - const testFileName = path.basename(editor.document.fileName) - const assumedSrcFileName = guessSrcFileName(testFileName, editor.document.languageId) - if (!assumedSrcFileName) { - return undefined - } - - const sourceFiles = await vscode.workspace.findFiles(`**/${assumedSrcFileName}`) - - throwIfCancelled(cancellationToken) - - if (sourceFiles.length > 0) { - return sourceFiles[0].toString() - } - return undefined -} - -function throwIfCancelled(token: vscode.CancellationToken): void | never { - if (token.isCancellationRequested) { - throw new ToolkitError(supplemetalContextFetchingTimeoutMsg, { cause: new CancellationError('timeout') }) - } -} diff --git a/packages/core/src/test/codewhisperer/testUtil.ts b/packages/core/src/test/codewhisperer/testUtil.ts index b0c66d6552f..dd8188b1006 100644 --- a/packages/core/src/test/codewhisperer/testUtil.ts +++ b/packages/core/src/test/codewhisperer/testUtil.ts @@ -14,7 +14,6 @@ import { } from '../../codewhisperer/models/model' import { MockDocument } from '../fake/fakeDocument' import { getLogger } from '../../shared/logger' -import { CodeWhispererCodeCoverageTracker } from '../../codewhisperer/tracker/codewhispererCodeCoverageTracker' import globals from '../../shared/extensionGlobals' import { session } from '../../codewhisperer/util/codeWhispererSession' import { DefaultAWSClientBuilder, ServiceOptions } from '../../shared/awsClientBuilder' @@ -30,7 +29,6 @@ import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports export async function resetCodeWhispererGlobalVariables() { vsCodeState.isIntelliSenseActive = false vsCodeState.isCodeWhispererEditing = false - CodeWhispererCodeCoverageTracker.instances.clear() globals.telemetry.logger.clear() session.reset() await globals.globalState.clear() From 4a279d832f9461e1e5d9cc93111b6e78a4b137c0 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Wed, 7 May 2025 11:58:57 -0400 Subject: [PATCH 05/48] feat(amazonq): add experiment for basic e2e flow of inline chat through Flare. (#7235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem This is the initial set of work required to get inline chat running through flare. ## Solution - **add a feature flag for inline chat**: this allows testing of the two implementations side-by-side by flipping the feature flag. - **move general utils out of chat**: stuff like encryption and editorState can all be reused. - **render full diff response from inline chat**: this does not include progress updates from the language server. ## Testing and Verification https://github.com/user-attachments/assets/0dff58b7-40f7-487d-9f9e-d58610201041 ## Future Work / Next Steps - ensure telemetry is still being emitted. - add tests for new flow. (there aren't any for the existing one) - handle partial events from the language server. ## Known Bugs - selecting part of a line will cause the text to insert mid-line  - running inline-chat without a selection causes the entire file to be copied (This is in JB, Eclipse Prod, but IMO it makes the feature unusable). --- - 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/extensionNode.ts | 2 - packages/amazonq/src/inlineChat/activation.ts | 5 +- .../controller/inlineChatController.ts | 53 +++++++++++++++++-- .../inlineChat/provider/inlineChatProvider.ts | 46 +++++++++++++++- packages/amazonq/src/lsp/chat/messages.ts | 41 +------------- packages/amazonq/src/lsp/client.ts | 3 ++ packages/amazonq/src/lsp/encryption.ts | 28 ++++++++++ packages/amazonq/src/lsp/utils.ts | 26 +++++++++ .../core/src/shared/settings-toolkit.gen.ts | 1 + packages/toolkit/package.json | 4 ++ 10 files changed, 161 insertions(+), 48 deletions(-) create mode 100644 packages/amazonq/src/lsp/encryption.ts create mode 100644 packages/amazonq/src/lsp/utils.ts diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts index 8224b9ce310..d42fafea058 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -25,7 +25,6 @@ import { DevOptions } from 'aws-core-vscode/dev' import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth' import api from './api' import { activate as activateCWChat } from './app/chat/activation' -import { activate as activateInlineChat } from './inlineChat/activation' import { beta } from 'aws-core-vscode/dev' import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications' import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' @@ -73,7 +72,6 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { } activateAgents() await activateTransformationHub(extContext as ExtContext) - activateInlineChat(context) const authProvider = new CommonAuthViewProvider( context, diff --git a/packages/amazonq/src/inlineChat/activation.ts b/packages/amazonq/src/inlineChat/activation.ts index a42dfdb3e02..01e9f420c05 100644 --- a/packages/amazonq/src/inlineChat/activation.ts +++ b/packages/amazonq/src/inlineChat/activation.ts @@ -5,8 +5,9 @@ import * as vscode from 'vscode' import { InlineChatController } from './controller/inlineChatController' import { registerInlineCommands } from './command/registerInlineCommands' +import { LanguageClient } from 'vscode-languageclient' -export function activate(context: vscode.ExtensionContext) { - const inlineChatController = new InlineChatController(context) +export function activate(context: vscode.ExtensionContext, client: LanguageClient, encryptionKey: Buffer) { + const inlineChatController = new InlineChatController(context, client, encryptionKey) registerInlineCommands(context, inlineChatController) } diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 7ace8d0095e..4eb7c0a7c26 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -14,6 +14,7 @@ import { CodelensProvider } from '../codeLenses/codeLenseProvider' import { PromptMessage, ReferenceLogController } from 'aws-core-vscode/codewhispererChat' import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' import { UserWrittenCodeTracker } from 'aws-core-vscode/codewhisperer' +import { LanguageClient } from 'vscode-languageclient' import { codicon, getIcon, @@ -23,6 +24,7 @@ import { Timeout, textDocumentUtil, isSageMaker, + Experiments, } from 'aws-core-vscode/shared' import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' @@ -33,14 +35,18 @@ export class InlineChatController { private readonly codeLenseProvider: CodelensProvider private readonly referenceLogController = new ReferenceLogController() private readonly inlineLineAnnotationController: InlineLineAnnotationController + private readonly computeDiffAndRenderOnEditor: (query: string) => Promise private userQuery: string | undefined private listeners: vscode.Disposable[] = [] - constructor(context: vscode.ExtensionContext) { - this.inlineChatProvider = new InlineChatProvider() + constructor(context: vscode.ExtensionContext, client: LanguageClient, encryptionKey: Buffer) { + this.inlineChatProvider = new InlineChatProvider(client, encryptionKey) this.inlineChatProvider.onErrorOccured(() => this.handleError()) this.codeLenseProvider = new CodelensProvider(context) this.inlineLineAnnotationController = new InlineLineAnnotationController(context) + this.computeDiffAndRenderOnEditor = Experiments.instance.get('amazonqLSPInlineChat', false) + ? this.computeDiffAndRenderOnEditorLSP.bind(this) + : this.computeDiffAndRenderOnEditorLocal.bind(this) } public async createTask( @@ -206,7 +212,7 @@ export class InlineChatController { await textDocumentUtil.addEofNewline(editor) this.task = await this.createTask(query, editor.document, editor.selection) await this.inlineLineAnnotationController.disable(editor) - await this.computeDiffAndRenderOnEditor(query, editor.document).catch(async (err) => { + await this.computeDiffAndRenderOnEditor(query).catch(async (err) => { getLogger().error('computeDiffAndRenderOnEditor error: %s', (err as Error)?.message) if (err instanceof Error) { void vscode.window.showErrorMessage(`Amazon Q: ${err.message}`) @@ -218,7 +224,46 @@ export class InlineChatController { }) } - private async computeDiffAndRenderOnEditor(query: string, document: vscode.TextDocument) { + private async computeDiffAndRenderOnEditorLSP(query: string) { + if (!this.task) { + return + } + + await this.updateTaskAndLenses(this.task, TaskState.InProgress) + getLogger().info(`inline chat query:\n${query}`) + const uuid = randomUUID() + const message: PromptMessage = { + message: query, + messageId: uuid, + command: undefined, + userIntent: undefined, + tabID: uuid, + } + + const response = await this.inlineChatProvider.processPromptMessageLSP(message) + + // TODO: add tests for this case. + if (!response.body) { + getLogger().warn('Empty body in inline chat response') + await this.handleError() + return + } + + // Update inline diff view + const textDiff = computeDiff(response.body, this.task, false) + const decorations = computeDecorations(this.task) + this.task.decorations = decorations + await this.applyDiff(this.task, textDiff ?? []) + this.decorator.applyDecorations(this.task) + + // Update Codelenses + await this.updateTaskAndLenses(this.task, TaskState.WaitingForDecision) + await setContext('amazonq.inline.codelensShortcutEnabled', true) + this.undoListener(this.task) + } + + // TODO: remove this implementation in favor of LSP + private async computeDiffAndRenderOnEditorLocal(query: string) { if (!this.task) { return } diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index e6534d65532..86fe0ac2ade 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -8,6 +8,8 @@ import { CodeWhispererStreamingServiceException, GenerateAssistantResponseCommandOutput, } from '@amzn/codewhisperer-streaming' +import { LanguageClient } from 'vscode-languageclient' +import { inlineChatRequestType } from '@aws/language-server-runtimes/protocol' import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' import { ChatSessionStorage, @@ -25,6 +27,9 @@ import { codeWhispererClient } from 'aws-core-vscode/codewhisperer' import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' import { InlineTask } from '../controller/inlineTask' import { extractAuthFollowUp } from 'aws-core-vscode/amazonq' +import { InlineChatParams, InlineChatResult } from '@aws/language-server-runtimes-types' +import { decodeRequest, encryptRequest } from '../../lsp/encryption' +import { getCursorState } from '../../lsp/utils' export class InlineChatProvider { private readonly editorContextExtractor: EditorContextExtractor @@ -34,13 +39,52 @@ export class InlineChatProvider { private errorEmitter = new vscode.EventEmitter() public onErrorOccured = this.errorEmitter.event - public constructor() { + public constructor( + private readonly client: LanguageClient, + private readonly encryptionKey: Buffer + ) { this.editorContextExtractor = new EditorContextExtractor() this.userIntentRecognizer = new UserIntentRecognizer() this.sessionStorage = new ChatSessionStorage() this.triggerEventsStorage = new TriggerEventsStorage() } + private getCurrentEditorParams(prompt: string): InlineChatParams { + const editor = vscode.window.activeTextEditor + if (!editor) { + throw new ToolkitError('No active editor') + } + + const documentUri = editor.document.uri.toString() + const cursorState = getCursorState(editor.selections) + return { + prompt: { + prompt, + }, + cursorState, + textDocument: { + uri: documentUri, + }, + } + } + + public async processPromptMessageLSP(message: PromptMessage): Promise { + // TODO: handle partial responses. + getLogger().info('Making inline chat request with message %O', message) + const params = this.getCurrentEditorParams(message.message ?? '') + const inlineChatRequest = await encryptRequest(params, this.encryptionKey) + const response = await this.client.sendRequest(inlineChatRequestType.method, inlineChatRequest) + const decryptedMessage = + typeof response === 'string' && this.encryptionKey + ? await decodeRequest(response, this.encryptionKey) + : response + const result: InlineChatResult = decryptedMessage as InlineChatResult + this.client.info(`Logging response for inline chat ${JSON.stringify(decryptedMessage)}`) + + return result + } + + // TODO: remove in favor of LSP implementation. public async processPromptMessage(message: PromptMessage) { return this.editorContextExtractor .extractContextForTrigger('ChatMessage') diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 9578858b708..93ede65fc9a 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -55,7 +55,6 @@ import { import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' -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' @@ -68,6 +67,8 @@ import { } from 'aws-core-vscode/amazonq' import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' +import { decodeRequest, encryptRequest } from '../encryption' +import { getCursorState } from '../utils' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { languageClient.info( @@ -99,21 +100,6 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie }) } -function getCursorState(selection: readonly vscode.Selection[]) { - return selection.map((s) => ({ - range: { - start: { - line: s.start.line, - character: s.start.character, - }, - end: { - line: s.end.line, - character: s.end.character, - }, - }, - })) -} - export function registerMessageListeners( languageClient: LanguageClient, provider: AmazonQChatViewProvider, @@ -487,29 +473,6 @@ function isServerEvent(command: string) { return command.startsWith('aws/chat/') || command === 'telemetry/event' } -async function encryptRequest(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> { - const payload = new TextEncoder().encode(JSON.stringify(params)) - - const encryptedMessage = await new jose.CompactEncrypt(payload) - .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) - .encrypt(encryptionKey) - - return { message: encryptedMessage } -} - -async function decodeRequest(request: string, key: Buffer): Promise { - const result = await jose.jwtDecrypt(request, key, { - clockTolerance: 60, // Allow up to 60 seconds to account for clock differences - contentEncryptionAlgorithms: ['A256GCM'], - keyManagementAlgorithms: ['dir'], - }) - - if (!result.payload) { - throw new Error('JWT payload not found') - } - return result.payload as T -} - /** * Decodes partial chat responses from the language server before sending them to mynah UI */ diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 5e9a482e7fb..0df836e33a8 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -39,6 +39,7 @@ import { processUtils } from 'aws-core-vscode/shared' import { activate } from './chat/activation' import { AmazonQResourcePaths } from './lspInstaller' import { ConfigSection, isValidConfigSection, toAmazonQLSPLogLevel } from './config' +import { activate as activateInlineChat } from '../inlineChat/activation' const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') @@ -182,6 +183,8 @@ export async function startLanguageServer( await activate(client, encryptionKey, resourcePaths.ui) } + activateInlineChat(extensionContext, client, encryptionKey) + const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond) const sendProfileToLsp = async () => { diff --git a/packages/amazonq/src/lsp/encryption.ts b/packages/amazonq/src/lsp/encryption.ts new file mode 100644 index 00000000000..213ee3c1553 --- /dev/null +++ b/packages/amazonq/src/lsp/encryption.ts @@ -0,0 +1,28 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as jose from 'jose' + +export async function encryptRequest(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> { + const payload = new TextEncoder().encode(JSON.stringify(params)) + + const encryptedMessage = await new jose.CompactEncrypt(payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + return { message: encryptedMessage } +} + +export async function decodeRequest(request: string, key: Buffer): Promise { + const result = await jose.jwtDecrypt(request, key, { + clockTolerance: 60, // Allow up to 60 seconds to account for clock differences + contentEncryptionAlgorithms: ['A256GCM'], + keyManagementAlgorithms: ['dir'], + }) + + if (!result.payload) { + throw new Error('JWT payload not found') + } + return result.payload as T +} diff --git a/packages/amazonq/src/lsp/utils.ts b/packages/amazonq/src/lsp/utils.ts new file mode 100644 index 00000000000..f5b010c536b --- /dev/null +++ b/packages/amazonq/src/lsp/utils.ts @@ -0,0 +1,26 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { CursorState } from '@aws/language-server-runtimes-types' + +/** + * Convert from vscode selection type to the general CursorState expected by the AmazonQLSP. + * @param selection + * @returns + */ +export function getCursorState(selection: readonly vscode.Selection[]): CursorState[] { + return selection.map((s) => ({ + range: { + start: { + line: s.start.line, + character: s.start.character, + }, + end: { + line: s.end.line, + character: s.end.character, + }, + }, + })) +} diff --git a/packages/core/src/shared/settings-toolkit.gen.ts b/packages/core/src/shared/settings-toolkit.gen.ts index 59a637a4870..10020cf51f9 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -44,6 +44,7 @@ export const toolkitSettings = { "jsonResourceModification": {}, "amazonqLSP": {}, "amazonqLSPInline": {}, + "amazonqLSPInlineChat": {}, "amazonqChatLSP": {} }, "aws.resources.enabledResources": {}, diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index cffb69d0c1a..0e6e24a17ae 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -255,6 +255,10 @@ "type": "boolean", "default": false }, + "amazonqLSPInlineChat": { + "type": "boolean", + "default": false + }, "amazonqChatLSP": { "type": "boolean", "default": true From 86ceeba095a402bc053e23bf0d1587c57c773af4 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Wed, 7 May 2025 12:03:00 -0400 Subject: [PATCH 06/48] feat(amazonq): add experiment for basic e2e flow of inline chat through Flare. (#7235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem This is the initial set of work required to get inline chat running through flare. ## Solution - **add a feature flag for inline chat**: this allows testing of the two implementations side-by-side by flipping the feature flag. - **move general utils out of chat**: stuff like encryption and editorState can all be reused. - **render full diff response from inline chat**: this does not include progress updates from the language server. ## Testing and Verification https://github.com/user-attachments/assets/0dff58b7-40f7-487d-9f9e-d58610201041 ## Future Work / Next Steps - ensure telemetry is still being emitted. - add tests for new flow. (there aren't any for the existing one) - handle partial events from the language server. ## Known Bugs - selecting part of a line will cause the text to insert mid-line  - running inline-chat without a selection causes the entire file to be copied (This is in JB, Eclipse Prod, but IMO it makes the feature unusable). --- - 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. From aeca817ff7307117c4556ee17c7cbc9e9495e1ee Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Wed, 7 May 2025 15:20:42 -0400 Subject: [PATCH 07/48] refactor(amazonq): pull out all decryption logic to one place (#7248) ## Problem Follow up to https://github.com/aws/aws-toolkit-vscode/pull/7235#discussion_r2077892394. ## Solution - extract all decrypting and encrypting logic to a single location. - add a simple test for this logic (encrypt and decrypt are inverses). - refactor existing implementations. ## Verification Used agentic chat with some tools, as well as inline chat and didn't notice a difference. If encryption were broken, I would expect this to fail immediately. --- - 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. --- .../inlineChat/provider/inlineChatProvider.ts | 15 ++++----- packages/amazonq/src/lsp/chat/messages.ts | 31 ++++++------------- packages/amazonq/src/lsp/encryption.ts | 10 ++++-- .../test/unit/amazonq/lsp/encryption.test.ts | 27 ++++++++++++++++ 4 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index 86fe0ac2ade..cfa3798945c 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -28,7 +28,7 @@ import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' import { InlineTask } from '../controller/inlineTask' import { extractAuthFollowUp } from 'aws-core-vscode/amazonq' import { InlineChatParams, InlineChatResult } from '@aws/language-server-runtimes-types' -import { decodeRequest, encryptRequest } from '../../lsp/encryption' +import { decryptResponse, encryptRequest } from '../../lsp/encryption' import { getCursorState } from '../../lsp/utils' export class InlineChatProvider { @@ -72,16 +72,13 @@ export class InlineChatProvider { // TODO: handle partial responses. getLogger().info('Making inline chat request with message %O', message) const params = this.getCurrentEditorParams(message.message ?? '') + const inlineChatRequest = await encryptRequest(params, this.encryptionKey) const response = await this.client.sendRequest(inlineChatRequestType.method, inlineChatRequest) - const decryptedMessage = - typeof response === 'string' && this.encryptionKey - ? await decodeRequest(response, this.encryptionKey) - : response - const result: InlineChatResult = decryptedMessage as InlineChatResult - this.client.info(`Logging response for inline chat ${JSON.stringify(decryptedMessage)}`) - - return result + const inlineChatResponse = await decryptResponse(response, this.encryptionKey) + this.client.info(`Logging response for inline chat ${JSON.stringify(inlineChatResponse)}`) + + return inlineChatResponse } // TODO: remove in favor of LSP implementation. diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 93ede65fc9a..32390d52006 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -67,7 +67,7 @@ import { } from 'aws-core-vscode/amazonq' import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' -import { decodeRequest, encryptRequest } from '../encryption' +import { decryptResponse, encryptRequest } from '../encryption' import { getCursorState } from '../utils' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { @@ -205,21 +205,12 @@ export function registerMessageListeners( const cancellationToken = new CancellationTokenSource() chatStreamTokens.set(chatParams.tabId, cancellationToken) - const chatDisposable = languageClient.onProgress( - chatRequestType, - partialResultToken, - (partialResult) => { - // Store the latest partial result - if (typeof partialResult === 'string' && encryptionKey) { - void decodeRequest(partialResult, encryptionKey).then( - (decoded) => (lastPartialResult = decoded) - ) - } else { - lastPartialResult = partialResult as ChatResult + const chatDisposable = languageClient.onProgress(chatRequestType, partialResultToken, (partialResult) => + handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId).then( + (result) => { + lastPartialResult = result } - - void handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId) - } + ) ) const editor = @@ -482,10 +473,7 @@ async function handlePartialResult( provider: AmazonQChatViewProvider, tabId: string ) { - const decryptedMessage = - typeof partialResult === 'string' && encryptionKey - ? await decodeRequest(partialResult, encryptionKey) - : (partialResult as T) + const decryptedMessage = await decryptResponse(partialResult, encryptionKey) if (decryptedMessage.body !== undefined) { void provider.webview?.postMessage({ @@ -495,6 +483,7 @@ async function handlePartialResult( tabId: tabId, }) } + return decryptedMessage } /** @@ -508,8 +497,8 @@ async function handleCompleteResult( tabId: string, disposable: Disposable ) { - const decryptedMessage = - typeof result === 'string' && encryptionKey ? await decodeRequest(result, encryptionKey) : (result as T) + const decryptedMessage = await decryptResponse(result, encryptionKey) + void provider.webview?.postMessage({ command: chatRequestType.method, params: decryptedMessage, diff --git a/packages/amazonq/src/lsp/encryption.ts b/packages/amazonq/src/lsp/encryption.ts index 213ee3c1553..246c64f476b 100644 --- a/packages/amazonq/src/lsp/encryption.ts +++ b/packages/amazonq/src/lsp/encryption.ts @@ -14,8 +14,14 @@ export async function encryptRequest(params: T, encryptionKey: Buffer): Promi return { message: encryptedMessage } } -export async function decodeRequest(request: string, key: Buffer): Promise { - const result = await jose.jwtDecrypt(request, key, { +export async function decryptResponse(response: unknown, key: Buffer | undefined) { + // Note that casts are required since language client requests return 'unknown' type. + // If we can't decrypt, return original response casted. + if (typeof response !== 'string' || key === undefined) { + return response as T + } + + const result = await jose.jwtDecrypt(response, key, { clockTolerance: 60, // Allow up to 60 seconds to account for clock differences contentEncryptionAlgorithms: ['A256GCM'], keyManagementAlgorithms: ['dir'], diff --git a/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts b/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts new file mode 100644 index 00000000000..06a901edde6 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts @@ -0,0 +1,27 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { decryptResponse, encryptRequest } from '../../../../src/lsp/encryption' +import { encryptionKey } from '../../../../src/lsp/auth' + +describe('LSP encryption', function () { + it('encrypt and decrypt invert eachother with same key', async function () { + const key = encryptionKey + const request = { + id: 0, + name: 'my Request', + isRealRequest: false, + metadata: { + tags: ['tag1', 'tag2'], + }, + } + const encryptedPayload = await encryptRequest(request, key) + const message = (encryptedPayload as { message: string }).message + const decrypted = await decryptResponse(message, key) + + assert.deepStrictEqual(decrypted, request) + }) +}) From 858cad668d1f6f47e6692bb4cb20f50cecf0d00a Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Wed, 7 May 2025 21:37:48 -0400 Subject: [PATCH 08/48] telemetry(amazonq): align flare/vscode codewhisperer_serviceInvocation metric (#7244) ## Problem vscode keeps track of `codewhispererImportRecommendationEnabled` inside of the `codewhisperer_serviceInvocation` event but flare doesn't ## Solution add it before emitting telemetry, since this is purely a client side feature --- - 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 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 32390d52006..887c2ae4c72 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -56,7 +56,7 @@ import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' -import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' +import { AuthUtil, CodeWhispererSettings, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl } from 'aws-core-vscode/shared' import { DefaultAmazonQAppInitContext, @@ -95,6 +95,14 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie const telemetryName: string = e.name if (telemetryName in telemetry) { + switch (telemetryName) { + case 'codewhisperer_serviceInvocation': { + // this feature is entirely client side right now + e.data.codewhispererImportRecommendationEnabled = + CodeWhispererSettings.instance.isImportRecommendationEnabled() + break + } + } telemetry[telemetryName as keyof TelemetryBase].emit(e.data) } }) From bbf7b0ee2c7226cdf1db556ea069c5f20310e822 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Thu, 8 May 2025 12:17:02 -0400 Subject: [PATCH 09/48] feat(amazonq): re-add activeState/line trackers (#7257) ## Problem - "Amazon Q is generating ..." does not show with the lsp mode ## Solution - re-add the line tracker with tests - re-implement the activeState tracker ## Notes In master the active state tracker decides whether or not to show "Amazon Q is generating ..." by the following: - When a change is made, the auto trigger decides whether or not to start a recommendation request. When a recommendation requests eventually starts, an event is sent to the active state tracker to tell it to start showing the "Amazon Q is generating ..." message. When the first recommendation starts loading and the results are shown to the user another event is sent telling it to hide the message. It de-bounces this message showing every 1000ms so that the message is not constantly toggling on/off In this implementation its slightly different: - VSCode decides when to trigger the inline completion through their inline completion provider. From here we show the "Amazon Q is generating ... " message until the first recommendation is received from the language server and shown to the user. It still de-bounces this message every 1000ms so that users aren't shown it too often --- - 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/app/inline/completion.ts | 12 +- .../src/app/inline/recommendationService.ts | 10 +- .../inline/stateTracker/activeStateTracker.ts | 95 ++++++ .../app/inline/stateTracker/lineTracker.ts | 178 +++++++++++ .../amazonq/apps/inline/completion.test.ts | 6 +- .../amazonq/apps/inline/inlineTracker.test.ts | 299 ++++++++++++++++++ .../apps/inline/recommendationService.test.ts | 6 +- packages/core/src/shared/index.ts | 3 + 8 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 packages/amazonq/src/app/inline/stateTracker/activeStateTracker.ts create mode 100644 packages/amazonq/src/app/inline/stateTracker/lineTracker.ts create mode 100644 packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 6783658cd2d..d6be20dcd77 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -33,6 +33,8 @@ import { ImportAdderProvider, CodeSuggestionsState, } from 'aws-core-vscode/codewhisperer' +import { ActiveStateTracker } from './stateTracker/activeStateTracker' +import { LineTracker } from './stateTracker/lineTracker' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -40,12 +42,16 @@ export class InlineCompletionManager implements Disposable { private languageClient: LanguageClient private sessionManager: SessionManager private recommendationService: RecommendationService + private lineTracker: LineTracker + private activeStateTracker: ActiveStateTracker private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' constructor(languageClient: LanguageClient) { this.languageClient = languageClient this.sessionManager = new SessionManager() - this.recommendationService = new RecommendationService(this.sessionManager) + this.lineTracker = new LineTracker() + this.activeStateTracker = new ActiveStateTracker(this.lineTracker) + this.recommendationService = new RecommendationService(this.sessionManager, this.activeStateTracker) this.inlineCompletionProvider = new AmazonQInlineCompletionItemProvider( languageClient, this.recommendationService, @@ -55,11 +61,15 @@ export class InlineCompletionManager implements Disposable { CodeWhispererConstants.platformLanguageIds, this.inlineCompletionProvider ) + + this.lineTracker.ready() } public dispose(): void { if (this.disposable) { this.disposable.dispose() + this.activeStateTracker.dispose() + this.lineTracker.dispose() } } diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 45dd0099ebd..12e40e9c189 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -11,9 +11,13 @@ import { import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' +import { ActiveStateTracker } from './stateTracker/activeStateTracker' export class RecommendationService { - constructor(private readonly sessionManager: SessionManager) {} + constructor( + private readonly sessionManager: SessionManager, + private readonly activeStateTracker: ActiveStateTracker + ) {} async getAllRecommendations( languageClient: LanguageClient, @@ -31,6 +35,8 @@ export class RecommendationService { } const requestStartTime = Date.now() + await this.activeStateTracker.showGenerating(context.triggerKind) + // Handle first request const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( inlineCompletionWithReferencesRequestType as any, @@ -54,6 +60,8 @@ export class RecommendationService { } else { this.sessionManager.closeSession() } + + this.activeStateTracker.hideGenerating() } private async processRemainingRequests( diff --git a/packages/amazonq/src/app/inline/stateTracker/activeStateTracker.ts b/packages/amazonq/src/app/inline/stateTracker/activeStateTracker.ts new file mode 100644 index 00000000000..837babe99e0 --- /dev/null +++ b/packages/amazonq/src/app/inline/stateTracker/activeStateTracker.ts @@ -0,0 +1,95 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { editorUtilities } from 'aws-core-vscode/shared' +import * as vscode from 'vscode' +import { LineSelection, LineTracker } from './lineTracker' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' +import { cancellableDebounce } from 'aws-core-vscode/utils' + +export class ActiveStateTracker implements vscode.Disposable { + private readonly _disposable: vscode.Disposable + + private readonly cwLineHintDecoration: vscode.TextEditorDecorationType = + vscode.window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 3em', + contentText: 'Amazon Q is generating...', + textDecoration: 'none', + fontWeight: 'normal', + fontStyle: 'normal', + color: 'var(--vscode-editorCodeLens-foreground)', + }, + rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen, + isWholeLine: true, + }) + + constructor(private readonly lineTracker: LineTracker) { + this._disposable = vscode.Disposable.from( + AuthUtil.instance.auth.onDidChangeConnectionState(async (e) => { + if (e.state !== 'authenticating') { + this.hideGenerating() + } + }), + AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(async () => { + this.hideGenerating() + }) + ) + } + + dispose() { + this._disposable.dispose() + } + + readonly refreshDebounced = cancellableDebounce(async () => { + await this._refresh(true) + }, 1000) + + async showGenerating(triggerType: vscode.InlineCompletionTriggerKind) { + if (triggerType === vscode.InlineCompletionTriggerKind.Invoke) { + // if user triggers on demand, immediately update the UI and cancel the previous debounced update if there is one + this.refreshDebounced.cancel() + await this._refresh(true) + } else { + await this.refreshDebounced.promise() + } + } + + async _refresh(shouldDisplay: boolean) { + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + + const selections = this.lineTracker.selections + if (!editor || !selections || !editorUtilities.isTextEditor(editor)) { + this.hideGenerating() + return + } + + if (!AuthUtil.instance.isConnectionValid()) { + this.hideGenerating() + return + } + + await this.updateDecorations(editor, selections, shouldDisplay) + } + + hideGenerating() { + vscode.window.activeTextEditor?.setDecorations(this.cwLineHintDecoration, []) + } + + async updateDecorations(editor: vscode.TextEditor, lines: LineSelection[], shouldDisplay: boolean) { + const range = editor.document.validateRange( + new vscode.Range(lines[0].active, lines[0].active, lines[0].active, lines[0].active) + ) + + if (shouldDisplay) { + editor.setDecorations(this.cwLineHintDecoration, [range]) + } else { + editor.setDecorations(this.cwLineHintDecoration, []) + } + } +} diff --git a/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts b/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts new file mode 100644 index 00000000000..58bee329a40 --- /dev/null +++ b/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts @@ -0,0 +1,178 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { editorUtilities, setContext } from 'aws-core-vscode/shared' + +export interface LineSelection { + anchor: number + active: number +} + +export interface LinesChangeEvent { + readonly editor: vscode.TextEditor | undefined + readonly selections: LineSelection[] | undefined + + readonly reason: 'editor' | 'selection' | 'content' +} + +/** + * This class providees a single interface to manage and access users' "line" selections + * Callers could use it by subscribing onDidChangeActiveLines to do UI updates or logic needed to be executed when line selections get changed + */ +export class LineTracker implements vscode.Disposable { + private _onDidChangeActiveLines = new vscode.EventEmitter() + get onDidChangeActiveLines(): vscode.Event { + return this._onDidChangeActiveLines.event + } + + private _editor: vscode.TextEditor | undefined + private _disposable: vscode.Disposable | undefined + + private _selections: LineSelection[] | undefined + get selections(): LineSelection[] | undefined { + return this._selections + } + + private _onReady: vscode.EventEmitter = new vscode.EventEmitter() + get onReady(): vscode.Event { + return this._onReady.event + } + + private _ready: boolean = false + get isReady() { + return this._ready + } + + constructor() { + this._disposable = vscode.Disposable.from( + vscode.window.onDidChangeActiveTextEditor(async (e) => { + await this.onActiveTextEditorChanged(e) + }), + vscode.window.onDidChangeTextEditorSelection(async (e) => { + await this.onTextEditorSelectionChanged(e) + }), + vscode.workspace.onDidChangeTextDocument((e) => { + this.onContentChanged(e) + }) + ) + + queueMicrotask(async () => await this.onActiveTextEditorChanged(vscode.window.activeTextEditor)) + } + + dispose() { + this._disposable?.dispose() + } + + ready() { + if (this._ready) { + throw new Error('Linetracker is already activated') + } + + this._ready = true + queueMicrotask(() => this._onReady.fire()) + } + + // @VisibleForTesting + async onActiveTextEditorChanged(editor: vscode.TextEditor | undefined) { + if (editor === this._editor) { + return + } + + this._editor = editor + this._selections = toLineSelections(editor?.selections) + if (this._selections && this._selections[0]) { + const s = this._selections.map((item) => item.active + 1) + await setContext('codewhisperer.activeLine', s) + } + + this.notifyLinesChanged('editor') + } + + // @VisibleForTesting + async onTextEditorSelectionChanged(e: vscode.TextEditorSelectionChangeEvent) { + // If this isn't for our cached editor and its not a real editor -- kick out + if (this._editor !== e.textEditor && !editorUtilities.isTextEditor(e.textEditor)) { + return + } + + const selections = toLineSelections(e.selections) + if (this._editor === e.textEditor && this.includes(selections)) { + return + } + + this._editor = e.textEditor + this._selections = selections + if (this._selections && this._selections[0]) { + const s = this._selections.map((item) => item.active + 1) + await setContext('codewhisperer.activeLine', s) + } + + this.notifyLinesChanged('selection') + } + + // @VisibleForTesting + onContentChanged(e: vscode.TextDocumentChangeEvent) { + const editor = vscode.window.activeTextEditor + if (e.document === editor?.document && e.contentChanges.length > 0 && editorUtilities.isTextEditor(editor)) { + this._editor = editor + this._selections = toLineSelections(this._editor?.selections) + + this.notifyLinesChanged('content') + } + } + + notifyLinesChanged(reason: 'editor' | 'selection' | 'content') { + const e: LinesChangeEvent = { editor: this._editor, selections: this.selections, reason: reason } + this._onDidChangeActiveLines.fire(e) + } + + includes(selections: LineSelection[]): boolean + includes(line: number, options?: { activeOnly: boolean }): boolean + includes(lineOrSelections: number | LineSelection[], options?: { activeOnly: boolean }): boolean { + if (typeof lineOrSelections !== 'number') { + return isIncluded(lineOrSelections, this._selections) + } + + if (this._selections === undefined || this._selections.length === 0) { + return false + } + + const line = lineOrSelections + const activeOnly = options?.activeOnly ?? true + + for (const selection of this._selections) { + if ( + line === selection.active || + (!activeOnly && + ((selection.anchor >= line && line >= selection.active) || + (selection.active >= line && line >= selection.anchor))) + ) { + return true + } + } + return false + } +} + +function isIncluded(selections: LineSelection[] | undefined, within: LineSelection[] | undefined): boolean { + if (selections === undefined && within === undefined) { + return true + } + if (selections === undefined || within === undefined || selections.length !== within.length) { + return false + } + + return selections.every((s, i) => { + const match = within[i] + return s.active === match.active && s.anchor === match.anchor + }) +} + +function toLineSelections(selections: readonly vscode.Selection[]): LineSelection[] +function toLineSelections(selections: readonly vscode.Selection[] | undefined): LineSelection[] | undefined +function toLineSelections(selections: readonly vscode.Selection[] | undefined) { + return selections?.map((s) => ({ active: s.active.line, anchor: s.anchor.line })) +} diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index d2182329e45..89b259c656f 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -15,6 +15,8 @@ import { ReferenceInlineProvider, ReferenceLogViewProvider, } from 'aws-core-vscode/codewhisperer' +import { ActiveStateTracker } from '../../../../../src/app/inline/stateTracker/activeStateTracker' +import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' describe('InlineCompletionManager', () => { let manager: InlineCompletionManager @@ -264,7 +266,9 @@ describe('InlineCompletionManager', () => { let setInlineReferenceStub: sinon.SinonStub beforeEach(() => { - recommendationService = new RecommendationService(mockSessionManager) + const lineTracker = new LineTracker() + const activeStateController = new ActiveStateTracker(lineTracker) + recommendationService = new RecommendationService(mockSessionManager, activeStateController) setInlineReferenceStub = sandbox.stub(ReferenceInlineProvider.instance, 'setInlineReference') mockSessionManager = { diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts new file mode 100644 index 00000000000..6b9490c72a5 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts @@ -0,0 +1,299 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LineSelection, LineTracker, AuthUtil } from 'aws-core-vscode/codewhisperer' +import sinon from 'sinon' +import { Disposable, TextEditor, Position, Range, Selection } from 'vscode' +import { toTextEditor } from 'aws-core-vscode/test' +import assert from 'assert' +import { waitUntil } from 'aws-core-vscode/shared' + +describe('LineTracker class', function () { + let sut: LineTracker + let disposable: Disposable + let editor: TextEditor + let sandbox: sinon.SinonSandbox + let counts = { + editor: 0, + selection: 0, + content: 0, + } + + beforeEach(async function () { + sut = new LineTracker() + sandbox = sinon.createSandbox() + counts = { + editor: 0, + selection: 0, + content: 0, + } + disposable = sut.onDidChangeActiveLines((e) => { + if (e.reason === 'content') { + counts.content++ + } else if (e.reason === 'selection') { + counts.selection++ + } else if (e.reason === 'editor') { + counts.editor++ + } + }) + + sandbox.stub(AuthUtil.instance, 'isConnected').returns(true) + sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) + }) + + afterEach(function () { + disposable.dispose() + sut.dispose() + sandbox.restore() + }) + + function assertEmptyCounts() { + assert.deepStrictEqual(counts, { + editor: 0, + selection: 0, + content: 0, + }) + } + + it('ready will emit onReady event', async function () { + let messageReceived = 0 + disposable = sut.onReady((_) => { + messageReceived++ + }) + + assert.strictEqual(sut.isReady, false) + sut.ready() + + await waitUntil( + async () => { + if (messageReceived !== 0) { + return + } + }, + { interval: 1000 } + ) + + assert.strictEqual(sut.isReady, true) + assert.strictEqual(messageReceived, 1) + }) + + describe('includes', function () { + // util function to help set up LineTracker.selections + async function setEditorSelection(selections: LineSelection[]): Promise { + const editor = await toTextEditor('\n\n\n\n\n\n\n\n\n\n', 'foo.py', undefined, { + preview: false, + }) + + const vscodeSelections = selections.map((s) => { + return new Selection(new Position(s.anchor, 0), new Position(s.active, 0)) + }) + + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: vscodeSelections, + kind: undefined, + }) + + assert.deepStrictEqual(sut.selections, selections) + return editor + } + + it('exact match when array of selections are provided', async function () { + const selections = [ + { + anchor: 1, + active: 1, + }, + { + anchor: 3, + active: 3, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + let actual = sut.includes([ + { active: 1, anchor: 1 }, + { active: 3, anchor: 3 }, + ]) + assert.strictEqual(actual, true) + + actual = sut.includes([ + { active: 2, anchor: 2 }, + { active: 4, anchor: 4 }, + ]) + assert.strictEqual(actual, false) + + // both active && anchor have to be the same + actual = sut.includes([ + { active: 1, anchor: 0 }, + { active: 3, anchor: 0 }, + ]) + assert.strictEqual(actual, false) + + // different length would simply return false + actual = sut.includes([ + { active: 1, anchor: 1 }, + { active: 3, anchor: 3 }, + { active: 5, anchor: 5 }, + ]) + assert.strictEqual(actual, false) + }) + + it('match active line if line number and activeOnly option are provided', async function () { + const selections = [ + { + anchor: 1, + active: 1, + }, + { + anchor: 3, + active: 3, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + let actual = sut.includes(1, { activeOnly: true }) + assert.strictEqual(actual, true) + + actual = sut.includes(2, { activeOnly: true }) + assert.strictEqual(actual, false) + }) + + it('range match if line number and activeOnly is set to false', async function () { + const selections = [ + { + anchor: 0, + active: 2, + }, + { + anchor: 4, + active: 6, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + for (const line of [0, 1, 2]) { + const actual = sut.includes(line, { activeOnly: false }) + assert.strictEqual(actual, true) + } + + for (const line of [4, 5, 6]) { + const actual = sut.includes(line, { activeOnly: false }) + assert.strictEqual(actual, true) + } + + let actual = sut.includes(3, { activeOnly: false }) + assert.strictEqual(actual, false) + + actual = sut.includes(7, { activeOnly: false }) + assert.strictEqual(actual, false) + }) + }) + + describe('onContentChanged', function () { + it('should fire lineChangedEvent and set current line selection', async function () { + editor = await toTextEditor('\n\n\n\n\n', 'foo.py', undefined, { preview: false }) + editor.selection = new Selection(new Position(5, 0), new Position(5, 0)) + assertEmptyCounts() + + sut.onContentChanged({ + document: editor.document, + contentChanges: [{ text: 'a', range: new Range(0, 0, 0, 0), rangeOffset: 0, rangeLength: 0 }], + reason: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, content: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 5, + active: 5, + }, + ]) + }) + }) + + describe('onTextEditorSelectionChanged', function () { + it('should fire lineChangedEvent if selection changes and set current line selection', async function () { + editor = await toTextEditor('\n\n\n\n\n', 'foo.py', undefined, { preview: false }) + editor.selection = new Selection(new Position(3, 0), new Position(3, 0)) + assertEmptyCounts() + + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, selection: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 3, + active: 3, + }, + ]) + + // if selection is included in the existing selections, won't emit an event + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, selection: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 3, + active: 3, + }, + ]) + }) + + it('should not fire lineChangedEvent if uri scheme is debug || output', async function () { + // if the editor is not a text editor, won't emit an event and selection will be set to undefined + async function assertLineChanged(schema: string) { + const anotherEditor = await toTextEditor('', 'bar.log', undefined, { preview: false }) + const uri = anotherEditor.document.uri + sandbox.stub(uri, 'scheme').get(() => schema) + + await sut.onTextEditorSelectionChanged({ + textEditor: anotherEditor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts }) + } + + await assertLineChanged('debug') + await assertLineChanged('output') + }) + }) + + describe('onActiveTextEditorChanged', function () { + it('shoudl fire lineChangedEvent', async function () { + const selections: Selection[] = [new Selection(0, 0, 1, 1)] + + editor = { selections: selections } as any + + assertEmptyCounts() + + await sut.onActiveTextEditorChanged(editor) + + assert.deepStrictEqual(counts, { ...counts, editor: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 0, + active: 1, + }, + ]) + }) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index b3628e22c35..1aa7028d1d9 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -10,6 +10,8 @@ import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' import { createMockDocument } from 'aws-core-vscode/test' +import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' +import { ActiveStateTracker } from '../../../../../src/app/inline/stateTracker/activeStateTracker' describe('RecommendationService', () => { let languageClient: LanguageClient @@ -28,7 +30,9 @@ describe('RecommendationService', () => { } as InlineCompletionItem const mockPartialResultToken = 'some-random-token' const sessionManager = new SessionManager() - const service = new RecommendationService(sessionManager) + const lineTracker = new LineTracker() + const activeStateController = new ActiveStateTracker(lineTracker) + const service = new RecommendationService(sessionManager, activeStateController) beforeEach(() => { sandbox = sinon.createSandbox() diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 4cda5285f69..2d23400c9bb 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -49,6 +49,9 @@ export * as env from './vscode/env' export * from './vscode/commands2' export * from './utilities/pathUtils' export * from './utilities/zipStream' +export * as editorUtilities from './utilities/editorUtilities' +export * as functionUtilities from './utilities/functionUtils' +export * as vscodeUtilities from './utilities/vsCodeUtils' export * from './errors' export * as messages from './utilities/messages' export * as errors from './errors' From c77668d4bb5096d2d4c05c01f9c3b6a666f5adc3 Mon Sep 17 00:00:00 2001 From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Date: Thu, 8 May 2025 16:41:27 -0400 Subject: [PATCH 10/48] refactor(amazonq): rename inline "generating" message class (#7267) This new name more accurately represents what this class is for. It is just a util to create the "Amazon Q generating" inline message. - Class is renamed - File is renamed and move out of the "stateTracker" folder --- - 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. Signed-off-by: nkomonen-amazon --- packages/amazonq/src/app/inline/completion.ts | 6 +++--- .../activeStateTracker.ts => inlineGeneratingMessage.ts} | 7 +++++-- packages/amazonq/src/app/inline/recommendationService.ts | 8 ++++---- .../test/unit/amazonq/apps/inline/completion.test.ts | 4 ++-- .../amazonq/apps/inline/recommendationService.test.ts | 4 ++-- 5 files changed, 16 insertions(+), 13 deletions(-) rename packages/amazonq/src/app/inline/{stateTracker/activeStateTracker.ts => inlineGeneratingMessage.ts} (93%) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index d6be20dcd77..9867fd2dd8c 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -33,7 +33,7 @@ import { ImportAdderProvider, CodeSuggestionsState, } from 'aws-core-vscode/codewhisperer' -import { ActiveStateTracker } from './stateTracker/activeStateTracker' +import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' export class InlineCompletionManager implements Disposable { @@ -43,14 +43,14 @@ export class InlineCompletionManager implements Disposable { private sessionManager: SessionManager private recommendationService: RecommendationService private lineTracker: LineTracker - private activeStateTracker: ActiveStateTracker + private activeStateTracker: InlineGeneratingMessage private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' constructor(languageClient: LanguageClient) { this.languageClient = languageClient this.sessionManager = new SessionManager() this.lineTracker = new LineTracker() - this.activeStateTracker = new ActiveStateTracker(this.lineTracker) + this.activeStateTracker = new InlineGeneratingMessage(this.lineTracker) this.recommendationService = new RecommendationService(this.sessionManager, this.activeStateTracker) this.inlineCompletionProvider = new AmazonQInlineCompletionItemProvider( languageClient, diff --git a/packages/amazonq/src/app/inline/stateTracker/activeStateTracker.ts b/packages/amazonq/src/app/inline/inlineGeneratingMessage.ts similarity index 93% rename from packages/amazonq/src/app/inline/stateTracker/activeStateTracker.ts rename to packages/amazonq/src/app/inline/inlineGeneratingMessage.ts index 837babe99e0..6c2d97fdad2 100644 --- a/packages/amazonq/src/app/inline/stateTracker/activeStateTracker.ts +++ b/packages/amazonq/src/app/inline/inlineGeneratingMessage.ts @@ -5,11 +5,14 @@ import { editorUtilities } from 'aws-core-vscode/shared' import * as vscode from 'vscode' -import { LineSelection, LineTracker } from './lineTracker' +import { LineSelection, LineTracker } from './stateTracker/lineTracker' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { cancellableDebounce } from 'aws-core-vscode/utils' -export class ActiveStateTracker implements vscode.Disposable { +/** + * Manages the inline ghost text message show when Inline Suggestions is "thinking". + */ +export class InlineGeneratingMessage implements vscode.Disposable { private readonly _disposable: vscode.Disposable private readonly cwLineHintDecoration: vscode.TextEditorDecorationType = diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 12e40e9c189..6f0c50d37e2 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -11,12 +11,12 @@ import { import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' -import { ActiveStateTracker } from './stateTracker/activeStateTracker' +import { InlineGeneratingMessage } from './inlineGeneratingMessage' export class RecommendationService { constructor( private readonly sessionManager: SessionManager, - private readonly activeStateTracker: ActiveStateTracker + private readonly inlineGeneratingMessage: InlineGeneratingMessage ) {} async getAllRecommendations( @@ -35,7 +35,7 @@ export class RecommendationService { } const requestStartTime = Date.now() - await this.activeStateTracker.showGenerating(context.triggerKind) + await this.inlineGeneratingMessage.showGenerating(context.triggerKind) // Handle first request const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( @@ -61,7 +61,7 @@ export class RecommendationService { this.sessionManager.closeSession() } - this.activeStateTracker.hideGenerating() + this.inlineGeneratingMessage.hideGenerating() } private async processRemainingRequests( diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 89b259c656f..9fa62163bfe 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -15,7 +15,7 @@ import { ReferenceInlineProvider, ReferenceLogViewProvider, } from 'aws-core-vscode/codewhisperer' -import { ActiveStateTracker } from '../../../../../src/app/inline/stateTracker/activeStateTracker' +import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' describe('InlineCompletionManager', () => { @@ -267,7 +267,7 @@ describe('InlineCompletionManager', () => { beforeEach(() => { const lineTracker = new LineTracker() - const activeStateController = new ActiveStateTracker(lineTracker) + const activeStateController = new InlineGeneratingMessage(lineTracker) recommendationService = new RecommendationService(mockSessionManager, activeStateController) setInlineReferenceStub = sandbox.stub(ReferenceInlineProvider.instance, 'setInlineReference') diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index 1aa7028d1d9..3b894b47b71 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -11,7 +11,7 @@ import { RecommendationService } from '../../../../../src/app/inline/recommendat import { SessionManager } from '../../../../../src/app/inline/sessionManager' import { createMockDocument } from 'aws-core-vscode/test' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' -import { ActiveStateTracker } from '../../../../../src/app/inline/stateTracker/activeStateTracker' +import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' describe('RecommendationService', () => { let languageClient: LanguageClient @@ -31,7 +31,7 @@ describe('RecommendationService', () => { const mockPartialResultToken = 'some-random-token' const sessionManager = new SessionManager() const lineTracker = new LineTracker() - const activeStateController = new ActiveStateTracker(lineTracker) + const activeStateController = new InlineGeneratingMessage(lineTracker) const service = new RecommendationService(sessionManager, activeStateController) beforeEach(() => { From 4eac9085ba8db3ab4b2e9f6da4219949a3c2dddf Mon Sep 17 00:00:00 2001 From: nkomonen-amazon Date: Thu, 8 May 2025 16:40:56 -0400 Subject: [PATCH 11/48] fix: Ensure we clear the "generating" inline message when done I saw while testing that the "Amazon Q is generating..." got stuck at some point. I think this fix should avoid that Signed-off-by: nkomonen-amazon --- .../src/app/inline/recommendationService.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 6f0c50d37e2..71b2e3368cb 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -35,33 +35,37 @@ export class RecommendationService { } const requestStartTime = Date.now() - await this.inlineGeneratingMessage.showGenerating(context.triggerKind) + try { + // Show UI indicators that we are generating suggestions + await this.inlineGeneratingMessage.showGenerating(context.triggerKind) - // Handle first request - const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType as any, - request, - token - ) + // Handle first request + const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( + inlineCompletionWithReferencesRequestType as any, + request, + token + ) - const firstCompletionDisplayLatency = Date.now() - requestStartTime - this.sessionManager.startSession( - firstResult.sessionId, - firstResult.items, - requestStartTime, - firstCompletionDisplayLatency - ) + const firstCompletionDisplayLatency = Date.now() - requestStartTime + this.sessionManager.startSession( + firstResult.sessionId, + firstResult.items, + requestStartTime, + firstCompletionDisplayLatency + ) - if (firstResult.partialResultToken) { - // If there are more results to fetch, handle them in the background - this.processRemainingRequests(languageClient, request, firstResult, token).catch((error) => { - languageClient.warn(`Error when getting suggestions: ${error}`) - }) - } else { - this.sessionManager.closeSession() + if (firstResult.partialResultToken) { + // If there are more results to fetch, handle them in the background + this.processRemainingRequests(languageClient, request, firstResult, token).catch((error) => { + languageClient.warn(`Error when getting suggestions: ${error}`) + }) + } else { + this.sessionManager.closeSession() + } + } finally { + // Remove all UI indicators of message generation since we are done + this.inlineGeneratingMessage.hideGenerating() } - - this.inlineGeneratingMessage.hideGenerating() } private async processRemainingRequests( From dd3b8299ebc28fccb63e822c569d528c9caa9917 Mon Sep 17 00:00:00 2001 From: nkomonen-amazon Date: Thu, 8 May 2025 17:25:59 -0400 Subject: [PATCH 12/48] fix: show spinning icon on status bar when generating suggestion This was a regression that appeared while doing the port to flare, now we will show the spinning symbol when generating a suggestion. Additionally the file was more appropriately named since it now only has the status bar related code. Signed-off-by: nkomonen-amazon --- .../src/app/inline/recommendationService.ts | 4 ++++ packages/core/src/codewhisperer/activation.ts | 2 +- packages/core/src/codewhisperer/index.ts | 2 +- ...nlineCompletionService.ts => statusBar.ts} | 21 ++++++++++++++----- .../commands/basicCommands.test.ts | 2 +- 5 files changed, 23 insertions(+), 8 deletions(-) rename packages/core/src/codewhisperer/service/{inlineCompletionService.ts => statusBar.ts} (88%) diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 71b2e3368cb..c9d84ef7642 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -12,6 +12,7 @@ import { CancellationToken, InlineCompletionContext, Position, TextDocument } fr import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' import { InlineGeneratingMessage } from './inlineGeneratingMessage' +import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' export class RecommendationService { constructor( @@ -34,10 +35,12 @@ export class RecommendationService { context, } const requestStartTime = Date.now() + const statusBar = CodeWhispererStatusBarManager.instance try { // Show UI indicators that we are generating suggestions await this.inlineGeneratingMessage.showGenerating(context.triggerKind) + await statusBar.setLoading() // Handle first request const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( @@ -65,6 +68,7 @@ export class RecommendationService { } finally { // Remove all UI indicators of message generation since we are done this.inlineGeneratingMessage.hideGenerating() + void statusBar.refreshStatusBar() // effectively "stop loading" } } diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 83875b9fe58..d6dd7fdc61d 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -62,7 +62,7 @@ import { } from './service/diagnosticsProvider' import { SecurityPanelViewProvider, openEditorAtRange } from './views/securityPanelViewProvider' import { Commands, registerCommandErrorHandler, registerDeclaredCommands } from '../shared/vscode/commands2' -import { refreshStatusBar } from './service/inlineCompletionService' +import { refreshStatusBar } from './service/statusBar' import { AuthUtil } from './util/authUtil' import { ImportAdderProvider } from './service/importAdderProvider' import { openUrl } from '../shared/utilities/vsCodeUtils' diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 05813444bd3..d782b2abefe 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -36,7 +36,7 @@ export { codeWhispererClient, } from './client/codewhisperer' export { listCodeWhispererCommands, listCodeWhispererCommandsId } from './ui/statusBarMenu' -export { refreshStatusBar, CodeWhispererStatusBar } from './service/inlineCompletionService' +export { refreshStatusBar, CodeWhispererStatusBarManager } from './service/statusBar' export { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' export { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' export { diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/statusBar.ts similarity index 88% rename from packages/core/src/codewhisperer/service/inlineCompletionService.ts rename to packages/core/src/codewhisperer/service/statusBar.ts index 18c8f0595aa..6aacfec73b7 100644 --- a/packages/core/src/codewhisperer/service/inlineCompletionService.ts +++ b/packages/core/src/codewhisperer/service/statusBar.ts @@ -10,7 +10,7 @@ import { codicon, getIcon } from '../../shared/icons' import { Commands } from '../../shared/vscode/commands2' import { listCodeWhispererCommandsId } from '../ui/statusBarMenu' -export class InlineCompletionService { +export class CodeWhispererStatusBarManager { private statusBar: CodeWhispererStatusBar constructor(statusBar: CodeWhispererStatusBar = CodeWhispererStatusBar.instance) { @@ -21,7 +21,7 @@ export class InlineCompletionService { }) } - static #instance: InlineCompletionService + static #instance: CodeWhispererStatusBarManager public static get instance() { return (this.#instance ??= new this()) @@ -41,6 +41,17 @@ export class InlineCompletionService { } } + /** + * Sets the status bar in to a "loading state", effectively showing + * the spinning circle. + * + * When loading is done, call {@link refreshStatusBar} to update the + * status bar to the latest state. + */ + async setLoading(): Promise { + await this.setState('loading') + } + private async setState(state: keyof typeof states) { switch (state) { case 'loading': { @@ -76,7 +87,7 @@ const states = { needsProfile: 'needsProfile', } as const -export class CodeWhispererStatusBar { +class CodeWhispererStatusBar { protected statusBar: vscode.StatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) static #instance: CodeWhispererStatusBar @@ -127,10 +138,10 @@ export class CodeWhispererStatusBar { } } -/** In this module due to circulare dependency issues */ +/** In this module due to circular dependency issues */ export const refreshStatusBar = Commands.declare( { id: 'aws.amazonq.refreshStatusBar', logging: false }, () => async () => { - await InlineCompletionService.instance.refreshStatusBar() + await CodeWhispererStatusBarManager.instance.refreshStatusBar() } ) diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 01c7c43c947..97d445a75c1 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -56,7 +56,7 @@ import { waitUntil } from '../../../shared/utilities/timeoutUtils' import { listCodeWhispererCommands } from '../../../codewhisperer/ui/statusBarMenu' import { CodeScanIssue, CodeScansState, CodeSuggestionsState, codeScanState } from '../../../codewhisperer/models/model' import { cwQuickPickSource } from '../../../codewhisperer/commands/types' -import { refreshStatusBar } from '../../../codewhisperer/service/inlineCompletionService' +import { refreshStatusBar } from '../../../codewhisperer/service/statusBar' import { focusAmazonQPanel } from '../../../codewhispererChat/commands/registerCommands' import * as diagnosticsProvider from '../../../codewhisperer/service/diagnosticsProvider' import { randomUUID } from '../../../shared/crypto' From 0d2c7fb82d95dc8f3a5e7dde8851ebd853975728 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Thu, 8 May 2025 20:48:08 -0400 Subject: [PATCH 13/48] feat(amazonq): add tutorial trackers for lsp (#7264) ## Problem the tutorial trackers aren't implemented when using the language server ## Solution - re-add the inlineLineAnnotationController (inlineChatTutorialAnnotation) for adding hints with inline chat - re-add the lineAnnotationController (inlineTutorialAnnotation) for adding the inline suggestions tutorial ## Notes in a future PR I'll fully deprecate the old trackers --- - 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/app/inline/completion.ts | 31 +- .../amazonq/src/app/inline/sessionManager.ts | 10 + .../inlineChatTutorialAnnotation.ts} | 15 +- .../tutorials/inlineTutorialAnnotation.ts | 526 ++++++++++++++++++ packages/amazonq/src/inlineChat/activation.ts | 10 +- .../controller/inlineChatController.ts | 19 +- packages/amazonq/src/lsp/client.ts | 73 ++- .../amazonq/apps/inline/completion.test.ts | 13 +- .../views/lineAnnotationController.ts | 23 +- 9 files changed, 668 insertions(+), 52 deletions(-) rename packages/amazonq/src/{inlineChat/decorations/inlineLineAnnotationController.ts => app/inline/tutorials/inlineChatTutorialAnnotation.ts} (72%) create mode 100644 packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 9867fd2dd8c..c1c950a4d89 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -35,6 +35,7 @@ import { } from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' +import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -43,19 +44,27 @@ export class InlineCompletionManager implements Disposable { private sessionManager: SessionManager private recommendationService: RecommendationService private lineTracker: LineTracker - private activeStateTracker: InlineGeneratingMessage + private incomingGeneratingMessage: InlineGeneratingMessage + private inlineTutorialAnnotation: InlineTutorialAnnotation private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' - constructor(languageClient: LanguageClient) { + constructor( + languageClient: LanguageClient, + sessionManager: SessionManager, + lineTracker: LineTracker, + inlineTutorialAnnotation: InlineTutorialAnnotation + ) { this.languageClient = languageClient - this.sessionManager = new SessionManager() - this.lineTracker = new LineTracker() - this.activeStateTracker = new InlineGeneratingMessage(this.lineTracker) - this.recommendationService = new RecommendationService(this.sessionManager, this.activeStateTracker) + this.sessionManager = sessionManager + this.lineTracker = lineTracker + this.incomingGeneratingMessage = new InlineGeneratingMessage(this.lineTracker) + this.recommendationService = new RecommendationService(this.sessionManager, this.incomingGeneratingMessage) + this.inlineTutorialAnnotation = inlineTutorialAnnotation this.inlineCompletionProvider = new AmazonQInlineCompletionItemProvider( languageClient, this.recommendationService, - this.sessionManager + this.sessionManager, + this.inlineTutorialAnnotation ) this.disposable = languages.registerInlineCompletionItemProvider( CodeWhispererConstants.platformLanguageIds, @@ -68,7 +77,7 @@ export class InlineCompletionManager implements Disposable { public dispose(): void { if (this.disposable) { this.disposable.dispose() - this.activeStateTracker.dispose() + this.incomingGeneratingMessage.dispose() this.lineTracker.dispose() } } @@ -113,6 +122,7 @@ export class InlineCompletionManager implements Disposable { if (item.mostRelevantMissingImports?.length) { await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine) } + this.sessionManager.incrementSuggestionCount() } commands.registerCommand('aws.amazonq.acceptInline', onInlineAcceptance) @@ -157,6 +167,7 @@ export class InlineCompletionManager implements Disposable { this.languageClient, this.recommendationService, this.sessionManager, + this.inlineTutorialAnnotation, false ) ) @@ -182,6 +193,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem private readonly languageClient: LanguageClient, private readonly recommendationService: RecommendationService, private readonly sessionManager: SessionManager, + private readonly inlineTutorialAnnotation: InlineTutorialAnnotation, private readonly isNewSession: boolean = true ) {} @@ -198,6 +210,9 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem return [] } + // tell the tutorial that completions has been triggered + await this.inlineTutorialAnnotation.triggered(context.triggerKind) + // make service requests if it's a new session await this.recommendationService.getAllRecommendations( this.languageClient, diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 4b70a684001..8286bc89459 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -18,6 +18,8 @@ interface CodeWhispererSession { export class SessionManager { private activeSession?: CodeWhispererSession private activeIndex: number = 0 + private _acceptedSuggestionCount: number = 0 + constructor() {} public startSession( @@ -95,6 +97,14 @@ export class SessionManager { return items } + public get acceptedSuggestionCount(): number { + return this._acceptedSuggestionCount + } + + public incrementSuggestionCount() { + this._acceptedSuggestionCount += 1 + } + public clear() { this.activeSession = undefined this.activeIndex = 0 diff --git a/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts b/packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts similarity index 72% rename from packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts rename to packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts index 9ec5e08122d..1208b4766af 100644 --- a/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts +++ b/packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts @@ -3,14 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Container } from 'aws-core-vscode/codewhisperer' import * as vscode from 'vscode' +import { InlineTutorialAnnotation } from './inlineTutorialAnnotation' +import { globals } from 'aws-core-vscode/shared' -export class InlineLineAnnotationController { +export class InlineChatTutorialAnnotation { private enabled: boolean = true - constructor(context: vscode.ExtensionContext) { - context.subscriptions.push( + constructor(private readonly inlineTutorialAnnotation: InlineTutorialAnnotation) { + globals.context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection(async ({ selections, textEditor }) => { let showShow = false @@ -33,12 +34,12 @@ export class InlineLineAnnotationController { private async setVisible(editor: vscode.TextEditor, visible: boolean) { let needsRefresh: boolean if (visible) { - needsRefresh = await Container.instance.lineAnnotationController.tryShowInlineHint() + needsRefresh = await this.inlineTutorialAnnotation.tryShowInlineHint() } else { - needsRefresh = await Container.instance.lineAnnotationController.tryHideInlineHint() + needsRefresh = await this.inlineTutorialAnnotation.tryHideInlineHint() } if (needsRefresh) { - await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') + await this.inlineTutorialAnnotation.refresh(editor, 'codewhisperer') } } diff --git a/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts new file mode 100644 index 00000000000..bd12b1d28dd --- /dev/null +++ b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts @@ -0,0 +1,526 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as os from 'os' +import { + AnnotationChangeSource, + AuthUtil, + inlinehintKey, + runtimeLanguageContext, + TelemetryHelper, +} from 'aws-core-vscode/codewhisperer' +import { editorUtilities, getLogger, globals, setContext, vscodeUtilities } from 'aws-core-vscode/shared' +import { LinesChangeEvent, LineSelection, LineTracker } from '../stateTracker/lineTracker' +import { telemetry } from 'aws-core-vscode/telemetry' +import { cancellableDebounce } from 'aws-core-vscode/utils' +import { SessionManager } from '../sessionManager' + +const case3TimeWindow = 30000 // 30 seconds + +const maxSmallIntegerV8 = 2 ** 30 // Max number that can be stored in V8's smis (small integers) + +function fromId(id: string | undefined, sessionManager: SessionManager): AnnotationState | undefined { + switch (id) { + case AutotriggerState.id: + return new AutotriggerState(sessionManager) + case PressTabState.id: + return new AutotriggerState(sessionManager) + case ManualtriggerState.id: + return new ManualtriggerState() + case TryMoreExState.id: + return new TryMoreExState() + case EndState.id: + return new EndState() + case InlineChatState.id: + return new InlineChatState() + default: + return undefined + } +} + +interface AnnotationState { + id: string + suppressWhileRunning: boolean + decorationRenderOptions?: vscode.ThemableDecorationAttachmentRenderOptions + + text: () => string + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined + isNextState(state: AnnotationState | undefined): boolean +} + +/** + * case 1: How Cwspr triggers + * Trigger Criteria: + * User opens an editor file && + * CW is not providing a suggestion && + * User has not accepted any suggestion + * + * Exit criteria: + * User accepts 1 suggestion + * + */ +export class AutotriggerState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_1' + id = AutotriggerState.id + + suppressWhileRunning = true + text = () => 'Amazon Q Tip 1/3: Start typing to get suggestions ([ESC] to exit)' + static acceptedCount = 0 + + constructor(private readonly sessionManager: SessionManager) {} + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + if (AutotriggerState.acceptedCount < this.sessionManager.acceptedSuggestionCount) { + return new ManualtriggerState() + } else if (this.sessionManager.getActiveRecommendation().length > 0) { + return new PressTabState(this.sessionManager) + } else { + return this + } + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof ManualtriggerState + } +} + +/** + * case 1-a: Tab to accept + * Trigger Criteria: + * Case 1 && + * Inline suggestion is being shown + * + * Exit criteria: + * User accepts 1 suggestion + */ +export class PressTabState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_1a' + id = PressTabState.id + + suppressWhileRunning = false + + text = () => 'Amazon Q Tip 1/3: Press [TAB] to accept ([ESC] to exit)' + + constructor(private readonly sessionManager: SessionManager) {} + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + return new AutotriggerState(this.sessionManager).updateState(changeSource, force) + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof ManualtriggerState + } +} + +/** + * case 2: Manual trigger + * Trigger Criteria: + * User exists case 1 && + * User navigates to a new line + * + * Exit criteria: + * User inokes manual trigger shortcut + */ +export class ManualtriggerState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_2' + id = ManualtriggerState.id + + suppressWhileRunning = true + + text = () => { + if (os.platform() === 'win32') { + return 'Amazon Q Tip 2/3: Invoke suggestions with [Alt] + [C] ([ESC] to exit)' + } + + return 'Amazon Q Tip 2/3: Invoke suggestions with [Option] + [C] ([ESC] to exit)' + } + hasManualTrigger: boolean = false + hasValidResponse: boolean = false + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + if (this.hasManualTrigger && this.hasValidResponse) { + if (changeSource !== 'codewhisperer') { + return new TryMoreExState() + } else { + return undefined + } + } else { + return this + } + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof TryMoreExState + } +} + +/** + * case 3: Learn more + * Trigger Criteria: + * User exists case 2 && + * User navigates to a new line + * + * Exit criteria: + * User accepts or rejects the suggestion + */ +export class TryMoreExState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_3' + id = TryMoreExState.id + + suppressWhileRunning = true + + text = () => 'Amazon Q Tip 3/3: For settings, open the Amazon Q menu from the status bar ([ESC] to exit)' + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState { + if (force) { + return new EndState() + } + return this + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof EndState + } + + static learnmoeCount: number = 0 +} + +export class EndState implements AnnotationState { + static id = 'codewhisperer_learnmore_end' + id = EndState.id + + suppressWhileRunning = true + text = () => '' + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState { + return this + } + isNextState(state: AnnotationState): boolean { + return false + } +} + +export class InlineChatState implements AnnotationState { + static id = 'amazonq_annotation_inline_chat' + id = InlineChatState.id + suppressWhileRunning = false + + text = () => { + if (os.platform() === 'darwin') { + return 'Amazon Q: Edit \u2318I' + } + return 'Amazon Q: Edit (Ctrl+I)' + } + updateState(_changeSource: AnnotationChangeSource, _force: boolean): AnnotationState { + return this + } + isNextState(_state: AnnotationState | undefined): boolean { + return false + } +} + +/** + * There are + * - existing users + * - new users + * -- new users who has not seen tutorial + * -- new users who has seen tutorial + * + * "existing users" should have the context key "CODEWHISPERER_AUTO_TRIGGER_ENABLED" + * "new users who has seen tutorial" should have the context key "inlineKey" and "CODEWHISPERER_AUTO_TRIGGER_ENABLED" + * the remaining grouop of users should belong to "new users who has not seen tutorial" + */ +export class InlineTutorialAnnotation implements vscode.Disposable { + private readonly _disposable: vscode.Disposable + private _editor: vscode.TextEditor | undefined + + private _currentState: AnnotationState + + private readonly cwLineHintDecoration: vscode.TextEditorDecorationType = + vscode.window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 3em', + // "borderRadius" and "padding" are not available on "after" type of decoration, this is a hack to inject these css prop to "after" content. Refer to https://github.com/microsoft/vscode/issues/68845 + textDecoration: ';border-radius:0.25rem;padding:0rem 0.5rem;', + width: 'fit-content', + }, + rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen, + }) + + constructor( + private readonly lineTracker: LineTracker, + private readonly sessionManager: SessionManager + ) { + const cachedState = fromId(globals.globalState.get(inlinehintKey), sessionManager) + const cachedAutotriggerEnabled = globals.globalState.get('CODEWHISPERER_AUTO_TRIGGER_ENABLED') + + // new users (has or has not seen tutorial) + if (cachedAutotriggerEnabled === undefined || cachedState !== undefined) { + this._currentState = cachedState ?? new AutotriggerState(this.sessionManager) + getLogger().debug( + `codewhisperer: new user login, activating inline tutorial. (autotriggerEnabled=${cachedAutotriggerEnabled}; inlineState=${cachedState?.id})` + ) + } else { + this._currentState = new EndState() + getLogger().debug(`codewhisperer: existing user login, disabling inline tutorial.`) + } + + this._disposable = vscode.Disposable.from( + vscodeUtilities.subscribeOnce(this.lineTracker.onReady)(async (_) => { + await this.onReady() + }), + this.lineTracker.onDidChangeActiveLines(async (e) => { + await this.onActiveLinesChanged(e) + }), + AuthUtil.instance.auth.onDidChangeConnectionState(async (e) => { + if (e.state !== 'authenticating') { + await this.refresh(vscode.window.activeTextEditor, 'editor') + } + }), + AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(async () => { + await this.refresh(vscode.window.activeTextEditor, 'editor') + }) + ) + } + + dispose() { + this._disposable.dispose() + } + + private _isReady: boolean = false + + private async onReady(): Promise { + this._isReady = !(this._currentState instanceof EndState) + await this._refresh(vscode.window.activeTextEditor, 'editor') + } + + async triggered(triggerType: vscode.InlineCompletionTriggerKind): Promise { + await telemetry.withTraceId(async () => { + if (!this._isReady) { + return + } + + if (this._currentState instanceof ManualtriggerState) { + if ( + triggerType === vscode.InlineCompletionTriggerKind.Invoke && + this._currentState.hasManualTrigger === false + ) { + this._currentState.hasManualTrigger = true + } + if ( + this.sessionManager.getActiveRecommendation().length > 0 && + this._currentState.hasValidResponse === false + ) { + this._currentState.hasValidResponse = true + } + } + + await this.refresh(vscode.window.activeTextEditor, 'codewhisperer') + }, TelemetryHelper.instance.traceId) + } + + isTutorialDone(): boolean { + return this._currentState.id === new EndState().id + } + + isInlineChatHint(): boolean { + return this._currentState.id === new InlineChatState().id + } + + async dismissTutorial() { + this._currentState = new EndState() + await setContext('aws.codewhisperer.tutorial.workInProgress', false) + await globals.globalState.update(inlinehintKey, this._currentState.id) + } + + /** + * Trys to show the inline hint, if the tutorial is not finished it will not be shown + */ + async tryShowInlineHint(): Promise { + if (this.isTutorialDone()) { + this._isReady = true + this._currentState = new InlineChatState() + return true + } + return false + } + + async tryHideInlineHint(): Promise { + if (this._currentState instanceof InlineChatState) { + this._currentState = new EndState() + return true + } + return false + } + + private async onActiveLinesChanged(e: LinesChangeEvent) { + if (!this._isReady) { + return + } + + this.clear() + + await this.refresh(e.editor, e.reason) + } + + clear() { + this._editor?.setDecorations(this.cwLineHintDecoration, []) + } + + async refresh(editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) { + if (force) { + this.refreshDebounced.cancel() + await this._refresh(editor, source, true) + } else { + await this.refreshDebounced.promise(editor, source) + } + } + + private readonly refreshDebounced = cancellableDebounce( + async (editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) => { + await this._refresh(editor, source, force) + }, + 250 + ) + + private async _refresh(editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) { + if (!this._isReady) { + this.clear() + return + } + + if (this.isTutorialDone()) { + this.clear() + return + } + + if (editor === undefined && this._editor === undefined) { + this.clear() + return + } + + const selections = this.lineTracker.selections + if (editor === undefined || selections === undefined || !editorUtilities.isTextEditor(editor)) { + this.clear() + return + } + + if (this._editor !== editor) { + // Clear any annotations on the previously active editor + this.clear() + this._editor = editor + } + + // Make sure the editor hasn't died since the await above and that we are still on the same line(s) + if (editor.document === undefined || !this.lineTracker.includes(selections)) { + this.clear() + return + } + + if (!AuthUtil.instance.isConnectionValid()) { + this.clear() + return + } + + // Disable Tips when language is not supported by Amazon Q. + if (!runtimeLanguageContext.isLanguageSupported(editor.document)) { + return + } + + await this.updateDecorations(editor, selections, source, force) + } + + private async updateDecorations( + editor: vscode.TextEditor, + lines: LineSelection[], + source: AnnotationChangeSource, + force?: boolean + ) { + const range = editor.document.validateRange( + new vscode.Range(lines[0].active, maxSmallIntegerV8, lines[0].active, maxSmallIntegerV8) + ) + + const decorationOptions = this.getInlineDecoration(editor, lines, source, force) as + | vscode.DecorationOptions + | undefined + + if (decorationOptions === undefined) { + this.clear() + await setContext('aws.codewhisperer.tutorial.workInProgress', false) + return + } else if (this.isTutorialDone()) { + // special case + // Endstate is meaningless and doesnt need to be rendered + this.clear() + await this.dismissTutorial() + return + } else if (decorationOptions.renderOptions?.after?.contentText === new TryMoreExState().text()) { + // special case + // case 3 exit criteria is to fade away in 30s + setTimeout(async () => { + await this.refresh(editor, source, true) + }, case3TimeWindow) + } + + decorationOptions.range = range + + await globals.globalState.update(inlinehintKey, this._currentState.id) + if (!this.isInlineChatHint()) { + await setContext('aws.codewhisperer.tutorial.workInProgress', true) + } + editor.setDecorations(this.cwLineHintDecoration, [decorationOptions]) + } + + getInlineDecoration( + editor: vscode.TextEditor, + lines: LineSelection[], + source: AnnotationChangeSource, + force?: boolean + ): Partial | undefined { + const isCWRunning = this.sessionManager.getActiveSession()?.isRequestInProgress ?? false + + const textOptions: vscode.ThemableDecorationAttachmentRenderOptions = { + contentText: '', + fontWeight: 'normal', + fontStyle: 'normal', + textDecoration: 'none', + color: 'var(--vscode-editor-background)', + backgroundColor: 'var(--vscode-foreground)', + } + + if (isCWRunning && this._currentState.suppressWhileRunning) { + return undefined + } + + const updatedState: AnnotationState | undefined = this._currentState.updateState(source, force ?? false) + + if (updatedState === undefined) { + return undefined + } + + if (this._currentState.isNextState(updatedState)) { + // special case because PressTabState is part of case_1 (1a) which possibly jumps directly from case_1a to case_2 and miss case_1 + if (this._currentState instanceof PressTabState) { + telemetry.ui_click.emit({ elementId: AutotriggerState.id, passive: true }) + } + telemetry.ui_click.emit({ elementId: this._currentState.id, passive: true }) + } + + // update state + this._currentState = updatedState + + // take snapshot of accepted session so that we can compre if there is delta -> users accept 1 suggestion after seeing this state + AutotriggerState.acceptedCount = this.sessionManager.acceptedSuggestionCount + + textOptions.contentText = this._currentState.text() + + return { + renderOptions: { after: textOptions }, + } + } + + public get currentState(): AnnotationState { + return this._currentState + } +} diff --git a/packages/amazonq/src/inlineChat/activation.ts b/packages/amazonq/src/inlineChat/activation.ts index 01e9f420c05..9f196f31ba3 100644 --- a/packages/amazonq/src/inlineChat/activation.ts +++ b/packages/amazonq/src/inlineChat/activation.ts @@ -6,8 +6,14 @@ import * as vscode from 'vscode' import { InlineChatController } from './controller/inlineChatController' import { registerInlineCommands } from './command/registerInlineCommands' import { LanguageClient } from 'vscode-languageclient' +import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' -export function activate(context: vscode.ExtensionContext, client: LanguageClient, encryptionKey: Buffer) { - const inlineChatController = new InlineChatController(context, client, encryptionKey) +export function activate( + context: vscode.ExtensionContext, + client: LanguageClient, + encryptionKey: Buffer, + inlineChatTutorialAnnotation: InlineChatTutorialAnnotation +) { + const inlineChatController = new InlineChatController(context, client, encryptionKey, inlineChatTutorialAnnotation) registerInlineCommands(context, inlineChatController) } diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 4eb7c0a7c26..7151a8f9723 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -26,7 +26,7 @@ import { isSageMaker, Experiments, } from 'aws-core-vscode/shared' -import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' +import { InlineChatTutorialAnnotation } from '../../app/inline/tutorials/inlineChatTutorialAnnotation' export class InlineChatController { private task: InlineTask | undefined @@ -34,16 +34,21 @@ export class InlineChatController { private readonly inlineChatProvider: InlineChatProvider private readonly codeLenseProvider: CodelensProvider private readonly referenceLogController = new ReferenceLogController() - private readonly inlineLineAnnotationController: InlineLineAnnotationController + private readonly inlineChatTutorialAnnotation: InlineChatTutorialAnnotation private readonly computeDiffAndRenderOnEditor: (query: string) => Promise private userQuery: string | undefined private listeners: vscode.Disposable[] = [] - constructor(context: vscode.ExtensionContext, client: LanguageClient, encryptionKey: Buffer) { + constructor( + context: vscode.ExtensionContext, + client: LanguageClient, + encryptionKey: Buffer, + inlineChatTutorialAnnotation: InlineChatTutorialAnnotation + ) { this.inlineChatProvider = new InlineChatProvider(client, encryptionKey) this.inlineChatProvider.onErrorOccured(() => this.handleError()) this.codeLenseProvider = new CodelensProvider(context) - this.inlineLineAnnotationController = new InlineLineAnnotationController(context) + this.inlineChatTutorialAnnotation = inlineChatTutorialAnnotation this.computeDiffAndRenderOnEditor = Experiments.instance.get('amazonqLSPInlineChat', false) ? this.computeDiffAndRenderOnEditorLSP.bind(this) : this.computeDiffAndRenderOnEditorLocal.bind(this) @@ -144,7 +149,7 @@ export class InlineChatController { this.codeLenseProvider.updateLenses(task) if (task.state === TaskState.InProgress) { if (vscode.window.activeTextEditor) { - await this.inlineLineAnnotationController.hide(vscode.window.activeTextEditor) + await this.inlineChatTutorialAnnotation.hide(vscode.window.activeTextEditor) } } await this.refreshCodeLenses(task) @@ -170,7 +175,7 @@ export class InlineChatController { this.listeners = [] this.task = undefined - this.inlineLineAnnotationController.enable() + this.inlineChatTutorialAnnotation.enable() await setContext('amazonq.inline.codelensShortcutEnabled', undefined) } @@ -211,7 +216,7 @@ export class InlineChatController { this.userQuery = query await textDocumentUtil.addEofNewline(editor) this.task = await this.createTask(query, editor.document, editor.selection) - await this.inlineLineAnnotationController.disable(editor) + await this.inlineChatTutorialAnnotation.disable(editor) await this.computeDiffAndRenderOnEditor(query).catch(async (err) => { getLogger().error('computeDiffAndRenderOnEditor error: %s', (err as Error)?.message) if (err instanceof Error) { diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 25665850554..c96b177f870 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -20,7 +20,12 @@ import { updateConfigurationRequestType, WorkspaceFolder, } from '@aws/language-server-runtimes/protocol' -import { AuthUtil, CodeWhispererSettings, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { + AuthUtil, + CodeWhispererSettings, + getSelectedCustomization, + TelemetryHelper, +} from 'aws-core-vscode/codewhisperer' import { Settings, createServerOptions, @@ -40,6 +45,11 @@ import { activate } from './chat/activation' import { AmazonQResourcePaths } from './lspInstaller' import { ConfigSection, isValidConfigSection, toAmazonQLSPLogLevel } from './config' import { activate as activateInlineChat } from '../inlineChat/activation' +import { telemetry } from 'aws-core-vscode/telemetry' +import { SessionManager } from '../app/inline/sessionManager' +import { LineTracker } from '../app/inline/stateTracker/lineTracker' +import { InlineTutorialAnnotation } from '../app/inline/tutorials/inlineTutorialAnnotation' +import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') @@ -168,25 +178,60 @@ export async function startLanguageServer( return client.onReady().then(async () => { await auth.refreshConnection() - if (Experiments.instance.get('amazonqLSPInline', true)) { - 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') + // session manager for inline + const sessionManager = new SessionManager() + + // keeps track of the line changes + const lineTracker = new LineTracker() + + // tutorial for inline suggestions + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) + + // tutorial for inline chat + const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) + + const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) + inlineManager.registerInlineCompletion() + toDispose.push( + inlineManager, + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + }), + Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, }) - ) - } + + const editor = vscode.window.activeTextEditor + if (editor) { + if (forceProceed) { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer', true) + } else { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer') + } + } + }), + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }), + Commands.register('aws.amazonq.dismissTutorial', async () => { + const editor = vscode.window.activeTextEditor + if (editor) { + inlineTutorialAnnotation.clear() + try { + telemetry.ui_click.emit({ elementId: `dismiss_${inlineTutorialAnnotation.currentState.id}` }) + } catch (_) {} + await inlineTutorialAnnotation.dismissTutorial() + getLogger().debug(`codewhisperer: user dismiss tutorial.`) + } + }) + ) if (Experiments.instance.get('amazonqChatLSP', true)) { await activate(client, encryptionKey, resourcePaths.ui) } - activateInlineChat(extensionContext, client, encryptionKey) + activateInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 9fa62163bfe..858f82b51cb 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -17,6 +17,7 @@ import { } from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' +import { InlineTutorialAnnotation } from '../../../../../src/app/inline/tutorials/inlineTutorialAnnotation' describe('InlineCompletionManager', () => { let manager: InlineCompletionManager @@ -74,7 +75,10 @@ describe('InlineCompletionManager', () => { sendNotification: sendNotificationStub, } as unknown as LanguageClient - manager = new InlineCompletionManager(languageClient) + const sessionManager = new SessionManager() + const lineTracker = new LineTracker() + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) + manager = new InlineCompletionManager(languageClient, sessionManager, lineTracker, inlineTutorialAnnotation) getActiveSessionStub = sandbox.stub(manager['sessionManager'], 'getActiveSession') getActiveRecommendationStub = sandbox.stub(manager['sessionManager'], 'getActiveRecommendation') getReferenceStub = sandbox.stub(ReferenceLogViewProvider, 'getReferenceLog') @@ -264,10 +268,12 @@ describe('InlineCompletionManager', () => { let getAllRecommendationsStub: sinon.SinonStub let recommendationService: RecommendationService let setInlineReferenceStub: sinon.SinonStub + let inlineTutorialAnnotation: InlineTutorialAnnotation beforeEach(() => { const lineTracker = new LineTracker() const activeStateController = new InlineGeneratingMessage(lineTracker) + inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager) recommendationService = new RecommendationService(mockSessionManager, activeStateController) setInlineReferenceStub = sandbox.stub(ReferenceInlineProvider.instance, 'setInlineReference') @@ -290,7 +296,8 @@ describe('InlineCompletionManager', () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, - mockSessionManager + mockSessionManager, + inlineTutorialAnnotation ) const items = await provider.provideInlineCompletionItems( mockDocument, @@ -306,6 +313,7 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, + inlineTutorialAnnotation, false ) const items = await provider.provideInlineCompletionItems( @@ -322,6 +330,7 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, + inlineTutorialAnnotation, false ) await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) diff --git a/packages/core/src/codewhisperer/views/lineAnnotationController.ts b/packages/core/src/codewhisperer/views/lineAnnotationController.ts index ae853a25739..765ed25a7a4 100644 --- a/packages/core/src/codewhisperer/views/lineAnnotationController.ts +++ b/packages/core/src/codewhisperer/views/lineAnnotationController.ts @@ -15,7 +15,6 @@ import globals from '../../shared/extensionGlobals' import { Container } from '../service/serviceContainer' import { telemetry } from '../../shared/telemetry/telemetry' import { getLogger } from '../../shared/logger/logger' -import { Commands } from '../../shared/vscode/commands2' // import { session } from '../util/codeWhispererSession' // import { RecommendationHandler } from '../service/recommendationHandler' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' @@ -300,18 +299,18 @@ export class LineAnnotationController implements vscode.Disposable { }), this.container.auth.secondaryAuth.onDidChangeActiveConnection(async () => { await this.refresh(vscode.window.activeTextEditor, 'editor') - }), - Commands.register('aws.amazonq.dismissTutorial', async () => { - const editor = vscode.window.activeTextEditor - if (editor) { - this.clear() - try { - telemetry.ui_click.emit({ elementId: `dismiss_${this._currentState.id}` }) - } catch (_) {} - await this.dismissTutorial() - getLogger().debug(`codewhisperer: user dismiss tutorial.`) - } }) + // Commands.register('aws.amazonq.dismissTutorial', async () => { + // const editor = vscode.window.activeTextEditor + // if (editor) { + // this.clear() + // try { + // telemetry.ui_click.emit({ elementId: `dismiss_${this._currentState.id}` }) + // } catch (_) {} + // await this.dismissTutorial() + // getLogger().debug(`codewhisperer: user dismiss tutorial.`) + // } + // }) ) } From 7e651557a84f0749b112a0e17205e72ad8429fc8 Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 8 May 2025 22:42:54 -0400 Subject: [PATCH 14/48] refactor(amazonq): remove local workspace indexing library --- packages/amazonq/src/app/chat/activation.ts | 13 +- .../amazonq/test/e2e/lsp/lspInstallerUtil.ts | 4 +- .../test/e2e/lsp/workspaceContextLsp.test.ts | 42 -- .../test/unit/amazonq/lsp/lspClient.test.ts | 72 ---- packages/core/src/amazonq/index.ts | 4 - packages/core/src/amazonq/lsp/lspClient.ts | 378 ------------------ .../core/src/amazonq/lsp/lspController.ts | 237 ----------- packages/core/src/amazonq/lsp/types.ts | 150 ------- .../src/amazonq/lsp/workspaceInstaller.ts | 39 -- .../controllers/chat/controller.ts | 209 +++++----- .../controllers/chat/messenger/messenger.ts | 7 +- .../controllers/chat/telemetryHelper.ts | 40 +- packages/core/src/dev/activation.ts | 11 - .../src/test/codewhisperer/zipUtil.test.ts | 19 - .../src/testInteg/perf/buildIndex.test.ts | 79 ---- 15 files changed, 106 insertions(+), 1198 deletions(-) delete mode 100644 packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts delete mode 100644 packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts delete mode 100644 packages/core/src/amazonq/lsp/lspClient.ts delete mode 100644 packages/core/src/amazonq/lsp/lspController.ts delete mode 100644 packages/core/src/amazonq/lsp/types.ts delete mode 100644 packages/core/src/amazonq/lsp/workspaceInstaller.ts delete mode 100644 packages/core/src/testInteg/perf/buildIndex.test.ts diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts index bf6b7cdc3df..af48bc65e05 100644 --- a/packages/amazonq/src/app/chat/activation.ts +++ b/packages/amazonq/src/app/chat/activation.ts @@ -6,22 +6,14 @@ import * as vscode from 'vscode' import { ExtensionContext } from 'vscode' import { telemetry } from 'aws-core-vscode/telemetry' -import { AuthUtil, CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' -import { Commands, placeholder, funcUtil } from 'aws-core-vscode/shared' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' +import { Commands, placeholder } from 'aws-core-vscode/shared' import * as amazonq from 'aws-core-vscode/amazonq' export async function activate(context: ExtensionContext) { const appInitContext = amazonq.DefaultAmazonQAppInitContext.instance await amazonq.TryChatCodeLensProvider.register(appInitContext.onDidChangeAmazonQVisibility.event) - const setupLsp = funcUtil.debounce(async () => { - void amazonq.LspController.instance.trySetupLsp(context, { - startUrl: AuthUtil.instance.startUrl, - maxIndexSize: CodeWhispererSettings.instance.getMaxIndexSize(), - isVectorIndexEnabled: false, - }) - }, 5000) - context.subscriptions.push( amazonq.focusAmazonQChatWalkthrough.register(), amazonq.walkthroughInlineSuggestionsExample.register(), @@ -37,7 +29,6 @@ export async function activate(context: ExtensionContext) { void vscode.env.openExternal(vscode.Uri.parse(amazonq.amazonQHelpUrl)) }) - void setupLsp() void setupAuthNotification() } diff --git a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts index c7ca7a4ff9b..457fa52d808 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts @@ -18,7 +18,7 @@ import { } from 'aws-core-vscode/shared' import * as semver from 'semver' import { assertTelemetry } from 'aws-core-vscode/test' -import { LspConfig, LspController } from 'aws-core-vscode/amazonq' +import { LspConfig } from 'aws-core-vscode/amazonq' import { LanguageServerSetup } from 'aws-core-vscode/telemetry' function createVersion(version: string, contents: TargetContent[]) { @@ -60,8 +60,6 @@ export function createLspInstallerTests({ installer = createInstaller() tempDir = await makeTemporaryToolkitFolder() sandbox.stub(LanguageServerResolver.prototype, 'defaultDownloadFolder').returns(tempDir) - // Called on extension activation and can contaminate telemetry. - sandbox.stub(LspController.prototype, 'trySetupLsp') }) afterEach(async () => { diff --git a/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts b/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts deleted file mode 100644 index 75d57949c0b..00000000000 --- a/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as os from 'os' -import { createLspInstallerTests } from './lspInstallerUtil' -import { defaultAmazonQWorkspaceLspConfig, LspClient, LspConfig, WorkspaceLspInstaller } from 'aws-core-vscode/amazonq' -import assert from 'assert' - -describe('AmazonQWorkspaceLSP', () => { - createLspInstallerTests({ - suiteName: 'AmazonQWorkspaceLSPInstaller', - lspConfig: defaultAmazonQWorkspaceLspConfig, - createInstaller: (lspConfig?: LspConfig) => new WorkspaceLspInstaller.WorkspaceLspInstaller(lspConfig), - targetContents: [ - { - bytes: 0, - filename: `qserver-${os.platform()}-${os.arch()}.zip`, - hashes: [], - url: 'http://fakeurl', - }, - ], - setEnv: (path: string) => { - process.env.__AMAZONQWORKSPACELSP_PATH = path - }, - resetEnv: () => { - delete process.env.__AMAZONQWORKSPACELSP_PATH - }, - }) - - it('activates', async () => { - const ok = await LspClient.instance.waitUntilReady() - if (!ok) { - assert.fail('Workspace context language server failed to become ready') - } - const serverUsage = await LspClient.instance.getLspServerUsage() - if (!serverUsage) { - assert.fail('Unable to verify that the workspace context language server has been activated') - } - }) -}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts b/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts deleted file mode 100644 index 369cda5402d..00000000000 --- a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as sinon from 'sinon' -import assert from 'assert' -import { globals, getNodeExecutableName } from 'aws-core-vscode/shared' -import { LspClient, lspClient as lspClientModule } from 'aws-core-vscode/amazonq' - -describe('Amazon Q LSP client', function () { - let lspClient: LspClient - let encryptFunc: sinon.SinonSpy - - beforeEach(async function () { - sinon.stub(globals, 'isWeb').returns(false) - lspClient = new LspClient() - encryptFunc = sinon.spy(lspClient, 'encrypt') - }) - - it('encrypts payload of query ', async () => { - await lspClient.queryVectorIndex('mock_input') - assert.ok(encryptFunc.calledOnce) - assert.ok(encryptFunc.calledWith(JSON.stringify({ query: 'mock_input' }))) - const value = await encryptFunc.returnValues[0] - // verifies JWT encryption header - assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`)) - }) - - it('encrypts payload of index files ', async () => { - await lspClient.buildIndex(['fileA'], 'path', 'all') - assert.ok(encryptFunc.calledOnce) - assert.ok( - encryptFunc.calledWith( - JSON.stringify({ - filePaths: ['fileA'], - projectRoot: 'path', - config: 'all', - language: '', - }) - ) - ) - const value = await encryptFunc.returnValues[0] - // verifies JWT encryption header - assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`)) - }) - - it('encrypt removes readable information', async () => { - const sample = 'hello' - const encryptedSample = await lspClient.encrypt(sample) - assert.ok(!encryptedSample.includes('hello')) - }) - - it('validates node executable + lsp bundle', async () => { - await assert.rejects(async () => { - await lspClientModule.activate(globals.context, { - // Mimic the `LspResolution` type. - node: 'node.bogus.exe', - lsp: 'fake/lsp.js', - }) - }, /.*failed to run basic .*node.*exitcode.*node\.bogus\.exe.*/) - await assert.rejects(async () => { - await lspClientModule.activate(globals.context, { - node: getNodeExecutableName(), - lsp: 'fake/lsp.js', - }) - }, /.*failed to run .*exitcode.*node.*lsp\.js/) - }) - - afterEach(() => { - sinon.restore() - }) -}) diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 14c0e4a59a0..b029ea50094 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -15,9 +15,6 @@ export { walkthroughInlineSuggestionsExample, walkthroughSecurityScanExample, } from './onboardingPage/walkthrough' -export { LspController } from './lsp/lspController' -export { LspClient } from './lsp/lspClient' -export * as lspClient from './lsp/lspClient' export { api } from './extApi' export { AmazonQChatViewProvider } from './webview/webView' export { amazonQHelpUrl } from '../shared/constants' @@ -41,7 +38,6 @@ export { CodeReference } from '../codewhispererChat/view/connector/connector' 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' export * as featureConfig from './webview/generators/featureConfig' diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts deleted file mode 100644 index eba89c961c4..00000000000 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ /dev/null @@ -1,378 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ -import * as vscode from 'vscode' -import { oneMB } from '../../shared/utilities/processUtils' -import * as path from 'path' -import * as nls from 'vscode-nls' -import * as crypto from 'crypto' -import * as jose from 'jose' - -import { Disposable, ExtensionContext } from 'vscode' - -import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient' -import { - BuildIndexRequestPayload, - BuildIndexRequestType, - GetUsageRequestType, - IndexConfig, - QueryInlineProjectContextRequestType, - QueryVectorIndexRequestType, - UpdateIndexV2RequestPayload, - UpdateIndexV2RequestType, - QueryRepomapIndexRequestType, - GetRepomapIndexJSONRequestType, - Usage, - GetContextCommandItemsRequestType, - ContextCommandItem, - GetIndexSequenceNumberRequestType, - GetContextCommandPromptRequestType, - AdditionalContextPrompt, -} from './types' -import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings' -import { fs } from '../../shared/fs/fs' -import { getLogger } from '../../shared/logger/logger' -import globals from '../../shared/extensionGlobals' -import { ResourcePaths } from '../../shared/lsp/types' -import { createServerOptions, validateNodeExe } from '../../shared/lsp/utils/platform' -import { waitUntil } from '../../shared/utilities/timeoutUtils' - -const localize = nls.loadMessageBundle() - -const key = crypto.randomBytes(32) -const logger = getLogger('amazonqWorkspaceLsp') - -/** - * LspClient manages the API call between VS Code extension and LSP server - * It encryptes the payload of API call. - */ -export class LspClient { - static #instance: LspClient - client: LanguageClient | undefined - - public static get instance() { - return (this.#instance ??= new this()) - } - - constructor() { - this.client = undefined - } - - async encrypt(payload: string) { - return await new jose.CompactEncrypt(new TextEncoder().encode(payload)) - .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) - .encrypt(key) - } - - async buildIndex(paths: string[], rootPath: string, config: IndexConfig) { - const payload: BuildIndexRequestPayload = { - filePaths: paths, - projectRoot: rootPath, - config: config, - language: '', - } - try { - const encryptedRequest = await this.encrypt(JSON.stringify(payload)) - const resp = await this.client?.sendRequest(BuildIndexRequestType, encryptedRequest) - return resp - } catch (e) { - logger.error(`buildIndex error: ${e}`) - return undefined - } - } - - async queryVectorIndex(request: string) { - try { - const encryptedRequest = await this.encrypt( - JSON.stringify({ - query: request, - }) - ) - const resp = await this.client?.sendRequest(QueryVectorIndexRequestType, encryptedRequest) - return resp - } catch (e) { - logger.error(`queryVectorIndex error: ${e}`) - return [] - } - } - - async queryInlineProjectContext(query: string, path: string, target: 'default' | 'codemap' | 'bm25') { - try { - const request = JSON.stringify({ - query: query, - filePath: path, - target, - }) - const encrypted = await this.encrypt(request) - const resp: any = await this.client?.sendRequest(QueryInlineProjectContextRequestType, encrypted) - return resp - } catch (e) { - logger.error(`queryInlineProjectContext error: ${e}`) - throw e - } - } - - async getLspServerUsage(): Promise { - if (this.client) { - return (await this.client.sendRequest(GetUsageRequestType, '')) as Usage - } - } - - async updateIndex(filePath: string[], mode: 'update' | 'remove' | 'add' | 'context_command_symbol_update') { - const payload: UpdateIndexV2RequestPayload = { - filePaths: filePath, - updateMode: mode, - } - try { - const encryptedRequest = await this.encrypt(JSON.stringify(payload)) - const resp = await this.client?.sendRequest(UpdateIndexV2RequestType, encryptedRequest) - return resp - } catch (e) { - logger.error(`updateIndex error: ${e}`) - return undefined - } - } - async queryRepomapIndex(filePaths: string[]) { - try { - const request = JSON.stringify({ - filePaths: filePaths, - }) - const resp: any = await this.client?.sendRequest(QueryRepomapIndexRequestType, await this.encrypt(request)) - return resp - } catch (e) { - logger.error(`QueryRepomapIndex error: ${e}`) - throw e - } - } - async getRepoMapJSON() { - try { - const request = JSON.stringify({}) - const resp: any = await this.client?.sendRequest( - GetRepomapIndexJSONRequestType, - await this.encrypt(request) - ) - return resp - } catch (e) { - logger.error(`queryInlineProjectContext error: ${e}`) - throw e - } - } - - async getContextCommandItems(): Promise { - try { - const workspaceFolders = vscode.workspace.workspaceFolders || [] - const request = JSON.stringify({ - workspaceFolders: workspaceFolders.map((it) => it.uri.fsPath), - }) - const resp: any = await this.client?.sendRequest( - GetContextCommandItemsRequestType, - await this.encrypt(request) - ) - return resp - } catch (e) { - logger.error(`getContextCommandItems error: ${e}`) - throw e - } - } - - async getContextCommandPrompt(contextCommandItems: ContextCommandItem[]): Promise { - try { - const request = JSON.stringify({ - contextCommands: contextCommandItems, - }) - const resp: any = await this.client?.sendRequest( - GetContextCommandPromptRequestType, - await this.encrypt(request) - ) - return resp || [] - } catch (e) { - logger.error(`getContextCommandPrompt error: ${e}`) - throw e - } - } - - async getIndexSequenceNumber(): Promise { - try { - const request = JSON.stringify({}) - const resp: any = await this.client?.sendRequest( - GetIndexSequenceNumberRequestType, - await this.encrypt(request) - ) - return resp - } catch (e) { - logger.error(`getIndexSequenceNumber error: ${e}`) - throw e - } - } - - async waitUntilReady() { - return waitUntil( - async () => { - if (this.client === undefined) { - return false - } - await this.client.onReady() - return true - }, - { interval: 500, timeout: 60_000 * 3, truthy: true } - ) - } -} - -/** - * Activates the language server (assumes the LSP server has already been downloaded): - * 1. start LSP server running over IPC protocol. - * 2. create a output channel named Amazon Q Language Server. - */ -export async function activate(extensionContext: ExtensionContext, resourcePaths: ResourcePaths) { - LspClient.instance // Tickle the singleton... :/ - const toDispose = extensionContext.subscriptions - - let rangeFormatting: Disposable | undefined - // The debug options for the server - // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging - const debugOptions = { execArgv: ['--nolazy', '--preserve-symlinks', '--stdio'] } - const workerThreads = CodeWhispererSettings.instance.getIndexWorkerThreads() - const gpu = CodeWhispererSettings.instance.isLocalIndexGPUEnabled() - - if (gpu) { - process.env.Q_ENABLE_GPU = 'true' - } else { - delete process.env.Q_ENABLE_GPU - } - if (workerThreads > 0 && workerThreads < 100) { - process.env.Q_WORKER_THREADS = workerThreads.toString() - } else { - delete process.env.Q_WORKER_THREADS - } - - const serverModule = resourcePaths.lsp - const memoryWarnThreshold = 800 * oneMB - - const serverOptions = createServerOptions({ - encryptionKey: key, - executable: [resourcePaths.node], - serverModule, - // TODO(jmkeyes): we always use the debug options...? - execArgv: debugOptions.execArgv, - warnThresholds: { memory: memoryWarnThreshold }, - }) - - const documentSelector = [{ scheme: 'file', language: '*' }] - - await validateNodeExe([resourcePaths.node], resourcePaths.lsp, debugOptions.execArgv, logger) - - // Options to control the language client - const clientOptions: LanguageClientOptions = { - // Register the server for json documents - documentSelector, - initializationOptions: { - handledSchemaProtocols: ['file', 'untitled'], // language server only loads file-URI. Fetching schemas with other protocols ('http'...) are made on the client. - provideFormatter: false, // tell the server to not provide formatting capability and ignore the `aws.stepfunctions.asl.format.enable` setting. - // this is used by LSP to determine index cache path, move to this folder so that when extension updates index is not deleted. - extensionPath: path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'cache'), - }, - // Log to the Amazon Q Logs so everything is in a single channel - // TODO: Add prefix to the language server logs so it is easier to search - outputChannel: globals.logOutputChannel, - } - - // Create the language client and start the client. - LspClient.instance.client = new LanguageClient( - 'amazonq', - localize('amazonq.server.name', 'Amazon Q Language Server'), - serverOptions, - clientOptions - ) - LspClient.instance.client.registerProposedFeatures() - - const disposable = LspClient.instance.client.start() - toDispose.push(disposable) - - let savedDocument: vscode.Uri | undefined = undefined - - const onAdd = async (filePaths: string[]) => { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex(filePaths, 'add') - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 500, timeout: 5_000, truthy: true } - ) - } - const onRemove = async (filePaths: string[]) => { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex(filePaths, 'remove') - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 500, timeout: 5_000, truthy: true } - ) - } - - toDispose.push( - vscode.workspace.onDidSaveTextDocument((document) => { - if (document.uri.scheme !== 'file') { - return - } - savedDocument = document.uri - }), - vscode.window.onDidChangeActiveTextEditor((editor) => { - if (savedDocument && editor && editor.document.uri.fsPath !== savedDocument.fsPath) { - void LspClient.instance.updateIndex([savedDocument.fsPath], 'update') - } - // user created a new empty file using File -> New File - // these events will not be captured by vscode.workspace.onDidCreateFiles - // because it was created by File Explorer(Win) or Finder(MacOS) - // TODO: consider using a high performance fs watcher - if (editor?.document.getText().length === 0) { - void onAdd([editor.document.uri.fsPath]) - } - }), - vscode.workspace.onDidCreateFiles(async (e) => { - await onAdd(e.files.map((f) => f.fsPath)) - }), - vscode.workspace.onDidDeleteFiles(async (e) => { - await onRemove(e.files.map((f) => f.fsPath)) - }), - vscode.workspace.onDidRenameFiles(async (e) => { - await onRemove(e.files.map((f) => f.oldUri.fsPath)) - await onAdd(e.files.map((f) => f.newUri.fsPath)) - }) - ) - - return LspClient.instance.client.onReady().then( - () => { - const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void } - toDispose.push(disposableFunc) - }, - (reason) => { - logger.error('client.onReady() failed: %O', reason) - } - ) -} - -export async function deactivate(): Promise { - if (!LspClient.instance.client) { - return undefined - } - return LspClient.instance.client.stop() -} diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts deleted file mode 100644 index 3b7bd98a61d..00000000000 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ /dev/null @@ -1,237 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as path from 'path' -import { getLogger } from '../../shared/logger/logger' -import { CurrentWsFolders, collectFilesForIndex } from '../../shared/utilities/workspaceUtils' -import { activate as activateLsp, LspClient } from './lspClient' -import { telemetry } from '../../shared/telemetry/telemetry' -import { isCloud9 } from '../../shared/extensionUtilities' -import globals, { isWeb } from '../../shared/extensionGlobals' -import { isAmazonInternalOs } from '../../shared/vscode/env' -import { WorkspaceLspInstaller } from './workspaceInstaller' -import { lspSetupStage } from '../../shared/lsp/utils/setupStage' -import { RelevantTextDocumentAddition } from '../../codewhispererChat/controllers/chat/model' -import { waitUntil } from '../../shared/utilities/timeoutUtils' - -export interface Chunk { - readonly filePath: string - readonly content: string - readonly context?: string - readonly relativePath?: string - readonly programmingLanguage?: string - readonly startLine?: number - readonly endLine?: number -} -export interface BuildIndexConfig { - startUrl?: string - maxIndexSize: number - isVectorIndexEnabled: boolean -} - -/* - * LSP Controller manages the status of Amazon Q Workspace Indexing LSP: - * 1. Downloading, verifying and installing LSP using DEXP LSP manifest and CDN. - * 2. Managing the LSP states. There are a couple of possible LSP states: - * Not installed. Installed. Running. Indexing. Indexing Done. - * LSP Controller converts the input and output of LSP APIs. - * The IDE extension code should invoke LSP API via this controller. - * 3. It perform pre-process and post process of LSP APIs - * Pre-process the input to Index Files API - * Post-process the output from Query API - */ -export class LspController { - static #instance: LspController - private _isIndexingInProgress = false - private _contextCommandSymbolsUpdated = false - private logger = getLogger('amazonqWorkspaceLsp') - - public static get instance() { - return (this.#instance ??= new this()) - } - - isIndexingInProgress() { - return this._isIndexingInProgress - } - - async query(s: string): Promise { - const chunks: Chunk[] | undefined = await LspClient.instance.queryVectorIndex(s) - const resp: RelevantTextDocumentAddition[] = [] - if (chunks) { - for (const chunk of chunks) { - const text = chunk.context ? chunk.context : chunk.content - if (chunk.programmingLanguage && chunk.programmingLanguage !== 'unknown') { - resp.push({ - text: text, - relativeFilePath: chunk.relativePath ? chunk.relativePath : path.basename(chunk.filePath), - programmingLanguage: { - languageName: chunk.programmingLanguage, - }, - startLine: chunk.startLine ?? -1, - endLine: chunk.endLine ?? -1, - }) - } else { - resp.push({ - text: text, - relativeFilePath: chunk.relativePath ? chunk.relativePath : path.basename(chunk.filePath), - startLine: chunk.startLine ?? -1, - endLine: chunk.endLine ?? -1, - }) - } - } - } - return resp - } - - async queryInlineProjectContext(query: string, path: string, target: 'bm25' | 'codemap' | 'default') { - try { - return await LspClient.instance.queryInlineProjectContext(query, path, target) - } catch (e) { - if (e instanceof Error) { - this.logger.error(`unexpected error while querying inline project context, e=${e.message}`) - } - return [] - } - } - - async buildIndex(buildIndexConfig: BuildIndexConfig) { - this.logger.info(`Starting to build index of project`) - const start = performance.now() - const projPaths = (vscode.workspace.workspaceFolders ?? []).map((folder) => folder.uri.fsPath) - if (projPaths.length === 0) { - this.logger.info(`Skipping building index. No projects found in workspace`) - return - } - projPaths.sort() - try { - this._isIndexingInProgress = true - const projRoot = projPaths[0] - const files = await collectFilesForIndex( - projPaths, - vscode.workspace.workspaceFolders as CurrentWsFolders, - true, - buildIndexConfig.maxIndexSize * 1024 * 1024 - ) - const totalSizeBytes = files.reduce( - (accumulator, currentFile) => accumulator + currentFile.fileSizeBytes, - 0 - ) - this.logger.info(`Found ${files.length} files in current project ${projPaths}`) - const config = buildIndexConfig.isVectorIndexEnabled ? 'all' : 'default' - const r = files.map((f) => f.fileUri.fsPath) - const resp = await LspClient.instance.buildIndex(r, projRoot, config) - if (resp) { - this.logger.debug(`Finish building index of project`) - const usage = await LspClient.instance.getLspServerUsage() - telemetry.amazonq_indexWorkspace.emit({ - duration: performance.now() - start, - result: 'Succeeded', - amazonqIndexFileCount: files.length, - amazonqIndexMemoryUsageInMB: usage ? usage.memoryUsage / (1024 * 1024) : undefined, - amazonqIndexCpuUsagePercentage: usage ? usage.cpuUsage : undefined, - amazonqIndexFileSizeInMB: totalSizeBytes / (1024 * 1024), - amazonqVectorIndexEnabled: buildIndexConfig.isVectorIndexEnabled, - credentialStartUrl: buildIndexConfig.startUrl, - }) - } else { - this.logger.error(`Failed to build index of project`) - telemetry.amazonq_indexWorkspace.emit({ - duration: performance.now() - start, - result: 'Failed', - amazonqIndexFileCount: 0, - amazonqIndexFileSizeInMB: 0, - amazonqVectorIndexEnabled: buildIndexConfig.isVectorIndexEnabled, - reason: `Unknown`, - }) - } - } catch (error) { - // TODO: use telemetry.run() - this.logger.error(`Failed to build index of project`) - telemetry.amazonq_indexWorkspace.emit({ - duration: performance.now() - start, - result: 'Failed', - amazonqIndexFileCount: 0, - amazonqIndexFileSizeInMB: 0, - amazonqVectorIndexEnabled: buildIndexConfig.isVectorIndexEnabled, - reason: `${error instanceof Error ? error.name : 'Unknown'}`, - reasonDesc: `Error when building index. ${error instanceof Error ? error.message : error}`, - }) - } finally { - this._isIndexingInProgress = false - } - } - - async trySetupLsp(context: vscode.ExtensionContext, buildIndexConfig: BuildIndexConfig) { - if (isCloud9() || isWeb() || isAmazonInternalOs()) { - this.logger.warn('Skipping LSP setup. LSP is not compatible with the current environment. ') - // do not do anything if in Cloud9 or Web mode or in AL2 (AL2 does not support node v18+) - return - } - setImmediate(async () => { - try { - await this.setupLsp(context) - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - void LspController.instance.buildIndex(buildIndexConfig) - // log the LSP server CPU and Memory usage per 30 minutes. - globals.clock.setInterval( - async () => { - const usage = await LspClient.instance.getLspServerUsage() - if (usage) { - this.logger.info( - `LSP server CPU ${usage.cpuUsage}%, LSP server Memory ${ - usage.memoryUsage / (1024 * 1024) - }MB ` - ) - } - }, - 30 * 60 * 1000 - ) - } catch (e) { - this.logger.error(`LSP failed to activate ${e}`) - } - }) - } - /** - * Updates context command symbols once per session by synchronizing with the LSP client index. - * Context menu will contain file and folders to begin with, - * then this asynchronous function should be invoked after the files and folders are found - * the LSP then further starts to parse workspace and find symbols, which takes - * anywhere from 5 seconds to about 40 seconds, depending on project size. - * @returns {Promise} - */ - async updateContextCommandSymbolsOnce() { - if (this._contextCommandSymbolsUpdated) { - return - } - this._contextCommandSymbolsUpdated = true - getLogger().debug(`Start adding symbols to context picker menu`) - try { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex([], 'context_command_symbol_update') - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 1000, timeout: 60_000, truthy: true } - ) - } catch (err) { - this.logger.error(`Failed to find symbols`) - } - } - - private async setupLsp(context: vscode.ExtensionContext) { - await lspSetupStage('all', async () => { - const installResult = await new WorkspaceLspInstaller().resolve() - await lspSetupStage('launch', async () => activateLsp(context, installResult.resourcePaths)) - this.logger.info('LSP activated') - }) - } -} diff --git a/packages/core/src/amazonq/lsp/types.ts b/packages/core/src/amazonq/lsp/types.ts deleted file mode 100644 index 2940ce240c8..00000000000 --- a/packages/core/src/amazonq/lsp/types.ts +++ /dev/null @@ -1,150 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { RequestType } from 'vscode-languageserver' - -export type IndexRequestPayload = { - filePaths: string[] - rootPath: string - refresh: boolean -} - -export type ClearRequest = string - -export const ClearRequestType: RequestType = new RequestType('lsp/clear') - -export type QueryRequest = string - -export const QueryRequestType: RequestType = new RequestType('lsp/query') - -export type GetUsageRequest = string - -export const GetUsageRequestType: RequestType = new RequestType('lsp/getUsage') - -export interface Usage { - memoryUsage: number - cpuUsage: number -} - -export type BuildIndexRequestPayload = { - filePaths: string[] - projectRoot: string - config: string - language: string -} - -export type BuildIndexRequest = string - -export const BuildIndexRequestType: RequestType = new RequestType('lsp/buildIndex') - -export type UpdateIndexV2Request = string - -export type UpdateIndexV2RequestPayload = { filePaths: string[]; updateMode: string } - -export const UpdateIndexV2RequestType: RequestType = new RequestType( - 'lsp/updateIndexV2' -) - -export type QueryInlineProjectContextRequest = string -export type QueryInlineProjectContextRequestPayload = { - query: string - filePath: string - target: string -} -export const QueryInlineProjectContextRequestType: RequestType = - new RequestType('lsp/queryInlineProjectContext') - -export type QueryVectorIndexRequestPayload = { query: string } - -export type QueryVectorIndexRequest = string - -export const QueryVectorIndexRequestType: RequestType = new RequestType( - 'lsp/queryVectorIndex' -) - -export type IndexConfig = 'all' | 'default' - -// RepoMapData -export type QueryRepomapIndexRequestPayload = { filePaths: string[] } -export type QueryRepomapIndexRequest = string -export const QueryRepomapIndexRequestType: RequestType = new RequestType( - 'lsp/queryRepomapIndex' -) -export type GetRepomapIndexJSONRequest = string -export const GetRepomapIndexJSONRequestType: RequestType = new RequestType( - 'lsp/getRepomapIndexJSON' -) - -export type GetContextCommandItemsRequestPayload = { workspaceFolders: string[] } -export type GetContextCommandItemsRequest = string -export const GetContextCommandItemsRequestType: RequestType = new RequestType( - 'lsp/getContextCommandItems' -) - -export type GetIndexSequenceNumberRequest = string -export const GetIndexSequenceNumberRequestType: RequestType = new RequestType( - 'lsp/getIndexSequenceNumber' -) - -export type ContextCommandItemType = 'file' | 'folder' | 'code' - -export type SymbolType = - | 'Class' - | 'Function' - | 'Interface' - | 'Type' - | 'Enum' - | 'Struct' - | 'Delegate' - | 'Namespace' - | 'Object' - | 'Module' - | 'Method' - -export interface Position { - line: number - column: number -} -export interface Span { - start: Position - end: Position -} - -// LSP definition of DocumentSymbol - -export interface DocumentSymbol { - name: string - kind: SymbolType - range: Span -} - -export interface ContextCommandItem { - workspaceFolder: string - type: ContextCommandItemType - relativePath: string - symbol?: DocumentSymbol - id?: string -} - -export type GetContextCommandPromptRequestPayload = { - contextCommands: { - workspaceFolder: string - type: 'file' | 'folder' - relativePath: string - }[] -} -export type GetContextCommandPromptRequest = string -export const GetContextCommandPromptRequestType: RequestType = - new RequestType('lsp/getContextCommandPrompt') - -export interface AdditionalContextPrompt { - content: string - name: string - description: string - startLine: number - endLine: number - filePath: string - relativePath: string -} diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts deleted file mode 100644 index 99e70f20cbf..00000000000 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import path from 'path' -import { ResourcePaths } from '../../shared/lsp/types' -import { getNodeExecutableName } from '../../shared/lsp/utils/platform' -import { fs } from '../../shared/fs/fs' -import { BaseLspInstaller } from '../../shared/lsp/baseLspInstaller' -import { getAmazonQWorkspaceLspConfig, LspConfig } from './config' - -export class WorkspaceLspInstaller extends BaseLspInstaller { - constructor(lspConfig: LspConfig = getAmazonQWorkspaceLspConfig()) { - super(lspConfig, 'amazonqWorkspaceLsp') - } - - protected override async postInstall(assetDirectory: string): Promise { - const resourcePaths = this.resourcePaths(assetDirectory) - await fs.chmod(resourcePaths.node, 0o755) - } - - protected override resourcePaths(assetDirectory?: string): ResourcePaths { - // local version - if (!assetDirectory) { - return { - lsp: this.config.path ?? '', - node: getNodeExecutableName(), - } - } - - const lspNodeName = - process.platform === 'win32' ? getNodeExecutableName() : `node-${process.platform}-${process.arch}` - return { - lsp: path.join(assetDirectory, `qserver-${process.platform}-${process.arch}/qserver/lspServer.js`), - node: path.join(assetDirectory, lspNodeName), - } - } -} diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index ba2072eb6dc..1fee99b4026 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -59,7 +59,6 @@ import { triggerPayloadToChatRequest } from './chatRequest/converter' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { openUrl } from '../../../shared/utilities/vsCodeUtils' import { randomUUID } from '../../../shared/crypto' -import { LspController } from '../../../amazonq/lsp/lspController' import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' import { getHttpStatusCode, AwsClientResponseError } from '../../../shared/errors' @@ -70,8 +69,6 @@ import { inspect } from '../../../shared/utilities/collectionUtils' import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' -import { LspClient } from '../../../amazonq/lsp/lspClient' -import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types' import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import { FeatureConfigProvider, Features } from '../../../shared/featureConfig' @@ -80,9 +77,6 @@ import { getUserPromptsDirectory, promptFileExtension, createSavedPromptCommandId, - aditionalContentNameLimit, - additionalContentInnerContextLimit, - workspaceChunkMaxSize, defaultContextLengths, } from '../../constants' import { ChatSession } from '../../clients/chat/v0/chat' @@ -527,7 +521,7 @@ export class ChatController { commands: [{ command: commandName, description: commandDescription }], }) } - const symbolsCmd: QuickActionCommand = contextCommand[0].commands?.[3] + // const symbolsCmd: QuickActionCommand = contextCommand[0].commands?.[3] const promptsCmd: QuickActionCommand = contextCommand[0].commands?.[4] // Check for user prompts @@ -543,7 +537,7 @@ export class ChatController { command: path.basename(name, promptFileExtension), icon: 'magic' as MynahIconsType, id: 'prompt', - label: 'file' as ContextCommandItemType, + // label: 'file' as ContextCommandItemType, route: [userPromptsDirectory, name], })) ) @@ -559,47 +553,47 @@ export class ChatController { icon: 'list-add' as MynahIconsType, }) - const lspClientReady = await LspClient.instance.waitUntilReady() - if (lspClientReady) { - const contextCommandItems = await LspClient.instance.getContextCommandItems() - const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] - const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] - - for (const contextCommandItem of contextCommandItems) { - const wsFolderName = path.basename(contextCommandItem.workspaceFolder) - if (contextCommandItem.type === 'file') { - filesCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'file' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'file' as MynahIconsType, - }) - } else if (contextCommandItem.type === 'folder') { - folderCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'folder' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'folder' as MynahIconsType, - }) - } else if (contextCommandItem.symbol && symbolsCmd.children) { - symbolsCmd.children?.[0].commands.push({ - command: contextCommandItem.symbol.name, - description: `${contextCommandItem.symbol.kind}, ${path.join(wsFolderName, contextCommandItem.relativePath)}, L${contextCommandItem.symbol.range.start.line}-${contextCommandItem.symbol.range.end.line}`, - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'code' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'code-block' as MynahIconsType, - }) - } - } - } + // const lspClientReady = await LspClient.instance.waitUntilReady() + // if (lspClientReady) { + // const contextCommandItems = await LspClient.instance.getContextCommandItems() + // const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] + // const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] + + // for (const contextCommandItem of contextCommandItems) { + // const wsFolderName = path.basename(contextCommandItem.workspaceFolder) + // if (contextCommandItem.type === 'file') { + // filesCmd.children?.[0].commands.push({ + // command: path.basename(contextCommandItem.relativePath), + // description: path.join(wsFolderName, contextCommandItem.relativePath), + // route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + // label: 'file' as ContextCommandItemType, + // id: contextCommandItem.id, + // icon: 'file' as MynahIconsType, + // }) + // } else if (contextCommandItem.type === 'folder') { + // folderCmd.children?.[0].commands.push({ + // command: path.basename(contextCommandItem.relativePath), + // description: path.join(wsFolderName, contextCommandItem.relativePath), + // route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + // label: 'folder' as ContextCommandItemType, + // id: contextCommandItem.id, + // icon: 'folder' as MynahIconsType, + // }) + // } else if (contextCommandItem.symbol && symbolsCmd.children) { + // symbolsCmd.children?.[0].commands.push({ + // command: contextCommandItem.symbol.name, + // description: `${contextCommandItem.symbol.kind}, ${path.join(wsFolderName, contextCommandItem.relativePath)}, L${contextCommandItem.symbol.range.start.line}-${contextCommandItem.symbol.range.end.line}`, + // route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + // label: 'code' as ContextCommandItemType, + // id: contextCommandItem.id, + // icon: 'code-block' as MynahIconsType, + // }) + // } + // } + // } this.messenger.sendContextCommandData(contextCommand) - void LspController.instance.updateContextCommandSymbolsOnce() + // void LspController.instance.updateContextCommandSymbolsOnce() } private handlePromptCreate(tabID: string) { @@ -1006,7 +1000,7 @@ export class ChatController { } private async resolveContextCommandPayload(triggerPayload: TriggerPayload, session: ChatSession) { - const contextCommands: ContextCommandItem[] = [] + const contextCommands: any[] = [] // Check for workspace rules to add to context const workspaceRules = await this.collectWorkspaceRules() @@ -1017,7 +1011,7 @@ export class ChatController { vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(rule))?.uri?.path || '' return { workspaceFolder: workspaceFolderPath, - type: 'file' as ContextCommandItemType, + type: 'file' as any, relativePath: path.relative(workspaceFolderPath, rule), } }) @@ -1029,7 +1023,7 @@ export class ChatController { if (typeof context !== 'string' && context.route && context.route.length === 2) { contextCommands.push({ workspaceFolder: context.route[0] || '', - type: (context.label || '') as ContextCommandItemType, + type: (context.label || '') as any, relativePath: context.route[1] || '', id: context.id, }) @@ -1044,45 +1038,45 @@ export class ChatController { return [] } workspaceFolders.sort() - const workspaceFolder = workspaceFolders[0] - for (const contextCommand of contextCommands) { - session.relativePathToWorkspaceRoot.set(contextCommand.workspaceFolder, contextCommand.workspaceFolder) - } - let prompts: AdditionalContextPrompt[] = [] - try { - prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) - } catch (e) { - // todo: handle @workspace used before indexing is ready - getLogger().verbose(`Could not get context command prompts: ${e}`) - } - - triggerPayload.contextLengths.additionalContextLengths = this.telemetryHelper.getContextLengths(prompts) - for (const prompt of prompts.slice(0, 20)) { - // Add system prompt for user prompts and workspace rules - const contextType = this.telemetryHelper.getContextType(prompt) - const description = - contextType === 'rule' || contextType === 'prompt' - ? `You must follow the instructions in ${prompt.relativePath}. Below are lines ${prompt.startLine}-${prompt.endLine} of this file:\n` - : prompt.description - - // Handle user prompts outside the workspace - const relativePath = prompt.filePath.startsWith(getUserPromptsDirectory()) - ? path.basename(prompt.filePath) - : path.relative(workspaceFolder, prompt.filePath) - - const entry = { - name: prompt.name.substring(0, aditionalContentNameLimit), - description: description.substring(0, aditionalContentNameLimit), - innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), - type: contextType, - relativePath: relativePath, - startLine: prompt.startLine, - endLine: prompt.endLine, - } - - triggerPayload.additionalContents.push(entry) - } - getLogger().info(`Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} `) + // const workspaceFolder = workspaceFolders[0] + // for (const contextCommand of contextCommands) { + // session.relativePathToWorkspaceRoot.set(contextCommand.workspaceFolder, contextCommand.workspaceFolder) + // } + // const prompts: any[] = [] + // try { + // // prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) + // } catch (e) { + // // todo: handle @workspace used before indexing is ready + // getLogger().verbose(`Could not get context command prompts: ${e}`) + // } + + // triggerPayload.contextLengths.additionalContextLengths = this.telemetryHelper.getContextLengths(prompts) + // for (const prompt of prompts.slice(0, 20)) { + // // Add system prompt for user prompts and workspace rules + // const contextType = this.telemetryHelper.getContextType(prompt) + // const description = + // contextType === 'rule' || contextType === 'prompt' + // ? `You must follow the instructions in ${prompt.relativePath}. Below are lines ${prompt.startLine}-${prompt.endLine} of this file:\n` + // : prompt.description + + // // Handle user prompts outside the workspace + // const relativePath = prompt.filePath.startsWith(getUserPromptsDirectory()) + // ? path.basename(prompt.filePath) + // : path.relative(workspaceFolder, prompt.filePath) + + // const entry = { + // name: prompt.name.substring(0, aditionalContentNameLimit), + // description: description.substring(0, aditionalContentNameLimit), + // innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), + // type: contextType, + // relativePath: relativePath, + // startLine: prompt.startLine, + // endLine: prompt.endLine, + // } + + // triggerPayload.additionalContents.push(entry) + // } + // getLogger().info(`Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} `) } private async generateResponse( @@ -1130,25 +1124,24 @@ export class ChatController { if (triggerPayload.useRelevantDocuments) { triggerPayload.message = triggerPayload.message.replace(/@workspace/, '') if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { - const start = performance.now() - const relevantTextDocuments = await LspController.instance.query(triggerPayload.message) - for (const relevantDocument of relevantTextDocuments) { - if (relevantDocument.text && relevantDocument.text.length > 0) { - triggerPayload.contextLengths.workspaceContextLength += relevantDocument.text.length - if (relevantDocument.text.length > workspaceChunkMaxSize) { - relevantDocument.text = relevantDocument.text.substring(0, workspaceChunkMaxSize) - getLogger().debug(`Truncating @workspace chunk: ${relevantDocument.relativeFilePath} `) - } - triggerPayload.relevantTextDocuments.push(relevantDocument) - } - } - - for (const doc of triggerPayload.relevantTextDocuments) { - getLogger().info( - `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}, start line: ${doc.startLine}, end line: ${doc.endLine}` - ) - } - triggerPayload.projectContextQueryLatencyMs = performance.now() - start + // const start = performance.now() + // const relevantTextDocuments = await LspController.instance.query(triggerPayload.message) + // for (const relevantDocument of relevantTextDocuments) { + // if (relevantDocument.text && relevantDocument.text.length > 0) { + // triggerPayload.contextLengths.workspaceContextLength += relevantDocument.text.length + // if (relevantDocument.text.length > workspaceChunkMaxSize) { + // relevantDocument.text = relevantDocument.text.substring(0, workspaceChunkMaxSize) + // getLogger().debug(`Truncating @workspace chunk: ${relevantDocument.relativeFilePath} `) + // } + // triggerPayload.relevantTextDocuments.push(relevantDocument) + // } + // } + // for (const doc of triggerPayload.relevantTextDocuments) { + // getLogger().info( + // `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}, start line: ${doc.startLine}, end line: ${doc.endLine}` + // ) + // } + // triggerPayload.projectContextQueryLatencyMs = performance.now() - start } else { this.messenger.sendOpenSettingsMessage(triggerID, tabID) return diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 8c914686ad4..ab059ecb22d 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -40,7 +40,6 @@ import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' import { CodeScanIssue } from '../../../../codewhisperer/models/model' import { marked } from 'marked' import { JSDOM } from 'jsdom' -import { LspController } from '../../../../amazonq/lsp/lspController' import { extractCodeBlockLanguage } from '../../../../shared/markdown' import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils' import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants' @@ -290,11 +289,7 @@ export class Messenger { relatedContent: { title: 'Sources', content: relatedSuggestions as any }, }) } - if ( - triggerPayload.relevantTextDocuments && - triggerPayload.relevantTextDocuments.length > 0 && - LspController.instance.isIndexingInProgress() - ) { + if (triggerPayload.relevantTextDocuments && triggerPayload.relevantTextDocuments.length > 0) { this.dispatcher.sendChatMessage( new ChatMessage( { diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 2d9e01db9a0..ac914e77b6b 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -2,7 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import * as path from 'path' + import { UserIntent } from '@amzn/codewhisperer-streaming' import { AmazonqAddMessage, @@ -28,7 +28,6 @@ import { ResponseBodyLinkClickMessage, SourceLinkClickMessage, TriggerPayload, - AdditionalContextLengths, AdditionalContextInfo, } from './model' import { TriggerEvent, TriggerEventsStorage } from '../../storages/triggerEvents' @@ -43,9 +42,6 @@ import { supportedLanguagesList } from '../chat/chatRequest/converter' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' import { undefinedIfEmpty } from '../../../shared/utilities/textUtilities' -import { AdditionalContextPrompt } from '../../../amazonq/lsp/types' -import { getUserPromptsDirectory, promptFileExtension } from '../../constants' -import { isInDirectory } from '../../../shared/filesystemUtilities' import { sleep } from '../../../shared/utilities/timeoutUtils' import { FileDiagnostic, @@ -164,40 +160,6 @@ export class CWCTelemetryHelper { telemetry.amazonq_exitFocusChat.emit({ result: 'Succeeded', passive: true }) } - public getContextType(prompt: AdditionalContextPrompt): string { - if (prompt.filePath.endsWith(promptFileExtension)) { - if (isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) { - return 'rule' - } else if (isInDirectory(getUserPromptsDirectory(), prompt.filePath)) { - return 'prompt' - } - } - return 'file' - } - - public getContextLengths(prompts: AdditionalContextPrompt[]): AdditionalContextLengths { - let fileContextLength = 0 - let promptContextLength = 0 - let ruleContextLength = 0 - - for (const prompt of prompts) { - const type = this.getContextType(prompt) - switch (type) { - case 'rule': - ruleContextLength += prompt.content.length - break - case 'file': - fileContextLength += prompt.content.length - break - case 'prompt': - promptContextLength += prompt.content.length - break - } - } - - return { fileContextLength, promptContextLength, ruleContextLength } - } - public async recordFeedback(message: ChatItemFeedbackMessage) { const logger = getLogger() try { diff --git a/packages/core/src/dev/activation.ts b/packages/core/src/dev/activation.ts index 8ce0f6aab11..16b5d7e53ad 100644 --- a/packages/core/src/dev/activation.ts +++ b/packages/core/src/dev/activation.ts @@ -25,7 +25,6 @@ import { NotificationsController } from '../notifications/controller' import { DevNotificationsState } from '../notifications/types' import { QuickPickItem } from 'vscode' import { ChildProcess } from '../shared/utilities/processUtils' -import { WorkspaceLspInstaller } from '../amazonq/lsp/workspaceInstaller' interface MenuOption { readonly label: string @@ -451,12 +450,6 @@ const resettableFeatures: readonly ResettableFeature[] = [ detail: 'Resets memory/global state for the notifications panel (includes dismissed, onReceive).', executor: resetNotificationsState, }, - { - name: 'workspace lsp', - label: 'Download Lsp ', - detail: 'Resets workspace LSP', - executor: resetWorkspaceLspDownload, - }, ] as const // TODO this is *somewhat* similar to `openStorageFromInput`. If we need another @@ -545,10 +538,6 @@ async function resetNotificationsState() { await targetNotificationsController.reset() } -async function resetWorkspaceLspDownload() { - await new WorkspaceLspInstaller().resolve() -} - async function editNotifications() { const storageKey = 'aws.notifications.dev' const current = globalState.get(storageKey) ?? {} diff --git a/packages/core/src/test/codewhisperer/zipUtil.test.ts b/packages/core/src/test/codewhisperer/zipUtil.test.ts index a82db4a6840..e6c4f4148e5 100644 --- a/packages/core/src/test/codewhisperer/zipUtil.test.ts +++ b/packages/core/src/test/codewhisperer/zipUtil.test.ts @@ -16,7 +16,6 @@ import { ToolkitError } from '../../shared/errors' import { fs } from '../../shared/fs/fs' import { tempDirPath } from '../../shared/filesystemUtilities' import { CodeWhispererConstants } from '../../codewhisperer/indexNode' -import { LspClient } from '../../amazonq/lsp/lspClient' describe('zipUtil', function () { const workspaceFolder = getTestWorkspaceFolder() @@ -179,23 +178,5 @@ describe('zipUtil', function () { assert.strictEqual(result.language, 'java') assert.strictEqual(result.scannedFiles.size, 4) }) - - it('Should handle file system errors during directory creation', async function () { - sinon.stub(LspClient, 'instance').get(() => ({ - getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'), - })) - sinon.stub(fs, 'mkdir').rejects(new Error('Directory creation failed')) - - await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Directory creation failed/) - }) - - it('Should handle zip project errors', async function () { - sinon.stub(LspClient, 'instance').get(() => ({ - getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'), - })) - sinon.stub(zipUtil, 'zipProject' as keyof ZipUtil).rejects(new Error('Zip failed')) - - await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Zip failed/) - }) }) }) diff --git a/packages/core/src/testInteg/perf/buildIndex.test.ts b/packages/core/src/testInteg/perf/buildIndex.test.ts deleted file mode 100644 index d60de3bdc3a..00000000000 --- a/packages/core/src/testInteg/perf/buildIndex.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { performanceTest } from '../../shared/performance/performance' -import * as sinon from 'sinon' -import * as vscode from 'vscode' -import assert from 'assert' -import { LspClient, LspController } from '../../amazonq' -import { LanguageClient, ServerOptions } from 'vscode-languageclient' -import { createTestWorkspace } from '../../test/testUtil' -import { BuildIndexRequestType, GetUsageRequestType } from '../../amazonq/lsp/types' -import { fs, getRandomString } from '../../shared' -import { FileSystem } from '../../shared/fs/fs' -import { getFsCallsUpperBound } from './utilities' - -interface SetupResult { - clientReqStub: sinon.SinonStub - fsSpy: sinon.SinonSpiedInstance - findFilesSpy: sinon.SinonSpy -} - -async function verifyResult(setup: SetupResult) { - // A correct run makes 2 requests, but don't want to make it exact to avoid over-sensitivity to implementation. If we make 10+ something is likely wrong. - assert.ok(setup.clientReqStub.callCount >= 2 && setup.clientReqStub.callCount <= 10) - assert.ok(setup.clientReqStub.calledWith(BuildIndexRequestType)) - assert.ok(setup.clientReqStub.calledWith(GetUsageRequestType)) - - assert.strictEqual(getFsCallsUpperBound(setup.fsSpy), 0, 'should not make any fs calls') - assert.ok(setup.findFilesSpy.callCount <= 2, 'findFiles should not be called more than twice') -} - -async function setupWithWorkspace(numFiles: number, options: { fileContent: string }): Promise { - // Force VSCode to find my test workspace only to keep test contained and controlled. - const testWorksapce = await createTestWorkspace(numFiles, options) - sinon.stub(vscode.workspace, 'workspaceFolders').value([testWorksapce]) - - // Avoid sending real request to lsp. - const clientReqStub = sinon.stub(LanguageClient.prototype, 'sendRequest').resolves(true) - const fsSpy = sinon.spy(fs) - const findFilesSpy = sinon.spy(vscode.workspace, 'findFiles') - LspClient.instance.client = new LanguageClient('amazonq', 'test-client', {} as ServerOptions, {}) - return { clientReqStub, fsSpy, findFilesSpy } -} - -describe('buildIndex', function () { - describe('performanceTests', function () { - afterEach(function () { - sinon.restore() - }) - performanceTest({}, 'indexing many small files', function () { - return { - setup: async () => setupWithWorkspace(250, { fileContent: '0123456789' }), - execute: async () => { - await LspController.instance.buildIndex({ - startUrl: '', - maxIndexSize: 30, - isVectorIndexEnabled: true, - }) - }, - verify: verifyResult, - } - }) - performanceTest({}, 'indexing few large files', function () { - return { - setup: async () => setupWithWorkspace(10, { fileContent: getRandomString(1000) }), - execute: async () => { - await LspController.instance.buildIndex({ - startUrl: '', - maxIndexSize: 30, - isVectorIndexEnabled: true, - }) - }, - verify: verifyResult, - } - }) - }) -}) From 079bb52baf1598b62729fd2f21152ba15f56aacb Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 8 May 2025 22:59:50 -0400 Subject: [PATCH 15/48] refactor(amazonq): refactor base lsp config --- packages/amazonq/src/lsp/config.ts | 5 +- .../amazonq/test/e2e/lsp/amazonqLsp.test.ts | 4 +- .../amazonq/test/e2e/lsp/lspInstallerUtil.ts | 5 +- .../test/unit/amazonq/lsp/config.test.ts | 140 ++++++++---------- packages/core/src/amazonq/index.ts | 1 - packages/core/src/amazonq/lsp/config.ts | 31 ---- .../core/src/shared/lsp/baseLspInstaller.ts | 9 +- 7 files changed, 72 insertions(+), 123 deletions(-) delete mode 100644 packages/core/src/amazonq/lsp/config.ts diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts index bb6870cb561..89a8ff3f714 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -3,10 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { DevSettings, getServiceEnvVarConfig } from 'aws-core-vscode/shared' -import { LspConfig } from 'aws-core-vscode/amazonq' +import { DevSettings, getServiceEnvVarConfig, BaseLspInstaller } from 'aws-core-vscode/shared' -export interface ExtendedAmazonQLSPConfig extends LspConfig { +export interface ExtendedAmazonQLSPConfig extends BaseLspInstaller.LspConfig { ui?: string } diff --git a/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts b/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts index d3e90ec4e8e..f4a60ff282b 100644 --- a/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts +++ b/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts @@ -6,13 +6,13 @@ import { AmazonQLspInstaller } from '../../../src/lsp/lspInstaller' import { defaultAmazonQLspConfig } from '../../../src/lsp/config' import { createLspInstallerTests } from './lspInstallerUtil' -import { LspConfig } from 'aws-core-vscode/amazonq' +import { BaseLspInstaller } from 'aws-core-vscode/shared' describe('AmazonQLSP', () => { createLspInstallerTests({ suiteName: 'AmazonQLSPInstaller', lspConfig: defaultAmazonQLspConfig, - createInstaller: (lspConfig?: LspConfig) => new AmazonQLspInstaller(lspConfig), + createInstaller: (lspConfig?: BaseLspInstaller.LspConfig) => new AmazonQLspInstaller(lspConfig), targetContents: [ { bytes: 0, diff --git a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts index 457fa52d808..d4251959756 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts @@ -18,7 +18,6 @@ import { } from 'aws-core-vscode/shared' import * as semver from 'semver' import { assertTelemetry } from 'aws-core-vscode/test' -import { LspConfig } from 'aws-core-vscode/amazonq' import { LanguageServerSetup } from 'aws-core-vscode/telemetry' function createVersion(version: string, contents: TargetContent[]) { @@ -44,8 +43,8 @@ export function createLspInstallerTests({ resetEnv, }: { suiteName: string - lspConfig: LspConfig - createInstaller: (lspConfig?: LspConfig) => BaseLspInstaller.BaseLspInstaller + lspConfig: BaseLspInstaller.LspConfig + createInstaller: (lspConfig?: BaseLspInstaller.LspConfig) => BaseLspInstaller.BaseLspInstaller targetContents: TargetContent[] setEnv: (path: string) => void resetEnv: () => void diff --git a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts index 0327395fe1a..69b15d6e311 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -7,97 +7,73 @@ import assert from 'assert' import { DevSettings } from 'aws-core-vscode/shared' import sinon from 'sinon' import { defaultAmazonQLspConfig, ExtendedAmazonQLSPConfig, getAmazonQLspConfig } from '../../../../src/lsp/config' -import { defaultAmazonQWorkspaceLspConfig, getAmazonQWorkspaceLspConfig, LspConfig } from 'aws-core-vscode/amazonq' -for (const [name, config, defaultConfig, setEnv, resetEnv] of [ - [ - 'getAmazonQLspConfig', - getAmazonQLspConfig, - defaultAmazonQLspConfig, - (envConfig: ExtendedAmazonQLSPConfig) => { - process.env.__AMAZONQLSP_MANIFEST_URL = envConfig.manifestUrl - process.env.__AMAZONQLSP_SUPPORTED_VERSIONS = envConfig.supportedVersions - process.env.__AMAZONQLSP_ID = envConfig.id - process.env.__AMAZONQLSP_PATH = envConfig.path - process.env.__AMAZONQLSP_UI = envConfig.ui - }, - () => { - delete process.env.__AMAZONQLSP_MANIFEST_URL - delete process.env.__AMAZONQLSP_SUPPORTED_VERSIONS - delete process.env.__AMAZONQLSP_ID - delete process.env.__AMAZONQLSP_PATH - delete process.env.__AMAZONQLSP_UI - }, - ], - [ - 'getAmazonQWorkspaceLspConfig', - getAmazonQWorkspaceLspConfig, - defaultAmazonQWorkspaceLspConfig, - (envConfig: LspConfig) => { - process.env.__AMAZONQWORKSPACELSP_MANIFEST_URL = envConfig.manifestUrl - process.env.__AMAZONQWORKSPACELSP_SUPPORTED_VERSIONS = envConfig.supportedVersions - process.env.__AMAZONQWORKSPACELSP_ID = envConfig.id - process.env.__AMAZONQWORKSPACELSP_PATH = envConfig.path - }, - () => { - delete process.env.__AMAZONQWORKSPACELSP_MANIFEST_URL - delete process.env.__AMAZONQWORKSPACELSP_SUPPORTED_VERSIONS - delete process.env.__AMAZONQWORKSPACELSP_ID - delete process.env.__AMAZONQWORKSPACELSP_PATH - }, - ], -] as const) { - describe(name, () => { - let sandbox: sinon.SinonSandbox - let serviceConfigStub: sinon.SinonStub - const settingConfig: LspConfig = { - manifestUrl: 'https://custom.url/manifest.json', - supportedVersions: '4.0.0', - id: 'AmazonQSetting', - suppressPromptPrefix: config().suppressPromptPrefix, - path: '/custom/path', - ...(name === 'getAmazonQLspConfig' && { ui: '/chat/client/location' }), - } +describe('getAmazonQLspConfig', () => { + let sandbox: sinon.SinonSandbox + let serviceConfigStub: sinon.SinonStub + const settingConfig: ExtendedAmazonQLSPConfig = { + manifestUrl: 'https://custom.url/manifest.json', + supportedVersions: '4.0.0', + id: 'AmazonQSetting', + suppressPromptPrefix: getAmazonQLspConfig().suppressPromptPrefix, + path: '/custom/path', + ui: '/chat/client/location', + } - beforeEach(() => { - sandbox = sinon.createSandbox() + beforeEach(() => { + sandbox = sinon.createSandbox() - serviceConfigStub = sandbox.stub() - sandbox.stub(DevSettings, 'instance').get(() => ({ - getServiceConfig: serviceConfigStub, - })) - }) + serviceConfigStub = sandbox.stub() + sandbox.stub(DevSettings, 'instance').get(() => ({ + getServiceConfig: serviceConfigStub, + })) + }) - afterEach(() => { - sandbox.restore() - resetEnv() - }) + afterEach(() => { + sandbox.restore() + resetEnv() + }) - it('uses default config', () => { - serviceConfigStub.returns({}) - assert.deepStrictEqual(config(), defaultConfig) - }) + it('uses default config', () => { + serviceConfigStub.returns({}) + assert.deepStrictEqual(getAmazonQLspConfig(), defaultAmazonQLspConfig) + }) - it('overrides path', () => { - const path = '/custom/path/to/lsp' - serviceConfigStub.returns({ path }) + it('overrides path', () => { + const path = '/custom/path/to/lsp' + serviceConfigStub.returns({ path }) - assert.deepStrictEqual(config(), { - ...defaultConfig, - path, - }) + assert.deepStrictEqual(getAmazonQLspConfig(), { + ...defaultAmazonQLspConfig, + path, }) + }) - it('overrides default settings', () => { - serviceConfigStub.returns(settingConfig) + it('overrides default settings', () => { + serviceConfigStub.returns(settingConfig) - assert.deepStrictEqual(config(), settingConfig) - }) + assert.deepStrictEqual(getAmazonQLspConfig(), settingConfig) + }) - it('environment variable takes precedence over settings', () => { - setEnv(settingConfig) - serviceConfigStub.returns({}) - assert.deepStrictEqual(config(), settingConfig) - }) + it('environment variable takes precedence over settings', () => { + setEnv(settingConfig) + serviceConfigStub.returns({}) + assert.deepStrictEqual(getAmazonQLspConfig(), settingConfig) }) -} + + function setEnv(envConfig: ExtendedAmazonQLSPConfig) { + process.env.__AMAZONQLSP_MANIFEST_URL = envConfig.manifestUrl + process.env.__AMAZONQLSP_SUPPORTED_VERSIONS = envConfig.supportedVersions + process.env.__AMAZONQLSP_ID = envConfig.id + process.env.__AMAZONQLSP_PATH = envConfig.path + process.env.__AMAZONQLSP_UI = envConfig.ui + } + + function resetEnv() { + delete process.env.__AMAZONQLSP_MANIFEST_URL + delete process.env.__AMAZONQLSP_SUPPORTED_VERSIONS + delete process.env.__AMAZONQLSP_ID + delete process.env.__AMAZONQLSP_PATH + delete process.env.__AMAZONQLSP_UI + } +}) diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index b029ea50094..3b7737b3547 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -37,7 +37,6 @@ export { ExtensionMessage } from '../amazonq/webview/ui/commands' export { CodeReference } from '../codewhispererChat/view/connector/connector' export { extractAuthFollowUp } from './util/authUtils' export { Messenger } from './commons/connector/baseMessenger' -export * from './lsp/config' export * as secondaryAuth from '../auth/secondaryAuth' export * as authConnection from '../auth/connection' export * as featureConfig from './webview/generators/featureConfig' diff --git a/packages/core/src/amazonq/lsp/config.ts b/packages/core/src/amazonq/lsp/config.ts deleted file mode 100644 index 5670d0d0ce4..00000000000 --- a/packages/core/src/amazonq/lsp/config.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DevSettings } from '../../shared/settings' -import { getServiceEnvVarConfig } from '../../shared/vscode/env' - -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.47', - id: 'AmazonQ-Workspace', // used across IDEs for identifying global storage/local disk locations. Do not change. - suppressPromptPrefix: 'amazonQWorkspace', - path: undefined, -} - -export function getAmazonQWorkspaceLspConfig(): LspConfig { - return { - ...defaultAmazonQWorkspaceLspConfig, - ...(DevSettings.instance.getServiceConfig('amazonqWorkspaceLsp', {}) as LspConfig), - ...getServiceEnvVarConfig('amazonqWorkspaceLsp', Object.keys(defaultAmazonQWorkspaceLspConfig)), - } -} diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index 0aeca1dfda4..7acf58ad788 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -5,7 +5,6 @@ import * as nodePath from 'path' import vscode from 'vscode' -import { LspConfig } from '../../amazonq/lsp/config' import { LanguageServerResolver } from './lspResolver' import { ManifestResolver } from './manifestResolver' import { LspResolution, ResourcePaths } from './types' @@ -14,6 +13,14 @@ import { Range } from 'semver' import { getLogger } from '../logger/logger' import type { Logger, LogTopic } from '../logger/logger' +export interface LspConfig { + manifestUrl: string + supportedVersions: string + id: string + suppressPromptPrefix: string + path?: string +} + export abstract class BaseLspInstaller { private logger: Logger From 69951077f72ec4dcc0427587dc63c15221eb62ae Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Thu, 8 May 2025 23:07:40 -0400 Subject: [PATCH 16/48] telemetry(amazonq): implement codewhisperer_clientComponentLatency (#7234) ## Problem the only metric it looks like we're missing for inline on the vscode side is `codewhisperer_clientComponentLatency` ## Solution codewhisperer_clientComponentLatency uses a very similar implementation as before the only differences are: 1. codewhispererCredentialFetchingLatency is no longer relevant because the token is always injected into the language server and it doesn't need to build the client on demand like before. - This causes the preprocessing latency to decrease, because that used to contain the time it takes to fetch the credentials 2. postProcessing latency is way lower because once we get the result vscode instantly displays it -- we no longer have control of that example metric now: ``` 2025-05-06 11:53:59.858 [debug] telemetry: codewhisperer_clientComponentLatency { Metadata: { codewhispererAllCompletionsLatency: '792.7122090000048', codewhispererCompletionType: 'Line', codewhispererCredentialFetchingLatency: '0', codewhispererCustomizationArn: 'arn:aws:codewhisperer:us-east-1:12345678910:customization/AAAAAAAAAA', codewhispererEndToEndLatency: '792.682249999998', codewhispererFirstCompletionLatency: '792.6440000000002', codewhispererLanguage: 'java', codewhispererPostprocessingLatency: '0.019500000002153683', codewhispererPreprocessingLatency: '0.007166999996115919', codewhispererRequestId: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX', codewhispererTriggerType: 'AutoTrigger', credentialStartUrl: 'https://XXXXX.XXXXX.com/start', awsAccount: 'not-set', awsRegion: 'us-east-1' }, Value: 1, Unit: 'None', Passive: true } ``` --- - 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/app/inline/completion.ts | 3 + .../src/app/inline/recommendationService.ts | 21 +++ .../amazonq/src/app/inline/telemetryHelper.ts | 162 ++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 packages/amazonq/src/app/inline/telemetryHelper.ts diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index c1c950a4d89..f41d0aacf86 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -36,6 +36,7 @@ import { import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' +import { TelemetryHelper } from './telemetryHelper' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -212,6 +213,8 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem // tell the tutorial that completions has been triggered await this.inlineTutorialAnnotation.triggered(context.triggerKind) + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setTriggerType(context.triggerKind) // make service requests if it's a new session await this.recommendationService.getAllRecommendations( diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index c9d84ef7642..461504f8db2 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -13,6 +13,7 @@ import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' +import { TelemetryHelper } from './telemetryHelper' export class RecommendationService { constructor( @@ -36,6 +37,9 @@ export class RecommendationService { } const requestStartTime = Date.now() const statusBar = CodeWhispererStatusBarManager.instance + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setPreprocessEndTime() + TelemetryHelper.instance.setSdkApiCallStartTime() try { // Show UI indicators that we are generating suggestions @@ -49,6 +53,14 @@ export class RecommendationService { token ) + // Set telemetry data for the first response + TelemetryHelper.instance.setSdkApiCallEndTime() + TelemetryHelper.instance.setSessionId(firstResult.sessionId) + if (firstResult.items.length > 0) { + TelemetryHelper.instance.setFirstResponseRequestId(firstResult.items[0].itemId) + } + TelemetryHelper.instance.setFirstSuggestionShowTime() + const firstCompletionDisplayLatency = Date.now() - requestStartTime this.sessionManager.startSession( firstResult.sessionId, @@ -64,6 +76,10 @@ export class RecommendationService { }) } else { this.sessionManager.closeSession() + + // No more results to fetch, mark pagination as complete + TelemetryHelper.instance.setAllPaginationEndTime() + TelemetryHelper.instance.tryRecordClientComponentLatency() } } finally { // Remove all UI indicators of message generation since we are done @@ -89,6 +105,11 @@ export class RecommendationService { this.sessionManager.updateSessionSuggestions(result.items) nextToken = result.partialResultToken } + this.sessionManager.closeSession() + + // All pagination requests completed + TelemetryHelper.instance.setAllPaginationEndTime() + TelemetryHelper.instance.tryRecordClientComponentLatency() } } diff --git a/packages/amazonq/src/app/inline/telemetryHelper.ts b/packages/amazonq/src/app/inline/telemetryHelper.ts new file mode 100644 index 00000000000..dffd267bee1 --- /dev/null +++ b/packages/amazonq/src/app/inline/telemetryHelper.ts @@ -0,0 +1,162 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { CodewhispererLanguage } from 'aws-core-vscode/shared' +import { CodewhispererTriggerType, telemetry } from 'aws-core-vscode/telemetry' +import { InlineCompletionTriggerKind } from 'vscode' + +export class TelemetryHelper { + // Variables needed for client component latency + private _invokeSuggestionStartTime = 0 + private _preprocessEndTime = 0 + private _sdkApiCallStartTime = 0 + private _sdkApiCallEndTime = 0 + private _allPaginationEndTime = 0 + private _firstSuggestionShowTime = 0 + private _firstResponseRequestId = '' + private _sessionId = '' + private _language: CodewhispererLanguage = 'java' + private _triggerType: CodewhispererTriggerType = 'OnDemand' + + constructor() {} + + static #instance: TelemetryHelper + + public static get instance() { + return (this.#instance ??= new this()) + } + + public resetClientComponentLatencyTime() { + this._invokeSuggestionStartTime = 0 + this._preprocessEndTime = 0 + this._sdkApiCallStartTime = 0 + this._sdkApiCallEndTime = 0 + this._firstSuggestionShowTime = 0 + this._allPaginationEndTime = 0 + this._firstResponseRequestId = '' + } + + public setInvokeSuggestionStartTime() { + this.resetClientComponentLatencyTime() + this._invokeSuggestionStartTime = performance.now() + } + + get invokeSuggestionStartTime(): number { + return this._invokeSuggestionStartTime + } + + public setPreprocessEndTime() { + this._preprocessEndTime = performance.now() + } + + get preprocessEndTime(): number { + return this._preprocessEndTime + } + + public setSdkApiCallStartTime() { + if (this._sdkApiCallStartTime === 0) { + this._sdkApiCallStartTime = performance.now() + } + } + + get sdkApiCallStartTime(): number { + return this._sdkApiCallStartTime + } + + public setSdkApiCallEndTime() { + if (this._sdkApiCallEndTime === 0 && this._sdkApiCallStartTime !== 0) { + this._sdkApiCallEndTime = performance.now() + } + } + + get sdkApiCallEndTime(): number { + return this._sdkApiCallEndTime + } + + public setAllPaginationEndTime() { + if (this._allPaginationEndTime === 0 && this._sdkApiCallEndTime !== 0) { + this._allPaginationEndTime = performance.now() + } + } + + get allPaginationEndTime(): number { + return this._allPaginationEndTime + } + + public setFirstSuggestionShowTime() { + if (this._firstSuggestionShowTime === 0 && this._sdkApiCallEndTime !== 0) { + this._firstSuggestionShowTime = performance.now() + } + } + + get firstSuggestionShowTime(): number { + return this._firstSuggestionShowTime + } + + public setFirstResponseRequestId(requestId: string) { + if (this._firstResponseRequestId === '') { + this._firstResponseRequestId = requestId + } + } + + get firstResponseRequestId(): string { + return this._firstResponseRequestId + } + + public setSessionId(sessionId: string) { + if (this._sessionId === '') { + this._sessionId = sessionId + } + } + + get sessionId(): string { + return this._sessionId + } + + public setLanguage(language: CodewhispererLanguage) { + this._language = language + } + + get language(): CodewhispererLanguage { + return this._language + } + + public setTriggerType(triggerType: InlineCompletionTriggerKind) { + if (triggerType === InlineCompletionTriggerKind.Invoke) { + this._triggerType = 'OnDemand' + } else if (triggerType === InlineCompletionTriggerKind.Automatic) { + this._triggerType = 'AutoTrigger' + } + } + + get triggerType(): string { + return this._triggerType + } + + // report client component latency after all pagination call finish + // and at least one suggestion is shown to the user + public tryRecordClientComponentLatency() { + if (this._firstSuggestionShowTime === 0 || this._allPaginationEndTime === 0) { + return + } + telemetry.codewhisperer_clientComponentLatency.emit({ + codewhispererAllCompletionsLatency: this._allPaginationEndTime - this._sdkApiCallStartTime, + codewhispererCompletionType: 'Line', + codewhispererCredentialFetchingLatency: 0, // no longer relevant, because we don't re-build the sdk. Flare already has that set + codewhispererCustomizationArn: getSelectedCustomization().arn, + codewhispererEndToEndLatency: this._firstSuggestionShowTime - this._invokeSuggestionStartTime, + codewhispererFirstCompletionLatency: this._sdkApiCallEndTime - this._sdkApiCallStartTime, + codewhispererLanguage: this._language, + codewhispererPostprocessingLatency: this._firstSuggestionShowTime - this._sdkApiCallEndTime, + codewhispererPreprocessingLatency: this._preprocessEndTime - this._invokeSuggestionStartTime, + codewhispererRequestId: this._firstResponseRequestId, + codewhispererSessionId: this._sessionId, + codewhispererTriggerType: this._triggerType, + credentialStartUrl: AuthUtil.instance.startUrl, + result: 'Succeeded', + }) + } +} From 5f6ee4ee9af907ac1a8c26251092dbce41afaed1 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Fri, 9 May 2025 11:06:10 -0400 Subject: [PATCH 17/48] Merge pull request #7277 from jpinkney-aws/closingBrackets ## Problem For some reason inline suggestions won't auto complete in function args e.g. ``` def getName( ``` or ``` def getName(firstName, ``` if you don't provide a range ## Solution provide a range similar to what the old implementation was doing --- packages/amazonq/src/app/inline/completion.ts | 9 +++++++-- .../test/unit/amazonq/apps/inline/completion.test.ts | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index f41d0aacf86..d70d979bd46 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -17,6 +17,7 @@ import { window, TextEditor, InlineCompletionTriggerKind, + Range, } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { @@ -228,10 +229,13 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem // get active item from session for displaying const items = this.sessionManager.getActiveRecommendation() const session = this.sessionManager.getActiveSession() - if (!session || !items.length) { + const editor = window.activeTextEditor + if (!session || !items.length || !editor) { return [] } - const editor = window.activeTextEditor + + const start = document.validatePosition(editor.selection.active) + const end = position for (const item of items) { item.command = { command: 'aws.amazonq.acceptInline', @@ -245,6 +249,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem session.firstCompletionDisplayLatency, ], } + item.range = new Range(start, end) ReferenceInlineProvider.instance.setInlineReference( position.line, item.insertText as string, diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 858f82b51cb..b0503c4fce7 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import sinon from 'sinon' -import { CancellationToken, commands, languages, Position } from 'vscode' +import { CancellationToken, commands, languages, Position, window } from 'vscode' import assert from 'assert' import { LanguageClient } from 'vscode-languageclient' import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion' @@ -291,6 +291,7 @@ describe('InlineCompletionManager', () => { getActiveRecommendationStub.returns(mockSuggestions) getAllRecommendationsStub = sandbox.stub(recommendationService, 'getAllRecommendations') getAllRecommendationsStub.resolves() + sandbox.stub(window, 'activeTextEditor').value(createMockTextEditor()) }), it('should call recommendation service to get new suggestions for new sessions', async () => { provider = new AmazonQInlineCompletionItemProvider( From 4b52ff439639e326168378f004a6d788075161da Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Fri, 9 May 2025 12:40:14 -0400 Subject: [PATCH 18/48] test(amazonq): update inline tests for flare (#7274) ## Problem inline tests don't work with the language server ## Solution - I removed the `${name} invoke on unsupported filetype` test because you can't trigger inline completions at all for unsupported filetypes (vscode doesn't allow it -- you need to specify file extentions) - hack: I have no way to spy on actual inline completions so I added a global variable that lets the tests know when recommendations are being generated :/. This allows us to wait for before accepting/rejecting requests - hack: codewhisperer_perceivedLatency, codewhisperer_serviceInvocation don't have the result field set in flare so it causes a lot of noisy logs. TODO add the result field there --- - 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/app/inline/completion.ts | 104 ++++++++++-------- .../amazonq/test/e2e/inline/inline.test.ts | 54 +-------- .../core/src/codewhisperer/models/model.ts | 7 ++ .../src/shared/telemetry/exemptMetrics.ts | 2 + 4 files changed, 71 insertions(+), 96 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index d70d979bd46..a8a7a70db0c 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -33,11 +33,13 @@ import { ReferenceLogViewProvider, ImportAdderProvider, CodeSuggestionsState, + vsCodeState, } from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' import { TelemetryHelper } from './telemetryHelper' +import { getLogger } from 'aws-core-vscode/shared' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -205,58 +207,66 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem context: InlineCompletionContext, token: CancellationToken ): Promise { - if (this.isNewSession) { - const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic - if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { - // return early when suggestions are disabled with auto trigger - return [] - } + try { + vsCodeState.isRecommendationsActive = true + if (this.isNewSession) { + const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic + if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { + // return early when suggestions are disabled with auto trigger + return [] + } - // tell the tutorial that completions has been triggered - await this.inlineTutorialAnnotation.triggered(context.triggerKind) - TelemetryHelper.instance.setInvokeSuggestionStartTime() - TelemetryHelper.instance.setTriggerType(context.triggerKind) + // tell the tutorial that completions has been triggered + await this.inlineTutorialAnnotation.triggered(context.triggerKind) + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setTriggerType(context.triggerKind) - // make service requests if it's a new session - await this.recommendationService.getAllRecommendations( - this.languageClient, - document, - position, - context, - token - ) - } - // get active item from session for displaying - const items = this.sessionManager.getActiveRecommendation() - const session = this.sessionManager.getActiveSession() - const editor = window.activeTextEditor - if (!session || !items.length || !editor) { - return [] - } + // make service requests if it's a new session + await this.recommendationService.getAllRecommendations( + this.languageClient, + document, + position, + context, + token + ) + } + // get active item from session for displaying + const items = this.sessionManager.getActiveRecommendation() + const session = this.sessionManager.getActiveSession() + const editor = window.activeTextEditor + if (!session || !items.length || !editor) { + return [] + } - const start = document.validatePosition(editor.selection.active) - const end = position - for (const item of items) { - item.command = { - command: 'aws.amazonq.acceptInline', - title: 'On acceptance', - arguments: [ - session.sessionId, - item, - editor, - session.requestStartTime, + const start = document.validatePosition(editor.selection.active) + const end = position + for (const item of items) { + item.command = { + command: 'aws.amazonq.acceptInline', + title: 'On acceptance', + arguments: [ + session.sessionId, + item, + editor, + session.requestStartTime, + position.line, + session.firstCompletionDisplayLatency, + ], + } + item.range = new Range(start, end) + ReferenceInlineProvider.instance.setInlineReference( position.line, - session.firstCompletionDisplayLatency, - ], + item.insertText as string, + item.references + ) + ImportAdderProvider.instance.onShowRecommendation(document, position.line, item) } - item.range = new Range(start, end) - ReferenceInlineProvider.instance.setInlineReference( - position.line, - item.insertText as string, - item.references - ) - ImportAdderProvider.instance.onShowRecommendation(document, position.line, item) + return items as InlineCompletionItem[] + } catch (e) { + getLogger('amazonqLsp').error('Failed to provide completion items: %O', e) + return [] + } finally { + vsCodeState.isRecommendationsActive = false } - return items as InlineCompletionItem[] } } diff --git a/packages/amazonq/test/e2e/inline/inline.test.ts b/packages/amazonq/test/e2e/inline/inline.test.ts index 72442b5cc6d..bcc41851eca 100644 --- a/packages/amazonq/test/e2e/inline/inline.test.ts +++ b/packages/amazonq/test/e2e/inline/inline.test.ts @@ -5,18 +5,10 @@ import * as vscode from 'vscode' import assert from 'assert' -import { - closeAllEditors, - getTestWindow, - registerAuthHook, - resetCodeWhispererGlobalVariables, - TestFolder, - toTextEditor, - using, -} from 'aws-core-vscode/test' -import { session } from 'aws-core-vscode/codewhisperer' +import { closeAllEditors, registerAuthHook, TestFolder, toTextEditor, using } from 'aws-core-vscode/test' import { Commands, globals, sleep, waitUntil, collectionUtil } from 'aws-core-vscode/shared' import { loginToIdC } from '../amazonq/utils/setup' +import { vsCodeState } from 'aws-core-vscode/codewhisperer' describe('Amazon Q Inline', async function () { const retries = 3 @@ -40,7 +32,6 @@ describe('Amazon Q Inline', async function () { const folder = await TestFolder.create() tempFolder = folder.path await closeAllEditors() - await resetCodeWhispererGlobalVariables() }) afterEach(async function () { @@ -70,17 +61,6 @@ describe('Amazon Q Inline', async function () { }) } - async function waitForRecommendations() { - const suggestionShown = await waitUntil(async () => session.getSuggestionState(0) === 'Showed', waitOptions) - if (!suggestionShown) { - throw new Error(`Suggestion did not show. Suggestion States: ${JSON.stringify(session.suggestionStates)}`) - } - console.table({ - 'suggestions states': JSON.stringify(session.suggestionStates), - recommendations: session.recommendations, - }) - } - /** * Waits for a specific telemetry event to be emitted with the expected suggestion state. * It looks like there might be a potential race condition in codewhisperer causing telemetry @@ -134,8 +114,9 @@ describe('Amazon Q Inline', async function () { await invokeCompletion() originalEditorContents = vscode.window.activeTextEditor?.document.getText() - // wait until the ghost text appears - await waitForRecommendations() + // wait until all the recommendations have finished + await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === true), waitOptions) + await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === false), waitOptions) } beforeEach(async () => { @@ -148,14 +129,12 @@ describe('Amazon Q Inline', async function () { try { await setup() console.log(`test run ${attempt} succeeded`) - logUserDecisionStatus() break } catch (e) { console.log(`test run ${attempt} failed`) console.log(e) logUserDecisionStatus() attempt++ - await resetCodeWhispererGlobalVariables() } } if (attempt === retries) { @@ -201,29 +180,6 @@ describe('Amazon Q Inline', async function () { assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents) }) }) - - it(`${name} invoke on unsupported filetype`, async function () { - await setupEditor({ - name: 'test.zig', - contents: `fn doSomething() void { - - }`, - }) - - /** - * Add delay between editor loading and invoking completion - * @see beforeEach in supported filetypes for more information - */ - await sleep(1000) - await invokeCompletion() - - if (name === 'automatic') { - // It should never get triggered since its not a supported file type - // assert.deepStrictEqual(RecommendationService.instance.isRunning, false) - } else { - await getTestWindow().waitForMessage('currently not supported by Amazon Q inline suggestions') - } - }) }) } }) diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 28072249371..d908b521062 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -33,6 +33,10 @@ interface VsCodeState { * Flag indicates whether codewhisperer is doing vscode.TextEditor.edit */ isCodeWhispererEditing: boolean + /** + * Keeps track of whether or not recommendations are currently running + */ + isRecommendationsActive: boolean /** * Timestamp of previous user edit */ @@ -44,6 +48,9 @@ interface VsCodeState { export const vsCodeState: VsCodeState = { isIntelliSenseActive: false, isCodeWhispererEditing: false, + // hack to globally keep track of whether or not recommendations are currently running. This allows us to know + // when recommendations have ran during e2e tests + isRecommendationsActive: false, lastUserModificationTime: 0, isFreeTierLimitReached: false, } diff --git a/packages/core/src/shared/telemetry/exemptMetrics.ts b/packages/core/src/shared/telemetry/exemptMetrics.ts index a3fc8d5ad78..4e0deacc058 100644 --- a/packages/core/src/shared/telemetry/exemptMetrics.ts +++ b/packages/core/src/shared/telemetry/exemptMetrics.ts @@ -29,6 +29,8 @@ const validationExemptMetrics: Set = new Set([ 'codewhisperer_codePercentage', 'codewhisperer_userModification', 'codewhisperer_userTriggerDecision', + 'codewhisperer_perceivedLatency', // flare doesn't currently set result property + 'codewhisperer_serviceInvocation', // flare doesn't currently set result property 'dynamicresource_selectResources', 'dynamicresource_copyIdentifier', 'dynamicresource_mutateResource', From 0429352dfd7d05ed642902adf19f560983a75e8d Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Mon, 12 May 2025 16:11:35 -0400 Subject: [PATCH 19/48] fix(amazonq): debounce inline suggestion requests. (#7289) ## Problem When typing a decent amount of text in quick succession, the language server will get throttled in its requests to the backend. This is because we send a request for recommendations on every key stroke, causing the language server to make a request on each key stroke. This is rightfully getting throttled by the backend. ## Solution - The ideal behavior is that we only make a request to the language server, and thus to the backend, when typing stops. Therefore, this is an ideal use case for `debounce`. However, we need to extend debounce slightly outlined below. - Apply `debounce` to the recommendations such that we wait 20 ms after typing stops before fetching the results. - By applying this at the recommendation level, none of the inline latency metrics are affected. ### Debounce Changes -Let f be some debounced function that takes a string argument, our current debounce does the following: ``` f('a') f('ab') f('abc') (pause for debounce delay) -> f would be called with 'a' ``` The issue is is that for suggestions, this means the language server request will be made with stale context (i.e. not including our most recent content). What we want instead is for the case above to call f with `'abc'` and not with `'a'` or `'ab'`. We can accomplish this by adding a flag to `debounce` allowing us to choose whether we call it with the first args of the debounce interval (default, and 'a' in the example above), or the most recent args ('abc' in the example above). ## Verification - I did not notice the added latency when testing inline. However it does seem slower than prod, with and without this change. - I was not able to get a throttling exception. --- - 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. --- .../src/app/inline/recommendationService.ts | 7 +- .../apps/inline/recommendationService.test.ts | 139 +++++++++++++++++- .../src/codewhisperer/models/constants.ts | 10 +- .../src/shared/utilities/functionUtils.ts | 13 +- .../shared/utilities/functionUtils.test.ts | 27 ++++ 5 files changed, 181 insertions(+), 15 deletions(-) diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 461504f8db2..403ac11a9e9 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -12,8 +12,9 @@ import { CancellationToken, InlineCompletionContext, Position, TextDocument } fr import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' import { InlineGeneratingMessage } from './inlineGeneratingMessage' -import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' +import { CodeWhispererStatusBarManager, inlineCompletionsDebounceDelay } from 'aws-core-vscode/codewhisperer' import { TelemetryHelper } from './telemetryHelper' +import { debounce } from 'aws-core-vscode/utils' export class RecommendationService { constructor( @@ -21,7 +22,9 @@ export class RecommendationService { private readonly inlineGeneratingMessage: InlineGeneratingMessage ) {} - async getAllRecommendations( + getAllRecommendations = debounce(this._getAllRecommendations.bind(this), inlineCompletionsDebounceDelay, true) + + private async _getAllRecommendations( languageClient: LanguageClient, document: TextDocument, position: Position, diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index 3b894b47b71..f7f344b895b 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -9,9 +9,10 @@ import { Position, CancellationToken, InlineCompletionItem } from 'vscode' import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' -import { createMockDocument } from 'aws-core-vscode/test' +import { createMockDocument, installFakeClock } from 'aws-core-vscode/test' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' +import { inlineCompletionsDebounceDelay } from 'aws-core-vscode/codewhisperer' describe('RecommendationService', () => { let languageClient: LanguageClient @@ -119,5 +120,141 @@ describe('RecommendationService', () => { const items2 = sessionManager.getActiveRecommendation() assert.deepStrictEqual(items2, [mockInlineCompletionItemTwo, { insertText: '1' } as InlineCompletionItem]) }) + + describe('debounce functionality', () => { + let clock: ReturnType + + beforeEach(() => { + clock = installFakeClock() + }) + + afterEach(() => { + clock.uninstall() + }) + + it('debounces multiple rapid calls', async () => { + const mockResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockResult) + + // Make multiple rapid calls + const promise1 = service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken + ) + const promise2 = service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken + ) + const promise3 = service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken + ) + + // Verify that the promises are the same object (debounced) + assert.strictEqual(promise1, promise2) + assert.strictEqual(promise2, promise3) + + await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) + + await promise1 + await promise2 + await promise3 + }) + + it('allows new calls after debounce period', async () => { + const mockResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockResult) + + const promise1 = service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken + ) + + await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) + + await promise1 + + const promise2 = service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken + ) + + assert.notStrictEqual( + promise1, + promise2, + 'promises should be different when seperated by debounce period' + ) + + await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) + + await promise2 + }) + + it('makes request with the last call', async () => { + const mockResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockResult) + + const promise1 = service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken + ) + + const promise2 = service.getAllRecommendations( + languageClient, + mockDocument, + { line: 2, character: 2 } as Position, + mockContext, + mockToken + ) + + await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) + + await promise1 + await promise2 + + const expectedRequestArgs = { + textDocument: { + uri: 'file:///test.py', + }, + position: { line: 2, character: 2 } as Position, + context: mockContext, + } + const firstCallArgs = sendRequestStub.firstCall.args[1] + assert.deepStrictEqual(firstCallArgs, expectedRequestArgs) + }) + }) }) }) diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index dc0426376ce..a6a89309d27 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -194,15 +194,9 @@ export const securityScanLearnMoreUri = 'https://docs.aws.amazon.com/amazonq/lat export const identityPoolID = 'us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9' /** - * the interval of the background thread invocation, which is triggered by the timer + * Delay for making requests once the user stops typing. Without a delay, inline suggestions request is triggered every keystroke. */ -export const defaultCheckPeriodMillis = 1000 * 60 * 5 - -// suggestion show delay, in milliseconds -export const suggestionShowDelay = 250 - -// add 200ms more delay on top of inline default 30-50ms -export const inlineSuggestionShowDelay = 200 +export const inlineCompletionsDebounceDelay = 25 export const referenceLog = 'Code Reference Log' diff --git a/packages/core/src/shared/utilities/functionUtils.ts b/packages/core/src/shared/utilities/functionUtils.ts index cbf89340ade..214721b1cdb 100644 --- a/packages/core/src/shared/utilities/functionUtils.ts +++ b/packages/core/src/shared/utilities/functionUtils.ts @@ -93,9 +93,10 @@ export function memoize(fn: (...args: U) => T): (...args: U) */ export function debounce( cb: (...args: Input) => Output | Promise, - delay: number = 0 + delay: number = 0, + useLastCall: boolean = false ): (...args: Input) => Promise { - return cancellableDebounce(cb, delay).promise + return cancellableDebounce(cb, delay, useLastCall).promise } /** @@ -104,10 +105,12 @@ export function debounce( */ export function cancellableDebounce( cb: (...args: Input) => Output | Promise, - delay: number = 0 + delay: number = 0, + useLastCall: boolean = false ): { promise: (...args: Input) => Promise; cancel: () => void } { let timeout: Timeout | undefined let promise: Promise | undefined + let lastestArgs: Input | undefined const cancel = (): void => { if (timeout) { @@ -119,6 +122,7 @@ export function cancellableDebounce( return { promise: (...args: Input) => { + lastestArgs = args timeout?.refresh() return (promise ??= new Promise((resolve, reject) => { @@ -126,7 +130,8 @@ export function cancellableDebounce( timeout.onCompletion(async () => { timeout = promise = undefined try { - resolve(await cb(...args)) + const argsToUse = useLastCall ? lastestArgs! : args + resolve(await cb(...argsToUse)) } catch (err) { reject(err) } diff --git a/packages/core/src/test/shared/utilities/functionUtils.test.ts b/packages/core/src/test/shared/utilities/functionUtils.test.ts index 7880d11ff63..b675fe74feb 100644 --- a/packages/core/src/test/shared/utilities/functionUtils.test.ts +++ b/packages/core/src/test/shared/utilities/functionUtils.test.ts @@ -152,6 +152,33 @@ describe('debounce', function () { assert.strictEqual(counter, 2) }) + describe('useLastCall option', function () { + let args: number[] + let clock: ReturnType + let addToArgs: (i: number) => void + + before(function () { + args = [] + clock = installFakeClock() + addToArgs = (n: number) => args.push(n) + }) + + afterEach(function () { + clock.uninstall() + args.length = 0 + }) + + it('only calls with the last args', async function () { + const debounced = debounce(addToArgs, 10, true) + const p1 = debounced(1) + const p2 = debounced(2) + const p3 = debounced(3) + await clock.tickAsync(100) + await Promise.all([p1, p2, p3]) + assert.deepStrictEqual(args, [3]) + }) + }) + describe('window rolling', function () { let clock: ReturnType const calls: ReturnType[] = [] From 2f7c3d9093ae38ad62207fd28df04e7c3f0561de Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Tue, 13 May 2025 10:12:39 -0400 Subject: [PATCH 20/48] refactor(amazonq): move debounce to top level. (#7292) ## Problem Adding logging statements to the top level is extremely noisy, since that part still triggers on each key stroke. This could also improve latency since any computation at the top-level will be redone on each keystroke. ## Solution - move the debounce up a layer. - remove outdated tests, since the debounce util already has tests for all this logic. - added new tests for new behavior. ## Verification - I tested this side-by-side with previous version and didn't notice a difference. --- - 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/app/inline/completion.ts | 10 +- .../src/app/inline/recommendationService.ts | 7 +- .../amazonq/apps/inline/completion.test.ts | 55 ++++++- .../apps/inline/recommendationService.test.ts | 139 +----------------- .../src/codewhisperer/models/constants.ts | 2 +- 5 files changed, 66 insertions(+), 147 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index a8a7a70db0c..06641a20463 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -34,12 +34,14 @@ import { ImportAdderProvider, CodeSuggestionsState, vsCodeState, + inlineCompletionsDebounceDelay, } from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' import { TelemetryHelper } from './telemetryHelper' import { getLogger } from 'aws-core-vscode/shared' +import { debounce } from 'aws-core-vscode/utils' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -201,7 +203,13 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem private readonly isNewSession: boolean = true ) {} - async provideInlineCompletionItems( + provideInlineCompletionItems = debounce( + this._provideInlineCompletionItems.bind(this), + inlineCompletionsDebounceDelay, + true + ) + + private async _provideInlineCompletionItems( document: TextDocument, position: Position, context: InlineCompletionContext, diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 403ac11a9e9..461504f8db2 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -12,9 +12,8 @@ import { CancellationToken, InlineCompletionContext, Position, TextDocument } fr import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' import { InlineGeneratingMessage } from './inlineGeneratingMessage' -import { CodeWhispererStatusBarManager, inlineCompletionsDebounceDelay } from 'aws-core-vscode/codewhisperer' +import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' import { TelemetryHelper } from './telemetryHelper' -import { debounce } from 'aws-core-vscode/utils' export class RecommendationService { constructor( @@ -22,9 +21,7 @@ export class RecommendationService { private readonly inlineGeneratingMessage: InlineGeneratingMessage ) {} - getAllRecommendations = debounce(this._getAllRecommendations.bind(this), inlineCompletionsDebounceDelay, true) - - private async _getAllRecommendations( + async getAllRecommendations( languageClient: LanguageClient, document: TextDocument, position: Position, diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index b0503c4fce7..1191dfc8319 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ import sinon from 'sinon' -import { CancellationToken, commands, languages, Position, window } from 'vscode' +import { CancellationToken, commands, InlineCompletionItem, languages, Position, window } from 'vscode' import assert from 'assert' import { LanguageClient } from 'vscode-languageclient' import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' -import { createMockDocument, createMockTextEditor } from 'aws-core-vscode/test' +import { createMockDocument, createMockTextEditor, installFakeClock } from 'aws-core-vscode/test' import { ReferenceHoverProvider, ReferenceInlineProvider, @@ -343,6 +343,57 @@ describe('InlineCompletionManager', () => { fakeReferences ) ) + }), + describe('debounce behavior', function () { + let clock: ReturnType + + beforeEach(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + it('should only trigger once on rapid events', async () => { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + false + ) + const p1 = provider.provideInlineCompletionItems( + mockDocument, + mockPosition, + mockContext, + mockToken + ) + const p2 = provider.provideInlineCompletionItems( + mockDocument, + mockPosition, + mockContext, + mockToken + ) + const p3 = provider.provideInlineCompletionItems( + mockDocument, + new Position(2, 2), + mockContext, + mockToken + ) + + await clock.tickAsync(1000) + + // All promises should be the same object when debounced properly. + assert.strictEqual(p1, p2) + assert.strictEqual(p1, p3) + await p1 + await p2 + const r3 = await p3 + + // calls the function with the latest provided args. + assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(2, 2)) + }) }) }) }) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index f7f344b895b..3b894b47b71 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -9,10 +9,9 @@ import { Position, CancellationToken, InlineCompletionItem } from 'vscode' import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' -import { createMockDocument, installFakeClock } from 'aws-core-vscode/test' +import { createMockDocument } from 'aws-core-vscode/test' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' -import { inlineCompletionsDebounceDelay } from 'aws-core-vscode/codewhisperer' describe('RecommendationService', () => { let languageClient: LanguageClient @@ -120,141 +119,5 @@ describe('RecommendationService', () => { const items2 = sessionManager.getActiveRecommendation() assert.deepStrictEqual(items2, [mockInlineCompletionItemTwo, { insertText: '1' } as InlineCompletionItem]) }) - - describe('debounce functionality', () => { - let clock: ReturnType - - beforeEach(() => { - clock = installFakeClock() - }) - - afterEach(() => { - clock.uninstall() - }) - - it('debounces multiple rapid calls', async () => { - const mockResult = { - sessionId: 'test-session', - items: [mockInlineCompletionItemOne], - partialResultToken: undefined, - } - - sendRequestStub.resolves(mockResult) - - // Make multiple rapid calls - const promise1 = service.getAllRecommendations( - languageClient, - mockDocument, - mockPosition, - mockContext, - mockToken - ) - const promise2 = service.getAllRecommendations( - languageClient, - mockDocument, - mockPosition, - mockContext, - mockToken - ) - const promise3 = service.getAllRecommendations( - languageClient, - mockDocument, - mockPosition, - mockContext, - mockToken - ) - - // Verify that the promises are the same object (debounced) - assert.strictEqual(promise1, promise2) - assert.strictEqual(promise2, promise3) - - await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) - - await promise1 - await promise2 - await promise3 - }) - - it('allows new calls after debounce period', async () => { - const mockResult = { - sessionId: 'test-session', - items: [mockInlineCompletionItemOne], - partialResultToken: undefined, - } - - sendRequestStub.resolves(mockResult) - - const promise1 = service.getAllRecommendations( - languageClient, - mockDocument, - mockPosition, - mockContext, - mockToken - ) - - await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) - - await promise1 - - const promise2 = service.getAllRecommendations( - languageClient, - mockDocument, - mockPosition, - mockContext, - mockToken - ) - - assert.notStrictEqual( - promise1, - promise2, - 'promises should be different when seperated by debounce period' - ) - - await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) - - await promise2 - }) - - it('makes request with the last call', async () => { - const mockResult = { - sessionId: 'test-session', - items: [mockInlineCompletionItemOne], - partialResultToken: undefined, - } - - sendRequestStub.resolves(mockResult) - - const promise1 = service.getAllRecommendations( - languageClient, - mockDocument, - mockPosition, - mockContext, - mockToken - ) - - const promise2 = service.getAllRecommendations( - languageClient, - mockDocument, - { line: 2, character: 2 } as Position, - mockContext, - mockToken - ) - - await clock.tickAsync(inlineCompletionsDebounceDelay + 1000) - - await promise1 - await promise2 - - const expectedRequestArgs = { - textDocument: { - uri: 'file:///test.py', - }, - position: { line: 2, character: 2 } as Position, - context: mockContext, - } - const firstCallArgs = sendRequestStub.firstCall.args[1] - assert.deepStrictEqual(firstCallArgs, expectedRequestArgs) - }) - }) }) }) diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index a6a89309d27..d2835a42e89 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -196,7 +196,7 @@ export const identityPoolID = 'us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9' /** * Delay for making requests once the user stops typing. Without a delay, inline suggestions request is triggered every keystroke. */ -export const inlineCompletionsDebounceDelay = 25 +export const inlineCompletionsDebounceDelay = 200 export const referenceLog = 'Code Reference Log' From 7bb3dac25d2f785e8452ab349a2a65148d566be9 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <43144436+aws-toolkit-automation@users.noreply.github.com> Date: Tue, 13 May 2025 13:59:27 -0400 Subject: [PATCH 21/48] Merge master into feature/flare-inline (#7261) ## Automatic merge failed - Resolve conflicts and push to this PR branch. - **Do not squash-merge** this PR. Use the "Create a merge commit" option to do a regular merge. ## Command line hint To perform the merge from the command line, you could do something like the following (where "origin" is the name of the remote in your local git repo): ``` git stash git fetch --all git checkout origin/feature/flare-inline git merge origin/master git commit git push origin HEAD:refs/heads/autoMerge/feature/flare-inline ``` --------- Signed-off-by: nkomonen-amazon Co-authored-by: aws-toolkit-automation <> Co-authored-by: Tom Zu <138054255+tomcat323@users.noreply.github.com> Co-authored-by: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Co-authored-by: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Co-authored-by: Tai Lai Co-authored-by: nkomonen-amazon Co-authored-by: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Co-authored-by: Josh Pinkney Co-authored-by: Adam Khamis <110852798+akhamis-amzn@users.noreply.github.com> Co-authored-by: Na Yue Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> Co-authored-by: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Co-authored-by: Jiatong Li Co-authored-by: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Co-authored-by: Brad Skaggs <126105424+brdskggs@users.noreply.github.com> Co-authored-by: hkobew --- package-lock.json | 13 +++-- package.json | 2 +- packages/amazonq/.changes/1.66.0.json | 14 ++++++ ...-9b0e6490-39a8-445f-9d67-9d762de7421c.json | 4 ++ packages/amazonq/CHANGELOG.md | 5 ++ packages/amazonq/package.json | 2 +- packages/amazonq/src/lsp/chat/messages.ts | 1 + packages/amazonq/src/lsp/client.ts | 15 ++++-- .../util/runtimeLanguageContext.test.ts | 34 +++++++++++++ .../webview/ui/quickActions/handler.ts | 2 + .../amazonqFeatureDev/session/sessionState.ts | 8 +++ .../src/codewhisperer/models/constants.ts | 3 ++ .../codewhisperer/util/customizationUtil.ts | 13 ++++- .../util/runtimeLanguageContext.ts | 50 +++++++++++++++++++ .../src/shared/telemetry/vscodeTelemetry.json | 9 ++++ packages/toolkit/.changes/3.60.0.json | 5 ++ packages/toolkit/CHANGELOG.md | 4 ++ packages/toolkit/package.json | 2 +- 18 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 packages/amazonq/.changes/1.66.0.json create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json create mode 100644 packages/toolkit/.changes/3.60.0.json diff --git a/package-lock.json b/package-lock.json index c5a890c2260..e5f1e7e7a70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.317", + "@aws-toolkits/telemetry": "^1.0.318", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -10760,11 +10760,10 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.317", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.317.tgz", - "integrity": "sha512-QFLBFfHZjuB2pBd1p0Tn/GMKTYYQu3/nrlj0Co7EkqozvDNDG0nTjxtkXxotbwjrqVD5Sv8i46gEdgsyQ7at3w==", + "version": "1.0.318", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.318.tgz", + "integrity": "sha512-L64GJ+KRN0fdTIx1CPIbbgBeFcg9DilsIxfjeZyod7ld0mw6he70rPopBtK4jP+pTEkfUE4wTRsaco1nWXz3+w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "ajv": "^6.12.6", "cross-spawn": "^7.0.6", @@ -26384,7 +26383,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.66.0-SNAPSHOT", + "version": "1.67.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -28098,7 +28097,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.60.0-SNAPSHOT", + "version": "3.61.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/package.json b/package.json index 30f0497cdb2..f4b31c22d83 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.317", + "@aws-toolkits/telemetry": "^1.0.318", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", diff --git a/packages/amazonq/.changes/1.66.0.json b/packages/amazonq/.changes/1.66.0.json new file mode 100644 index 00000000000..ab4a819b85a --- /dev/null +++ b/packages/amazonq/.changes/1.66.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-05-09", + "version": "1.66.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Avoid inline completion 'Improperly formed request' errors when file is too large" + }, + { + "type": "Bug Fix", + "description": "Named agent tabs sometimes open with unnecessary input options" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json b/packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json new file mode 100644 index 00000000000..f17516bb8f4 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Previous and subsequent cells are used as context for completion in a Jupyter notebook" +} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index b5ceba33c7c..197aecdfdf6 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.66.0 2025-05-09 + +- **Bug Fix** Avoid inline completion 'Improperly formed request' errors when file is too large +- **Bug Fix** Named agent tabs sometimes open with unnecessary input options + ## 1.65.0 2025-05-05 - **Feature** Support selecting customizations across all Q profiles with automatic profile switching for enterprise users diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 0553b16a973..f9d466f5767 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.66.0-SNAPSHOT", + "version": "1.67.0-SNAPSHOT", "extensionKind": [ "workspace" ], diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 887c2ae4c72..5eab1aa17db 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -103,6 +103,7 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie break } } + languageClient.info(`[Telemetry] Emitting ${telemetryName} telemetry: ${JSON.stringify(e.data)}`) telemetry[telemetryName as keyof TelemetryBase].emit(e.data) } }) diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index c96b177f870..0884c885607 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -5,7 +5,6 @@ import vscode, { env, version } from 'vscode' import * as nls from 'vscode-nls' -import * as crypto from 'crypto' import { LanguageClient, LanguageClientOptions, RequestType, State } from 'vscode-languageclient' import { InlineCompletionManager } from '../app/inline/completion' import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth' @@ -39,6 +38,8 @@ import { getOptOutPreference, isAmazonInternalOs, fs, + getClientId, + extensionVersion, } from 'aws-core-vscode/shared' import { processUtils } from 'aws-core-vscode/shared' import { activate } from './chat/activation' @@ -129,9 +130,9 @@ export async function startLanguageServer( version: version, extension: { name: 'AmazonQ-For-VSCode', - version: '0.0.1', + version: extensionVersion, }, - clientId: crypto.randomUUID(), + clientId: getClientId(globals.globalState), }, awsClientCapabilities: { q: { @@ -143,7 +144,7 @@ export async function startLanguageServer( }, }, contextConfiguration: { - workspaceIdentifier: extensionContext.storageUri, + workspaceIdentifier: extensionContext.storageUri?.path, }, logLevel: toAmazonQLSPLogLevel(globals.logOutputChannel.logLevel), }, @@ -336,6 +337,12 @@ function onServerRestartHandler(client: LanguageClient, auth: AmazonQLspAuth) { return } + // Emit telemetry that a crash was detected. + // It is not guaranteed to 100% be a crash since somehow the server may have been intentionally restarted, + // but most of the time it probably will have been due to a crash. + // TODO: Port this metric override to common definitions + telemetry.languageServer_crash.emit({ id: 'AmazonQ' }) + // Need to set the auth token in the again await auth.refreshConnection(true) }) diff --git a/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts b/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts index 59c3771abb4..a5cc430a5a9 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts @@ -333,6 +333,40 @@ describe('runtimeLanguageContext', function () { } }) + describe('getSingleLineCommentPrefix', function () { + it('should return the correct comment prefix for supported languages', function () { + assert.strictEqual(languageContext.getSingleLineCommentPrefix('java'), '// ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('javascript'), '// ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('jsonc'), '// ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('kotlin'), '// ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('lua'), '-- ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('python'), '# ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('ruby'), '# ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('sql'), '-- ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('tf'), '# ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('typescript'), '// ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('vue'), '') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('yaml'), '# ') + }) + + it('should normalize language ID before getting comment prefix', function () { + assert.strictEqual(languageContext.getSingleLineCommentPrefix('hcl'), '# ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('javascriptreact'), '// ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('shellscript'), '# ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('typescriptreact'), '// ') + assert.strictEqual(languageContext.getSingleLineCommentPrefix('yml'), '# ') + }) + + it('should return empty string for unsupported languages', function () { + assert.strictEqual(languageContext.getSingleLineCommentPrefix('nonexistent'), '') + assert.strictEqual(languageContext.getSingleLineCommentPrefix(undefined), '') + }) + + it('should return empty string for plaintext', function () { + assert.strictEqual(languageContext.getSingleLineCommentPrefix('plaintext'), '') + }) + }) + // for now we will only jsx mapped to javascript, tsx mapped to typescript, all other language should remain the same describe('test covertCwsprRequest', function () { const leftFileContent = 'left' diff --git a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts index fe124d1fc0c..f0d707247e9 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts @@ -355,6 +355,8 @@ export class QuickActionHandler { loadingChat: true, cancelButtonWhenLoading: false, }) + } else { + this.mynahUI.updateStore(affectedTabId, { promptInputOptions: [] }) } if (affectedTabId && this.isHybridChatEnabled) { diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts index 5890539409f..5879c16493f 100644 --- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts +++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts @@ -205,6 +205,14 @@ export class FeatureDevCodeGenState extends BaseCodeGenState { 429 ) } + case codegenResult.codeGenerationStatusDetail?.includes('FileCreationFailed'): { + return new ApiServiceError( + i18n('AWS.amazonq.featureDev.error.codeGen.default'), + 'GetTaskAssistCodeGeneration', + 'FileCreationFailedException', + 500 + ) + } default: { return new ApiServiceError( i18n('AWS.amazonq.featureDev.error.codeGen.default'), diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index d2835a42e89..2a2c84b4866 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -87,6 +87,9 @@ export const lineBreakWin = '\r\n' export const supplementalContextTimeoutInMs = 100 export const supplementalContextMaxTotalLength = 20480 + +export const editorStateMaxLength = 40000 + /** * Ux of recommendations */ diff --git a/packages/core/src/codewhisperer/util/customizationUtil.ts b/packages/core/src/codewhisperer/util/customizationUtil.ts index d989fadf05c..600317c53e0 100644 --- a/packages/core/src/codewhisperer/util/customizationUtil.ts +++ b/packages/core/src/codewhisperer/util/customizationUtil.ts @@ -59,7 +59,7 @@ export const onProfileChangedListener: (event: ProfileChangedEvent) => any = asy if (event.intent === 'customization') { return } - + const logger = getLogger() if (!event.profile) { await setSelectedCustomization(baseCustomization) return @@ -69,7 +69,16 @@ export const onProfileChangedListener: (event: ProfileChangedEvent) => any = asy const selectedCustomization = getSelectedCustomization() // No need to validate base customization which has empty arn. if (selectedCustomization.arn.length > 0) { - await switchToBaseCustomizationAndNotify() + const customizationProvider = await CustomizationProvider.init(event.profile) + const customizations = await customizationProvider.listAvailableCustomizations() + + const r = customizations.find((it) => it.arn === selectedCustomization.arn) + if (!r) { + logger.debug( + `profile ${event.profile.name} doesnt have access to customization ${selectedCustomization.name} but has access to ${customizations.map((it) => it.name)}` + ) + await switchToBaseCustomizationAndNotify() + } } } diff --git a/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts b/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts index 9a495cf5356..3a1403b453e 100644 --- a/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts +++ b/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts @@ -58,6 +58,13 @@ export class RuntimeLanguageContext { */ private supportedLanguageExtensionMap: ConstantMap + /** + * A map storing single-line comment prefixes for different languages + * Key: CodewhispererLanguage + * Value: Comment prefix string + */ + private languageSingleLineCommentPrefixMap: ConstantMap + constructor() { this.supportedLanguageMap = createConstantMap< CodeWhispererConstants.PlatformLanguageId | CodewhispererLanguage, @@ -146,6 +153,39 @@ export class RuntimeLanguageContext { psm1: 'powershell', r: 'r', }) + this.languageSingleLineCommentPrefixMap = createConstantMap({ + c: '// ', + cpp: '// ', + csharp: '// ', + dart: '// ', + go: '// ', + hcl: '# ', + java: '// ', + javascript: '// ', + json: '// ', + jsonc: '// ', + jsx: '// ', + kotlin: '// ', + lua: '-- ', + php: '// ', + plaintext: '', + powershell: '# ', + python: '# ', + r: '# ', + ruby: '# ', + rust: '// ', + scala: '// ', + shell: '# ', + sql: '-- ', + swift: '// ', + systemVerilog: '// ', + tf: '# ', + tsx: '// ', + typescript: '// ', + vue: '', // vue lacks a single-line comment prefix + yaml: '# ', + yml: '# ', + }) } /** @@ -159,6 +199,16 @@ export class RuntimeLanguageContext { return this.supportedLanguageMap.get(languageId) } + /** + * Get the comment prefix for a given language + * @param language The language to get comment prefix for + * @returns The comment prefix string, or empty string if not found + */ + public getSingleLineCommentPrefix(language?: string): string { + const normalizedLanguage = this.normalizeLanguage(language) + return normalizedLanguage ? (this.languageSingleLineCommentPrefixMap.get(normalizedLanguage) ?? '') : '' + } + /** * Normalize client side language id to service aware language id (service is not aware of jsx/tsx) * Only used when invoking CodeWhisperer service API, for client usage please use normalizeLanguage diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index 4a5117ee252..b28aeec4847 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -1019,6 +1019,15 @@ } ] }, + { + "name": "languageServer_crash", + "description": "Called when a language server crash is detected. TODO: Port this to common", + "metadata": [ + { + "type": "id" + } + ] + }, { "name": "ide_heartbeat", "description": "A heartbeat sent by the extension", diff --git a/packages/toolkit/.changes/3.60.0.json b/packages/toolkit/.changes/3.60.0.json new file mode 100644 index 00000000000..2464e57a4b0 --- /dev/null +++ b/packages/toolkit/.changes/3.60.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-05-06", + "version": "3.60.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index 215c7c68cba..e21988fcd50 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.60.0 2025-05-06 + +- Miscellaneous non-user-facing changes + ## 3.59.0 2025-05-05 - Miscellaneous non-user-facing changes diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 0e6e24a17ae..80e554cf53a 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.60.0-SNAPSHOT", + "version": "3.61.0-SNAPSHOT", "extensionKind": [ "workspace" ], From b649dbd1292d162888258c001d4b2bfc46858bff Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <43144436+aws-toolkit-automation@users.noreply.github.com> Date: Wed, 14 May 2025 10:35:48 -0400 Subject: [PATCH 22/48] Merge master into feature/flare-inline (#7299) ## Automatic merge failed - Resolve conflicts and push to this PR branch. - **Do not squash-merge** this PR. Use the "Create a merge commit" option to do a regular merge. ## Command line hint To perform the merge from the command line, you could do something like the following (where "origin" is the name of the remote in your local git repo): ``` git stash git fetch --all git checkout origin/feature/flare-inline git merge origin/master git commit git push origin HEAD:refs/heads/autoMerge/feature/flare-inline ``` --------- Signed-off-by: nkomonen-amazon Co-authored-by: aws-toolkit-automation <> Co-authored-by: Tom Zu <138054255+tomcat323@users.noreply.github.com> Co-authored-by: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Co-authored-by: Tai Lai Co-authored-by: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Co-authored-by: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Co-authored-by: Adam Khamis <110852798+akhamis-amzn@users.noreply.github.com> Co-authored-by: Na Yue Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> Co-authored-by: Jiatong Li Co-authored-by: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Co-authored-by: Brad Skaggs <126105424+brdskggs@users.noreply.github.com> Co-authored-by: Zoe Lin <60411978+zixlin7@users.noreply.github.com> Co-authored-by: chungjac Co-authored-by: Lei Gao <97199248+leigaol@users.noreply.github.com> Co-authored-by: hkobew --- package-lock.json | 18 ++++++---------- package.json | 2 +- ...-bb976b5f-7697-42d8-89a9-8e96310a23f4.json | 4 ++++ packages/amazonq/src/extension.ts | 4 ++-- packages/amazonq/src/lsp/chat/activation.ts | 5 +++++ packages/amazonq/src/lsp/client.ts | 21 +++++++++---------- packages/core/src/shared/index.ts | 2 +- packages/core/src/shared/telemetry/util.ts | 4 ++-- packages/core/src/shared/vscode/env.ts | 14 ++++++------- .../core/src/test/shared/vscode/env.test.ts | 17 +++++++-------- 10 files changed, 44 insertions(+), 47 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-bb976b5f-7697-42d8-89a9-8e96310a23f4.json diff --git a/package-lock.json b/package-lock.json index e5f1e7e7a70..cff472d4104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.318", + "@aws-toolkits/telemetry": "^1.0.319", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -10760,10 +10760,11 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.318", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.318.tgz", - "integrity": "sha512-L64GJ+KRN0fdTIx1CPIbbgBeFcg9DilsIxfjeZyod7ld0mw6he70rPopBtK4jP+pTEkfUE4wTRsaco1nWXz3+w==", + "version": "1.0.321", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.321.tgz", + "integrity": "sha512-pL1TZOyREfEuZjvjhAPyb/6fOaPLlXMft4i1mbHJVs2rnJBKFAsJOl3osmCLKXuqiMT7jhmzOE8dRCkEuLleIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "ajv": "^6.12.6", "cross-spawn": "^7.0.6", @@ -10786,8 +10787,6 @@ }, "node_modules/@aws/chat-client-ui-types": { "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.1.26.tgz", - "integrity": "sha512-WlF0fP1nojueknr815dg6Ivs+Q3e5onvWTH1nI05jysSzUHjsWwFDBrsxqJXfaPIFhPrbQzHqoxHbhIwQ1OLuw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10829,8 +10828,6 @@ }, "node_modules/@aws/language-server-runtimes-types": { "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.26.tgz", - "integrity": "sha512-c63rpUbcrtLqaC33t6elRApQqLbQvFgKzIQ2z/VCavE5F7HSLBfzhHkhgUFd775fBpsF4MHrIzwNitYLhDGobw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11781,8 +11778,6 @@ }, "node_modules/@aws/mynah-ui": { "version": "4.30.3", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.30.3.tgz", - "integrity": "sha512-Xy22dzCaFUqpdSHMpLa8Dsq98DiAUq49dm7Iu8Yj2YZXSCyfKQiYMJOfwU8IoqeNcEney5JRMJpf+/RysWugbA==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { @@ -24693,9 +24688,8 @@ }, "node_modules/ts-node": { "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", diff --git a/package.json b/package.json index f4b31c22d83..493327237eb 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.318", + "@aws-toolkits/telemetry": "^1.0.319", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", diff --git a/packages/amazonq/.changes/next-release/Bug Fix-bb976b5f-7697-42d8-89a9-8e96310a23f4.json b/packages/amazonq/.changes/next-release/Bug Fix-bb976b5f-7697-42d8-89a9-8e96310a23f4.json new file mode 100644 index 00000000000..988fb2bcc7b --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-bb976b5f-7697-42d8-89a9-8e96310a23f4.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Support chat in AL2 aarch64" +} diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 9d9c3061e6b..a84c68fff4c 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -33,7 +33,7 @@ import { maybeShowMinVscodeWarning, Experiments, isSageMaker, - isAmazonInternalOs, + isAmazonLinux2, } from 'aws-core-vscode/shared' import { ExtStartUpSources } from 'aws-core-vscode/telemetry' import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' @@ -123,7 +123,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is await activateCodeWhisperer(extContext as ExtContext) if ( (Experiments.instance.get('amazonqLSP', true) || Auth.instance.isInternalAmazonUser()) && - (!isAmazonInternalOs() || (await hasGlibcPatch())) + (!isAmazonLinux2() || hasGlibcPatch()) ) { // start the Amazon Q LSP for internal users first // for AL2, start LSP if glibc patch is found diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index 9dd1d31c3de..3a36377b9b5 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -25,6 +25,11 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu type: 'profile', profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) + // We need to push the cached customization on startup explicitly + await pushConfigUpdate(languageClient, { + type: 'customization', + customization: getSelectedCustomization(), + }) const provider = new AmazonQChatViewProvider(mynahUIPath) diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 0884c885607..f5159328605 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -36,8 +36,7 @@ import { getLogger, undefinedIfEmpty, getOptOutPreference, - isAmazonInternalOs, - fs, + isAmazonLinux2, getClientId, extensionVersion, } from 'aws-core-vscode/shared' @@ -55,8 +54,11 @@ import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChat const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') -export async function hasGlibcPatch(): Promise { - return await fs.exists('/opt/vsc-sysroot/lib64/ld-linux-x86-64.so.2') +export const glibcLinker: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER || '' +export const glibcPath: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH || '' + +export function hasGlibcPatch(): boolean { + return glibcLinker.length > 0 && glibcPath.length > 0 } export async function startLanguageServer( @@ -81,13 +83,8 @@ export async function startLanguageServer( const traceServerEnabled = Settings.instance.isSet(`${clientId}.trace.server`) let executable: string[] = [] // apply the GLIBC 2.28 path to node js runtime binary - if (isAmazonInternalOs() && (await hasGlibcPatch())) { - executable = [ - '/opt/vsc-sysroot/lib64/ld-linux-x86-64.so.2', - '--library-path', - '/opt/vsc-sysroot/lib64', - resourcePaths.node, - ] + if (isAmazonLinux2() && hasGlibcPatch()) { + executable = [glibcLinker, '--library-path', glibcPath, resourcePaths.node] getLogger('amazonqLsp').info(`Patched node runtime with GLIBC to ${executable}`) } else { executable = [resourcePaths.node] @@ -380,6 +377,8 @@ function getConfigSection(section: ConfigSection) { includeSuggestionsWithCodeReferences: CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled(), shareCodeWhispererContentWithAWS: !CodeWhispererSettings.instance.isOptoutEnabled(), + includeImportsWithSuggestions: CodeWhispererSettings.instance.isImportRecommendationEnabled(), + sendUserWrittenCodeMetrics: true, }, ] case 'aws.logLevel': diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 2d23400c9bb..76e83258c12 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -18,7 +18,7 @@ export * from './extensionUtilities' export * from './extensionStartup' export { RegionProvider } from './regions/regionProvider' export { Commands } from './vscode/commands2' -export { getMachineId, getServiceEnvVarConfig, isAmazonInternalOs } from './vscode/env' +export { getMachineId, getServiceEnvVarConfig, isAmazonLinux2 } from './vscode/env' export { getLogger } from './logger/logger' export { activateExtension, openUrl } from './utilities/vsCodeUtils' export { waitUntil, sleep, Timeout } from './utilities/timeoutUtils' diff --git a/packages/core/src/shared/telemetry/util.ts b/packages/core/src/shared/telemetry/util.ts index 4d136bc96f0..310c36b82d6 100644 --- a/packages/core/src/shared/telemetry/util.ts +++ b/packages/core/src/shared/telemetry/util.ts @@ -15,7 +15,7 @@ import { isAutomation, isRemoteWorkspace, isCloudDesktop, - isAmazonInternalOs, + isAmazonLinux2, } from '../vscode/env' import { addTypeName } from '../utilities/typeConstructors' import globals, { isWeb } from '../extensionGlobals' @@ -290,7 +290,7 @@ export async function getComputeEnvType(): Promise { } else if (isSageMaker()) { return web ? 'sagemaker-web' : 'sagemaker' } else if (isRemoteWorkspace()) { - if (isAmazonInternalOs()) { + if (isAmazonLinux2()) { if (await isCloudDesktop()) { return 'cloudDesktop-amzn' } diff --git a/packages/core/src/shared/vscode/env.ts b/packages/core/src/shared/vscode/env.ts index 004db0efc27..02d46ae6695 100644 --- a/packages/core/src/shared/vscode/env.ts +++ b/packages/core/src/shared/vscode/env.ts @@ -125,23 +125,21 @@ export function isRemoteWorkspace(): boolean { } /** - * There is Amazon Linux 2, but additionally an Amazon Linux 2 Internal. - * The internal version is for Amazon employees only. And this version can - * be used by either EC2 OR CloudDesktop. It is not exclusive to either. + * There is Amazon Linux 2. * - * Use {@link isCloudDesktop()} to know if we are specifically using it. + * Use {@link isCloudDesktop()} to know if we are specifically using internal Amazon Linux 2. * - * Example: `5.10.220-188.869.amzn2int.x86_64` + * Example: `5.10.220-188.869.amzn2int.x86_64` or `5.10.236-227.928.amzn2.x86_64` (Cloud Dev Machine) */ -export function isAmazonInternalOs() { - return os.release().includes('amzn2int') && process.platform === 'linux' +export function isAmazonLinux2() { + return (os.release().includes('.amzn2int.') || os.release().includes('.amzn2.')) && process.platform === 'linux' } /** * Returns true if we are in an internal Amazon Cloud Desktop */ export async function isCloudDesktop() { - if (!isAmazonInternalOs()) { + if (!isAmazonLinux2()) { return false } diff --git a/packages/core/src/test/shared/vscode/env.test.ts b/packages/core/src/test/shared/vscode/env.test.ts index ef81bdf05ab..cf09d085e68 100644 --- a/packages/core/src/test/shared/vscode/env.test.ts +++ b/packages/core/src/test/shared/vscode/env.test.ts @@ -5,13 +5,7 @@ import assert from 'assert' import path from 'path' -import { - isCloudDesktop, - getEnvVars, - getServiceEnvVarConfig, - isAmazonInternalOs as isAmazonInternalOS, - isBeta, -} from '../../../shared/vscode/env' +import { isCloudDesktop, getEnvVars, getServiceEnvVarConfig, isAmazonLinux2, isBeta } from '../../../shared/vscode/env' import { ChildProcess } from '../../../shared/utilities/processUtils' import * as sinon from 'sinon' import os from 'os' @@ -103,13 +97,16 @@ describe('env', function () { assert.strictEqual(isBeta(), expected) }) - it('isAmazonInternalOS', function () { + it('isAmazonLinux2', function () { sandbox.stub(process, 'platform').value('linux') const versionStub = stubOsVersion('5.10.220-188.869.amzn2int.x86_64') - assert.strictEqual(isAmazonInternalOS(), true) + assert.strictEqual(isAmazonLinux2(), true) + + versionStub.returns('5.10.236-227.928.amzn2.x86_64') + assert.strictEqual(isAmazonLinux2(), true) versionStub.returns('5.10.220-188.869.NOT_INTERNAL.x86_64') - assert.strictEqual(isAmazonInternalOS(), false) + assert.strictEqual(isAmazonLinux2(), false) }) it('isCloudDesktop', async function () { From 692847a39be81347e3d828660420b134278557fe Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Wed, 14 May 2025 10:36:12 -0400 Subject: [PATCH 23/48] test(amazonq): add tests for edge cases (#7305) ## Problem There are two edge cases discovered during testing that warrant some tests. 1. the language server will respond without a range, causing the result to not be rendered. 2. the language server COULD respond with a `StringValue` instead of string (currently doesn't, but valid in the type contract). ## Solution - add tests for these cases, and do minor refactoring to make this easier. --- - 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/app/inline/completion.ts | 17 +++--- .../src/app/inline/recommendationService.ts | 4 +- .../amazonq/apps/inline/completion.test.ts | 58 ++++++++++++++++++- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 06641a20463..565736eb39d 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -8,7 +8,6 @@ import { InlineCompletionContext, InlineCompletionItem, InlineCompletionItemProvider, - InlineCompletionList, Position, TextDocument, commands, @@ -214,7 +213,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem position: Position, context: InlineCompletionContext, token: CancellationToken - ): Promise { + ): Promise { try { vsCodeState.isRecommendationsActive = true if (this.isNewSession) { @@ -246,8 +245,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem return [] } - const start = document.validatePosition(editor.selection.active) - const end = position + const cursorPosition = document.validatePosition(position) for (const item of items) { item.command = { command: 'aws.amazonq.acceptInline', @@ -257,17 +255,18 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem item, editor, session.requestStartTime, - position.line, + cursorPosition.line, session.firstCompletionDisplayLatency, ], } - item.range = new Range(start, end) + item.range = new Range(cursorPosition, cursorPosition) + item.insertText = typeof item.insertText === 'string' ? item.insertText : item.insertText.value ReferenceInlineProvider.instance.setInlineReference( - position.line, - item.insertText as string, + cursorPosition.line, + item.insertText, item.references ) - ImportAdderProvider.instance.onShowRecommendation(document, position.line, item) + ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item) } return items as InlineCompletionItem[] } catch (e) { diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 461504f8db2..ac0b16f140c 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -48,7 +48,7 @@ export class RecommendationService { // Handle first request const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType as any, + inlineCompletionWithReferencesRequestType.method, request, token ) @@ -98,7 +98,7 @@ export class RecommendationService { while (nextToken) { const request = { ...initialRequest, partialResultToken: nextToken } const result: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType as any, + inlineCompletionWithReferencesRequestType.method, request, token ) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 1191dfc8319..0fa83130ad5 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -3,9 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ import sinon from 'sinon' -import { CancellationToken, commands, InlineCompletionItem, languages, Position, window } from 'vscode' +import { CancellationToken, commands, InlineCompletionItem, languages, Position, window, Range } from 'vscode' import assert from 'assert' import { LanguageClient } from 'vscode-languageclient' +import { StringValue } from 'vscode-languageserver-types' import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' @@ -344,6 +345,61 @@ describe('InlineCompletionManager', () => { ) ) }), + it('should add a range to the completion item when missing', async function () { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + true + ) + getActiveRecommendationStub.returns([ + { + insertText: 'testText', + itemId: 'itemId', + }, + { + insertText: 'testText2', + itemId: 'itemId2', + range: undefined, + }, + ]) + const cursorPosition = new Position(5, 6) + const result = await provider.provideInlineCompletionItems( + mockDocument, + cursorPosition, + mockContext, + mockToken + ) + + for (const item of result) { + assert.deepStrictEqual(item.range, new Range(cursorPosition, cursorPosition)) + } + }), + it('should handle StringValue instead of strings', async function () { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + true + ) + const expectedText = 'this is my text' + getActiveRecommendationStub.returns([ + { + insertText: { kind: 'snippet', value: 'this is my text' } satisfies StringValue, + itemId: 'itemId', + }, + ]) + const result = await provider.provideInlineCompletionItems( + mockDocument, + mockPosition, + mockContext, + mockToken + ) + + assert.strictEqual(result[0].insertText, expectedText) + }), describe('debounce behavior', function () { let clock: ReturnType From ffb6ec136689c3f9c145b9783307331a886da157 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Wed, 14 May 2025 19:55:46 -0400 Subject: [PATCH 24/48] fix(amazonq): standardize reference type for reference log (#7311) ## Problem References are not added to the reference log at the bottom. The root cause is that the reference tracker log is expecting CW shaped references, but we are passing "Flare" shaped ones. ## Solution - Update the reference log to standardize the shape of incoming references. ## Testing and Verification - added unit test. - Verified e2e: https://github.com/user-attachments/assets/620f6f3d-1205-405e-96a2-322d5e8220c4 - --- - 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. --- .../service/referenceLogViewProvider.test.ts | 36 +++++++- .../service/referenceLogViewProvider.ts | 82 ++++++++++++++----- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts index 1c1b6322675..dcacf745a57 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/referenceLogViewProvider.test.ts @@ -5,7 +5,6 @@ import assert from 'assert' import { createMockTextEditor, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' import { ReferenceLogViewProvider, LicenseUtil } from 'aws-core-vscode/codewhisperer' - describe('referenceLogViewProvider', function () { beforeEach(async function () { await resetCodeWhispererGlobalVariables() @@ -66,4 +65,39 @@ describe('referenceLogViewProvider', function () { assert.ok(!actual.includes(LicenseUtil.getLicenseHtml('MIT'))) }) }) + + it('accepts references from CW and language server', async function () { + const cwReference = { + licenseName: 'MIT', + repository: 'TEST_REPO', + url: 'cw.com', + recommendationContentSpan: { + start: 0, + end: 10, + }, + } + + const flareReference = { + referenceName: 'test reference', + referenceUrl: 'flare.com', + licenseName: 'apache', + position: { + startCharacter: 0, + endCharacter: 10, + }, + } + + const actual = ReferenceLogViewProvider.getReferenceLog( + '', + [cwReference, flareReference], + createMockTextEditor() + ) + + assert.ok(actual.includes('MIT')) + assert.ok(actual.includes('apache')) + assert.ok(actual.includes('TEST_REPO')) + assert.ok(actual.includes('test reference')) + assert.ok(actual.includes('flare.com')) + assert.ok(actual.includes('cw.com')) + }) }) diff --git a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts index 9ec20b8cb44..d51424b1c46 100644 --- a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts @@ -4,13 +4,15 @@ */ import * as vscode from 'vscode' -import { References } from '../client/codewhisperer' import { LicenseUtil } from '../util/licenseUtil' import * as CodeWhispererConstants from '../models/constants' import { CodeWhispererSettings } from '../util/codewhispererSettings' import globals from '../../shared/extensionGlobals' import { AuthUtil } from '../util/authUtil' import { session } from '../util/codeWhispererSession' +import CodeWhispererClient from '../client/codewhispererclient' +import CodeWhispererUserClient from '../client/codewhispereruserclient' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes-types' export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.codeWhisperer.referenceLog' @@ -52,28 +54,23 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { } } - public static getReferenceLog(recommendation: string, references: References, editor: vscode.TextEditor): string { + public static getReferenceLog(recommendation: string, references: Reference[], editor: vscode.TextEditor): string { const filePath = editor.document.uri.path const time = new Date().toLocaleString() let text = `` for (const reference of references) { + const standardReference = toStandardReference(reference) if ( - reference.recommendationContentSpan === undefined || - reference.recommendationContentSpan.start === undefined || - reference.recommendationContentSpan.end === undefined + standardReference.position === undefined || + standardReference.position.start === undefined || + standardReference.position.end === undefined ) { continue } - const code = recommendation.substring( - reference.recommendationContentSpan.start, - reference.recommendationContentSpan.end - ) - const firstCharLineNumber = - editor.document.positionAt(session.startCursorOffset + reference.recommendationContentSpan.start).line + - 1 - const lastCharLineNumber = - editor.document.positionAt(session.startCursorOffset + reference.recommendationContentSpan.end - 1) - .line + 1 + const { start, end } = standardReference.position + const code = recommendation.substring(start, end) + const firstCharLineNumber = editor.document.positionAt(session.startCursorOffset + start).line + 1 + const lastCharLineNumber = editor.document.positionAt(session.startCursorOffset + end - 1).line + 1 let lineInfo = `` if (firstCharLineNumber === lastCharLineNumber) { lineInfo = `(line at ${firstCharLineNumber})` @@ -84,11 +81,11 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { text += `And ` } - let license = `${reference.licenseName}` - let repository = reference.repository?.length ? reference.repository : 'unknown' - if (reference.url?.length) { - repository = `${reference.repository}` - license = `${reference.licenseName || 'unknown'}` + let license = `${standardReference.licenseName}` + let repository = standardReference.repository?.length ? standardReference.repository : 'unknown' + if (standardReference.url?.length) { + repository = `${standardReference.repository}` + license = `${standardReference.licenseName || 'unknown'}` } text += @@ -144,3 +141,48 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { ` } } + +/** + * Reference log needs to support references directly from CW, as well as those from Flare. These references have different shapes, so we standarize them here. + */ +type GetInnerType = T extends (infer U)[] ? U : never +type Reference = + | CodeWhispererClient.Reference + | CodeWhispererUserClient.Reference + | GetInnerType + +type StandardizedReference = { + licenseName?: string + position?: { + start?: number + end?: number + } + repository?: string + url?: string +} + +/** + * Convert a general reference to the standardized format expected by the reference log. + * @param ref + * @returns + */ +function toStandardReference(ref: Reference): StandardizedReference { + const isCWReference = (ref: any) => ref.recommendationContentSpan !== undefined + + if (isCWReference(ref)) { + const castRef = ref as CodeWhispererClient.Reference + return { + licenseName: castRef.licenseName!, + position: { start: castRef.recommendationContentSpan?.start, end: castRef.recommendationContentSpan?.end }, + repository: castRef.repository, + url: castRef.url, + } + } + const castRef = ref as GetInnerType + return { + licenseName: castRef.licenseName, + position: { start: castRef.position?.startCharacter, end: castRef.position?.endCharacter }, + repository: castRef.referenceName, + url: castRef.referenceUrl, + } +} From b5540e28bc6797e8916c9a9420111ba75ca7a48e Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 15 May 2025 12:17:04 -0400 Subject: [PATCH 25/48] feat(amazonq): show a message when no suggestions are found on manual invoke (#7318) ## Problem When manually invoking inline suggestions, it is possible for the backend to still not reply with results. Since the user took an action to invoke this, we should let them know when it fails. ## Solution - add a message with same timeout as original implementation. ## Testing and Verification - added unit test. - E2E flow demo (notice it doesn't trigger on automatic failures, but does on manual invoke failures): https://github.com/user-attachments/assets/7cf34178-e91e-4d38-8875-1259c5552a53 --- - 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. --------- Co-authored-by: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> --- packages/amazonq/src/app/inline/completion.ts | 12 +- .../amazonq/apps/inline/completion.test.ts | 121 +++++++++++------- .../src/codewhisperer/models/constants.ts | 2 +- packages/core/src/shared/utilities/index.ts | 1 + 4 files changed, 85 insertions(+), 51 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 565736eb39d..330bb3d24d0 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -34,13 +34,14 @@ import { CodeSuggestionsState, vsCodeState, inlineCompletionsDebounceDelay, + noInlineSuggestionsMsg, } from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' import { TelemetryHelper } from './telemetryHelper' import { getLogger } from 'aws-core-vscode/shared' -import { debounce } from 'aws-core-vscode/utils' +import { debounce, messageUtils } from 'aws-core-vscode/utils' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -241,7 +242,16 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem const items = this.sessionManager.getActiveRecommendation() const session = this.sessionManager.getActiveSession() const editor = window.activeTextEditor + + // Show message to user when manual invoke fails to produce results. + if (items.length === 0 && context.triggerKind === InlineCompletionTriggerKind.Invoke) { + void messageUtils.showTimedMessage(noInlineSuggestionsMsg, 2000) + } + if (!session || !items.length || !editor) { + getLogger().debug( + `Failed to produce inline suggestion results. Received ${items.length} items from service` + ) return [] } diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 0fa83130ad5..6ce9adc5a69 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -3,15 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ import sinon from 'sinon' -import { CancellationToken, commands, InlineCompletionItem, languages, Position, window, Range } from 'vscode' +import { + CancellationToken, + commands, + InlineCompletionItem, + languages, + Position, + window, + Range, + InlineCompletionTriggerKind, +} from 'vscode' import assert from 'assert' import { LanguageClient } from 'vscode-languageclient' import { StringValue } from 'vscode-languageserver-types' import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' -import { createMockDocument, createMockTextEditor, installFakeClock } from 'aws-core-vscode/test' +import { createMockDocument, createMockTextEditor, getTestWindow, installFakeClock } from 'aws-core-vscode/test' import { + noInlineSuggestionsMsg, ReferenceHoverProvider, ReferenceInlineProvider, ReferenceLogViewProvider, @@ -400,57 +410,70 @@ describe('InlineCompletionManager', () => { assert.strictEqual(result[0].insertText, expectedText) }), - describe('debounce behavior', function () { - let clock: ReturnType - - beforeEach(function () { - clock = installFakeClock() - }) - - after(function () { - clock.uninstall() - }) - - it('should only trigger once on rapid events', async () => { - provider = new AmazonQInlineCompletionItemProvider( - languageClient, - recommendationService, - mockSessionManager, - inlineTutorialAnnotation, - false - ) - const p1 = provider.provideInlineCompletionItems( - mockDocument, - mockPosition, - mockContext, - mockToken - ) - const p2 = provider.provideInlineCompletionItems( - mockDocument, - mockPosition, - mockContext, - mockToken - ) - const p3 = provider.provideInlineCompletionItems( - mockDocument, - new Position(2, 2), - mockContext, - mockToken - ) + it('shows message to user when manual invoke fails to produce results', async function () { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + true + ) + getActiveRecommendationStub.returns([]) + const messageShown = new Promise((resolve) => + getTestWindow().onDidShowMessage((e) => { + assert.strictEqual(e.message, noInlineSuggestionsMsg) + resolve(true) + }) + ) + await provider.provideInlineCompletionItems( + mockDocument, + mockPosition, + { triggerKind: InlineCompletionTriggerKind.Invoke, selectedCompletionInfo: undefined }, + mockToken + ) + await messageShown + }) + describe('debounce behavior', function () { + let clock: ReturnType + + beforeEach(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + it('should only trigger once on rapid events', async () => { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + false + ) + const p1 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + const p2 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + const p3 = provider.provideInlineCompletionItems( + mockDocument, + new Position(2, 2), + mockContext, + mockToken + ) - await clock.tickAsync(1000) + await clock.tickAsync(1000) - // All promises should be the same object when debounced properly. - assert.strictEqual(p1, p2) - assert.strictEqual(p1, p3) - await p1 - await p2 - const r3 = await p3 + // All promises should be the same object when debounced properly. + assert.strictEqual(p1, p2) + assert.strictEqual(p1, p3) + await p1 + await p2 + const r3 = await p3 - // calls the function with the latest provided args. - assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(2, 2)) - }) + // calls the function with the latest provided args. + assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(2, 2)) }) + }) }) }) }) diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 2a2c84b4866..8f00088f26d 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -155,7 +155,7 @@ export const runningSecurityScan = 'Reviewing project for code issues...' export const runningFileScan = 'Reviewing current file for code issues...' -export const noSuggestions = 'No suggestions from Amazon Q' +export const noInlineSuggestionsMsg = 'No suggestions from Amazon Q' export const licenseFilter = 'Amazon Q suggestions were filtered due to reference settings' diff --git a/packages/core/src/shared/utilities/index.ts b/packages/core/src/shared/utilities/index.ts index 520390b5204..ecf753090ca 100644 --- a/packages/core/src/shared/utilities/index.ts +++ b/packages/core/src/shared/utilities/index.ts @@ -6,3 +6,4 @@ export { isExtensionInstalled, isExtensionActive } from './vsCodeUtils' export { VSCODE_EXTENSION_ID } from '../extensions' export * from './functionUtils' +export * as messageUtils from './messages' From b63185c9fee0d643abf50a7870c787748cd78ab1 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 15 May 2025 12:27:02 -0400 Subject: [PATCH 26/48] fix: avoid relying on session state --- packages/amazonq/src/app/inline/completion.ts | 75 +++++-------------- .../amazonq/src/app/inline/sessionManager.ts | 21 +----- .../amazonq/apps/inline/completion.test.ts | 32 ++------ 3 files changed, 27 insertions(+), 101 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 330bb3d24d0..a75a85f5b1c 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -158,39 +158,6 @@ export class InlineCompletionManager implements Disposable { this.languageClient.sendNotification(this.logSessionResultMessageName, params) } commands.registerCommand('aws.amazonq.rejectCodeSuggestion', onInlineRejection) - - /* - We have to overwrite the prev. and next. commands because the inlineCompletionProvider only contained the current item - To show prev. and next. recommendation we need to re-register a new provider with the previous or next item - */ - - const swapProviderAndShow = async () => { - await commands.executeCommand('editor.action.inlineSuggest.hide') - this.disposable.dispose() - this.disposable = languages.registerInlineCompletionItemProvider( - CodeWhispererConstants.platformLanguageIds, - new AmazonQInlineCompletionItemProvider( - this.languageClient, - this.recommendationService, - this.sessionManager, - this.inlineTutorialAnnotation, - false - ) - ) - await commands.executeCommand('editor.action.inlineSuggest.trigger') - } - - const prevCommandHandler = async () => { - this.sessionManager.decrementActiveIndex() - await swapProviderAndShow() - } - commands.registerCommand('editor.action.inlineSuggest.showPrevious', prevCommandHandler) - - const nextCommandHandler = async () => { - this.sessionManager.incrementActiveIndex() - await swapProviderAndShow() - } - commands.registerCommand('editor.action.inlineSuggest.showNext', nextCommandHandler) } } @@ -199,8 +166,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem private readonly languageClient: LanguageClient, private readonly recommendationService: RecommendationService, private readonly sessionManager: SessionManager, - private readonly inlineTutorialAnnotation: InlineTutorialAnnotation, - private readonly isNewSession: boolean = true + private readonly inlineTutorialAnnotation: InlineTutorialAnnotation ) {} provideInlineCompletionItems = debounce( @@ -215,29 +181,28 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem context: InlineCompletionContext, token: CancellationToken ): Promise { + getLogger().debug(`provideInlineCompletionItems: ${context.triggerKind}`) try { vsCodeState.isRecommendationsActive = true - if (this.isNewSession) { - const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic - if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { - // return early when suggestions are disabled with auto trigger - return [] - } - - // tell the tutorial that completions has been triggered - await this.inlineTutorialAnnotation.triggered(context.triggerKind) - TelemetryHelper.instance.setInvokeSuggestionStartTime() - TelemetryHelper.instance.setTriggerType(context.triggerKind) - - // make service requests if it's a new session - await this.recommendationService.getAllRecommendations( - this.languageClient, - document, - position, - context, - token - ) + getLogger().debug(`provideInlineCompletionItems triggered`) + const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic + if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { + // return early when suggestions are disabled with auto trigger + return [] } + + // tell the tutorial that completions has been triggered + await this.inlineTutorialAnnotation.triggered(context.triggerKind) + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setTriggerType(context.triggerKind) + + await this.recommendationService.getAllRecommendations( + this.languageClient, + document, + position, + context, + token + ) // get active item from session for displaying const items = this.sessionManager.getActiveRecommendation() const session = this.sessionManager.getActiveSession() diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 8286bc89459..67ad18c36ed 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -74,27 +74,10 @@ export class SessionManager { */ public getActiveRecommendation(): InlineCompletionItemWithReferences[] { - let suggestionCount = this.activeSession?.suggestions.length - if (!suggestionCount) { - return [] - } - if (suggestionCount === 1 && this.activeSession?.isRequestInProgress) { - suggestionCount += 1 - } - - const activeSuggestion = this.activeSession?.suggestions[this.activeIndex] - if (!activeSuggestion) { + if (!this.activeSession) { return [] } - const items = [activeSuggestion] - // to make the total number of suggestions match the actual number - for (let i = 1; i < suggestionCount; i++) { - items.push({ - ...activeSuggestion, - insertText: `${i}`, - }) - } - return items + return this.activeSession.suggestions } public get acceptedSuggestionCount(): number { diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 6ce9adc5a69..cb551e437ef 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -320,30 +320,12 @@ describe('InlineCompletionManager', () => { assert(getAllRecommendationsStub.calledOnce) assert.deepStrictEqual(items, mockSuggestions) }), - it('should not call recommendation service for existing sessions', async () => { - provider = new AmazonQInlineCompletionItemProvider( - languageClient, - recommendationService, - mockSessionManager, - inlineTutorialAnnotation, - false - ) - const items = await provider.provideInlineCompletionItems( - mockDocument, - mockPosition, - mockContext, - mockToken - ) - assert(getAllRecommendationsStub.notCalled) - assert.deepStrictEqual(items, mockSuggestions) - }), it('should handle reference if there is any', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation, - false + inlineTutorialAnnotation ) await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) assert(setInlineReferenceStub.calledOnce) @@ -360,8 +342,7 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation, - true + inlineTutorialAnnotation ) getActiveRecommendationStub.returns([ { @@ -391,8 +372,7 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation, - true + inlineTutorialAnnotation ) const expectedText = 'this is my text' getActiveRecommendationStub.returns([ @@ -415,8 +395,7 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation, - true + inlineTutorialAnnotation ) getActiveRecommendationStub.returns([]) const messageShown = new Promise((resolve) => @@ -449,8 +428,7 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation, - false + inlineTutorialAnnotation ) const p1 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) const p2 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) From 7f5f9b7c4a69f85d52aa52ee584008366fd7aded Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 15 May 2025 13:26:33 -0400 Subject: [PATCH 27/48] cleanup: remove unused session logic --- .../amazonq/src/app/inline/sessionManager.ts | 20 ------------------- .../apps/inline/recommendationService.test.ts | 7 ------- 2 files changed, 27 deletions(-) diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 67ad18c36ed..4c7bb05f464 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -17,7 +17,6 @@ interface CodeWhispererSession { export class SessionManager { private activeSession?: CodeWhispererSession - private activeIndex: number = 0 private _acceptedSuggestionCount: number = 0 constructor() {} @@ -35,7 +34,6 @@ export class SessionManager { requestStartTime, firstCompletionDisplayLatency, } - this.activeIndex = 0 } public closeSession() { @@ -56,23 +54,6 @@ export class SessionManager { this.activeSession.suggestions = [...this.activeSession.suggestions, ...suggestions] } - public incrementActiveIndex() { - const suggestionCount = this.activeSession?.suggestions?.length - if (!suggestionCount) { - return - } - this.activeIndex === suggestionCount - 1 ? suggestionCount - 1 : this.activeIndex++ - } - - public decrementActiveIndex() { - this.activeIndex === 0 ? 0 : this.activeIndex-- - } - - /* - We have to maintain the active suggestion index ourselves because VS Code doesn't expose which suggestion it's currently showing - In order to keep track of the right suggestion state, and for features such as reference tracker, this hack is still needed - */ - public getActiveRecommendation(): InlineCompletionItemWithReferences[] { if (!this.activeSession) { return [] @@ -90,6 +71,5 @@ export class SessionManager { public clear() { this.activeSession = undefined - this.activeIndex = 0 } } diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index 3b894b47b71..57eca77b147 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -111,13 +111,6 @@ describe('RecommendationService', () => { ...expectedRequestArgs, partialResultToken: mockPartialResultToken, }) - - // Verify session management - const items = sessionManager.getActiveRecommendation() - assert.deepStrictEqual(items, [mockInlineCompletionItemOne, { insertText: '1' } as InlineCompletionItem]) - sessionManager.incrementActiveIndex() - const items2 = sessionManager.getActiveRecommendation() - assert.deepStrictEqual(items2, [mockInlineCompletionItemTwo, { insertText: '1' } as InlineCompletionItem]) }) }) }) From 45540670f2fa5e784e9f8d7b0d2cb78c56d0e54a Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 15 May 2025 17:56:31 -0400 Subject: [PATCH 28/48] test: delete outdated tests --- .../amazonq/apps/inline/completion.test.ts | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index cb551e437ef..f9c3510ac8d 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -230,46 +230,6 @@ describe('InlineCompletionManager', () => { assert(registerProviderStub.calledTwice) // Once in constructor, once after rejection }) }) - - describe('previous command', () => { - it('should register and handle previous command correctly', async () => { - const prevCommandCall = registerCommandStub - .getCalls() - .find((call) => call.args[0] === 'editor.action.inlineSuggest.showPrevious') - - assert(prevCommandCall, 'Previous command should be registered') - - if (prevCommandCall) { - const handler = prevCommandCall.args[1] - await handler() - - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.hide')) - assert(disposableStub.calledOnce) - assert(registerProviderStub.calledTwice) - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.trigger')) - } - }) - }) - - describe('next command', () => { - it('should register and handle next command correctly', async () => { - const nextCommandCall = registerCommandStub - .getCalls() - .find((call) => call.args[0] === 'editor.action.inlineSuggest.showNext') - - assert(nextCommandCall, 'Next command should be registered') - - if (nextCommandCall) { - const handler = nextCommandCall.args[1] - await handler() - - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.hide')) - assert(disposableStub.calledOnce) - assert(registerProviderStub.calledTwice) - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.trigger')) - } - }) - }) }) describe('AmazonQInlineCompletionItemProvider', () => { From 8c938364f0d5fb6b5abd86b0bb81d774b4062a74 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 15 May 2025 17:58:10 -0400 Subject: [PATCH 29/48] refactor: remove noisy logs --- packages/amazonq/src/app/inline/completion.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index a75a85f5b1c..9fdd475587d 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -181,10 +181,8 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem context: InlineCompletionContext, token: CancellationToken ): Promise { - getLogger().debug(`provideInlineCompletionItems: ${context.triggerKind}`) try { vsCodeState.isRecommendationsActive = true - getLogger().debug(`provideInlineCompletionItems triggered`) const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { // return early when suggestions are disabled with auto trigger From 0165fc84159f81279a5f632da5d6d5b5e531981c Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 15 May 2025 18:47:08 -0400 Subject: [PATCH 30/48] fix: don't apply codelense to each item --- packages/amazonq/src/app/inline/completion.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 9fdd475587d..047a78055bc 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -28,7 +28,6 @@ import { RecommendationService } from './recommendationService' import { CodeWhispererConstants, ReferenceHoverProvider, - ReferenceInlineProvider, ReferenceLogViewProvider, ImportAdderProvider, CodeSuggestionsState, @@ -234,11 +233,6 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem } item.range = new Range(cursorPosition, cursorPosition) item.insertText = typeof item.insertText === 'string' ? item.insertText : item.insertText.value - ReferenceInlineProvider.instance.setInlineReference( - cursorPosition.line, - item.insertText, - item.references - ) ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item) } return items as InlineCompletionItem[] From 2fdbb8436a852d2dbf4c40f7893c1e43215c7f69 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 15 May 2025 19:25:14 -0400 Subject: [PATCH 31/48] test: remove inline refrence codelense from tests --- .../unit/amazonq/apps/inline/completion.test.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index f9c3510ac8d..a8bc854c97f 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -20,12 +20,7 @@ import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '.. import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' import { createMockDocument, createMockTextEditor, getTestWindow, installFakeClock } from 'aws-core-vscode/test' -import { - noInlineSuggestionsMsg, - ReferenceHoverProvider, - ReferenceInlineProvider, - ReferenceLogViewProvider, -} from 'aws-core-vscode/codewhisperer' +import { noInlineSuggestionsMsg, ReferenceHoverProvider, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' import { InlineTutorialAnnotation } from '../../../../../src/app/inline/tutorials/inlineTutorialAnnotation' @@ -238,7 +233,6 @@ describe('InlineCompletionManager', () => { let provider: AmazonQInlineCompletionItemProvider let getAllRecommendationsStub: sinon.SinonStub let recommendationService: RecommendationService - let setInlineReferenceStub: sinon.SinonStub let inlineTutorialAnnotation: InlineTutorialAnnotation beforeEach(() => { @@ -246,7 +240,6 @@ describe('InlineCompletionManager', () => { const activeStateController = new InlineGeneratingMessage(lineTracker) inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager) recommendationService = new RecommendationService(mockSessionManager, activeStateController) - setInlineReferenceStub = sandbox.stub(ReferenceInlineProvider.instance, 'setInlineReference') mockSessionManager = { getActiveSession: getActiveSessionStub, @@ -288,14 +281,6 @@ describe('InlineCompletionManager', () => { inlineTutorialAnnotation ) await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) - assert(setInlineReferenceStub.calledOnce) - assert( - setInlineReferenceStub.calledWithExactly( - mockPosition.line, - mockSuggestions[0].insertText, - fakeReferences - ) - ) }), it('should add a range to the completion item when missing', async function () { provider = new AmazonQInlineCompletionItemProvider( From a814ee0cf6d302f85e06c6029a7575e214dae666 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <43144436+aws-toolkit-automation@users.noreply.github.com> Date: Thu, 15 May 2025 19:33:53 -0400 Subject: [PATCH 32/48] Merge master into feature/flare-inline (#7310) ## Automatic merge failed - Resolve conflicts and push to this PR branch. - **Do not squash-merge** this PR. Use the "Create a merge commit" option to do a regular merge. ## Command line hint To perform the merge from the command line, you could do something like the following (where "origin" is the name of the remote in your local git repo): ``` git stash git fetch --all git checkout origin/feature/flare-inline git merge origin/master git commit git push origin HEAD:refs/heads/autoMerge/feature/flare-inline ``` --------- Signed-off-by: nkomonen-amazon Co-authored-by: aws-toolkit-automation <> Co-authored-by: Tom Zu <138054255+tomcat323@users.noreply.github.com> Co-authored-by: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Co-authored-by: Tai Lai Co-authored-by: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Co-authored-by: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Co-authored-by: Adam Khamis <110852798+akhamis-amzn@users.noreply.github.com> Co-authored-by: Na Yue Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> Co-authored-by: Jiatong Li Co-authored-by: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Co-authored-by: Brad Skaggs <126105424+brdskggs@users.noreply.github.com> Co-authored-by: Zoe Lin <60411978+zixlin7@users.noreply.github.com> Co-authored-by: chungjac Co-authored-by: Lei Gao <97199248+leigaol@users.noreply.github.com> Co-authored-by: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Co-authored-by: invictus <149003065+ashishrp-aws@users.noreply.github.com> Co-authored-by: hkobew --- CONTRIBUTING.md | 3 + docs/images/exportAmazonQLogs.png | Bin 0 -> 117008 bytes docs/images/openExportLogs.png | Bin 0 -> 110167 bytes package-lock.json | 1665 ++++------------- package.json | 2 +- packages/amazonq/.changes/1.67.0.json | 14 + packages/amazonq/.changes/1.68.0.json | 18 + packages/amazonq/CHANGELOG.md | 11 + packages/amazonq/package.json | 2 +- packages/amazonq/src/lsp/auth.ts | 7 +- packages/amazonq/src/lsp/chat/activation.ts | 62 +- packages/amazonq/src/lsp/chat/messages.ts | 4 + packages/amazonq/src/lsp/client.ts | 295 +-- packages/amazonq/src/lsp/config.ts | 47 + .../util/runtimeLanguageContext.test.ts | 2 - packages/core/package.json | 4 +- packages/core/src/auth/index.ts | 1 + .../util/runtimeLanguageContext.ts | 25 +- packages/toolkit/.changes/3.61.0.json | 5 + packages/toolkit/.changes/3.62.0.json | 5 + packages/toolkit/CHANGELOG.md | 8 + packages/toolkit/package.json | 2 +- 22 files changed, 685 insertions(+), 1497 deletions(-) create mode 100644 docs/images/exportAmazonQLogs.png create mode 100644 docs/images/openExportLogs.png create mode 100644 packages/amazonq/.changes/1.67.0.json create mode 100644 packages/amazonq/.changes/1.68.0.json create mode 100644 packages/toolkit/.changes/3.61.0.json create mode 100644 packages/toolkit/.changes/3.62.0.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c9bcba59f5..04dbdd11a26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -387,6 +387,9 @@ If you need to report an issue attach these to give the most detailed informatio 4. Open the Command Palette again and select `Reload Window`. 5. Now you should see additional `[debug]` prefixed logs in the output. - ![](./docs/images/logsDebugLog.png) +6. To export logs, click the kebab (`...`), select `Export Logs`, and then select the appropriate channel (`Amazon Q Logs` for Amazon Q) + - ![](./docs/images/openExportLogs.png) + - ![](./docs/images/exportAmazonQLogs.png) ### Telemetry diff --git a/docs/images/exportAmazonQLogs.png b/docs/images/exportAmazonQLogs.png new file mode 100644 index 0000000000000000000000000000000000000000..ae74b3c7df135ed1c3f47c4913dec8922e3e5d14 GIT binary patch literal 117008 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 literal 0 HcmV?d00001 diff --git a/docs/images/openExportLogs.png b/docs/images/openExportLogs.png new file mode 100644 index 0000000000000000000000000000000000000000..4a9902f8c284b4079ea016912dfc2af07ae58734 GIT binary patch literal 110167 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
  • S@ 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¥$=5$ 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index cff472d4104..8b81bcb688a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.319", + "@aws-toolkits/telemetry": "^1.0.322", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -68,22 +68,6 @@ "resolved": "src.gen/@amzn/codewhisperer-streaming", "link": true }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "license": "Apache-2.0", @@ -8113,6 +8097,7 @@ "node_modules/@aws-sdk/credential-provider-ini": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/credential-provider-env": "3.758.0", @@ -8135,6 +8120,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/client-sso": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -8182,6 +8168,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/core": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", @@ -8202,6 +8189,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-env": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -8216,6 +8204,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-http": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -8235,6 +8224,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-process": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -8250,6 +8240,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/client-sso": "3.758.0", "@aws-sdk/core": "3.758.0", @@ -8267,6 +8258,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-host-header": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", @@ -8280,6 +8272,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-logger": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -8292,6 +8285,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", @@ -8305,6 +8299,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -8321,6 +8316,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/region-config-resolver": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", @@ -8336,6 +8332,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/token-providers": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/nested-clients": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -8351,6 +8348,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/types": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8362,6 +8360,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { "version": "3.743.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -8375,6 +8374,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -8385,6 +8385,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -8407,6 +8408,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/abort-controller": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8418,6 +8420,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/config-resolver": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -8432,6 +8435,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/core": { "version": "3.1.5", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", @@ -8449,6 +8453,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/credential-provider-imds": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", @@ -8463,6 +8468,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/fetch-http-handler": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", @@ -8477,6 +8483,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/hash-node": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-buffer-from": "^4.0.0", @@ -8490,6 +8497,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/invalid-dependency": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8501,6 +8509,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/is-array-buffer": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -8511,6 +8520,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-content-length": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", @@ -8523,6 +8533,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-endpoint": { "version": "4.0.6", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", @@ -8540,6 +8551,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-retry": { "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -8558,6 +8570,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-serde": { "version": "4.0.2", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8569,6 +8582,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-stack": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8580,6 +8594,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-config-provider": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -8593,6 +8608,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-http-handler": { "version": "4.0.3", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -8607,6 +8623,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/property-provider": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8618,6 +8635,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/protocol-http": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8629,6 +8647,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-builder": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", @@ -8641,6 +8660,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-parser": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8652,6 +8672,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/service-error-classification": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0" }, @@ -8662,6 +8683,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/shared-ini-file-loader": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8673,6 +8695,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/signature-v4": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "@smithy/protocol-http": "^5.0.1", @@ -8690,6 +8713,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/smithy-client": { "version": "4.1.6", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", @@ -8706,6 +8730,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/types": { "version": "4.1.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -8716,6 +8741,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/url-parser": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", @@ -8728,6 +8754,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-base64": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", @@ -8740,6 +8767,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-browser": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -8750,6 +8778,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-node": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -8760,6 +8789,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-buffer-from": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" @@ -8771,6 +8801,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-config-provider": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -8781,6 +8812,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-defaults-mode-browser": { "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", @@ -8795,6 +8827,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-defaults-mode-node": { "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/config-resolver": "^4.0.1", "@smithy/credential-provider-imds": "^4.0.1", @@ -8811,6 +8844,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-endpoints": { "version": "3.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -8823,6 +8857,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-hex-encoding": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -8833,6 +8868,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-middleware": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -8844,6 +8880,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-retry": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/service-error-classification": "^4.0.1", "@smithy/types": "^4.1.0", @@ -8856,6 +8893,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-stream": { "version": "4.1.2", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", @@ -8873,6 +8911,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -8883,6 +8922,7 @@ "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-utf8": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" @@ -9054,6 +9094,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/nested-clients": "3.758.0", @@ -9069,6 +9110,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/core": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", @@ -9089,6 +9131,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/types": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9100,6 +9143,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/abort-controller": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9111,6 +9155,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/core": { "version": "3.1.5", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", @@ -9128,6 +9173,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/fetch-http-handler": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", @@ -9142,6 +9188,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/is-array-buffer": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -9152,6 +9199,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-endpoint": { "version": "4.0.6", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", @@ -9169,6 +9217,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-serde": { "version": "4.0.2", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9180,6 +9229,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-stack": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9191,6 +9241,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-config-provider": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -9204,6 +9255,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-http-handler": { "version": "4.0.3", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -9218,6 +9270,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/property-provider": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9229,6 +9282,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/protocol-http": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9240,6 +9294,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/querystring-builder": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", @@ -9252,6 +9307,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/querystring-parser": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9263,6 +9319,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/shared-ini-file-loader": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9274,6 +9331,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/signature-v4": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "@smithy/protocol-http": "^5.0.1", @@ -9291,6 +9349,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/smithy-client": { "version": "4.1.6", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", @@ -9307,6 +9366,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/types": { "version": "4.1.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -9317,6 +9377,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/url-parser": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", @@ -9329,6 +9390,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-base64": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", @@ -9341,6 +9403,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-body-length-browser": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -9351,6 +9414,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-buffer-from": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" @@ -9362,6 +9426,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-hex-encoding": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -9372,6 +9437,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-middleware": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9383,6 +9449,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-stream": { "version": "4.1.2", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", @@ -9400,6 +9467,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -9410,6 +9478,7 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-utf8": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" @@ -9793,6 +9862,7 @@ "node_modules/@aws-sdk/nested-clients": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9840,6 +9910,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/core": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", @@ -9860,6 +9931,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-host-header": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", @@ -9873,6 +9945,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-logger": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -9885,6 +9958,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", @@ -9898,6 +9972,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -9914,6 +9989,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/region-config-resolver": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", @@ -9929,6 +10005,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/types": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9940,6 +10017,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { "version": "3.743.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -9953,6 +10031,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -9963,6 +10042,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -9985,6 +10065,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/abort-controller": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -9996,6 +10077,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/config-resolver": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -10010,6 +10092,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/core": { "version": "3.1.5", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", @@ -10027,6 +10110,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/credential-provider-imds": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", @@ -10041,6 +10125,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/fetch-http-handler": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", @@ -10055,6 +10140,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/hash-node": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-buffer-from": "^4.0.0", @@ -10068,6 +10154,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/invalid-dependency": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10079,6 +10166,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/is-array-buffer": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -10089,6 +10177,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-content-length": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", @@ -10101,6 +10190,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-endpoint": { "version": "4.0.6", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", @@ -10118,6 +10208,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-retry": { "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -10136,6 +10227,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-serde": { "version": "4.0.2", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10147,6 +10239,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-stack": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10158,6 +10251,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-config-provider": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -10171,6 +10265,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { "version": "4.0.3", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -10185,6 +10280,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/property-provider": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10196,6 +10292,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/protocol-http": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10207,6 +10304,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/querystring-builder": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", @@ -10219,6 +10317,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/querystring-parser": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10230,6 +10329,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/service-error-classification": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0" }, @@ -10240,6 +10340,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/shared-ini-file-loader": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10251,6 +10352,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/signature-v4": { "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "@smithy/protocol-http": "^5.0.1", @@ -10268,6 +10370,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/smithy-client": { "version": "4.1.6", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", @@ -10284,6 +10387,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/types": { "version": "4.1.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -10294,6 +10398,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/url-parser": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", @@ -10306,6 +10411,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-base64": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", @@ -10318,6 +10424,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-body-length-browser": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -10328,6 +10435,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-body-length-node": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -10338,6 +10446,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-buffer-from": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" @@ -10349,6 +10458,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-config-provider": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -10359,6 +10469,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-defaults-mode-browser": { "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", @@ -10373,6 +10484,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-defaults-mode-node": { "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/config-resolver": "^4.0.1", "@smithy/credential-provider-imds": "^4.0.1", @@ -10389,6 +10501,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-endpoints": { "version": "3.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -10401,6 +10514,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-hex-encoding": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -10411,6 +10525,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-middleware": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -10422,6 +10537,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-retry": { "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/service-error-classification": "^4.0.1", "@smithy/types": "^4.1.0", @@ -10434,6 +10550,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-stream": { "version": "4.1.2", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", @@ -10451,6 +10568,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -10461,6 +10579,7 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": { "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" @@ -10760,9 +10879,9 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.321", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.321.tgz", - "integrity": "sha512-pL1TZOyREfEuZjvjhAPyb/6fOaPLlXMft4i1mbHJVs2rnJBKFAsJOl3osmCLKXuqiMT7jhmzOE8dRCkEuLleIw==", + "version": "1.0.322", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.322.tgz", + "integrity": "sha512-KtLabV3ycRH31EAZ0xoWrdpIBG3ym8CQAqgkHd9DSefndbepPRa07atfXw73Ok9J5aA81VHCFpx1dwrLg39EcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10794,26 +10913,23 @@ } }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.70", + "version": "0.2.81", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.81.tgz", + "integrity": "sha512-wnwa8ctVCAckIpfWSblHyLVzl6UKX5G7ft+yetH1pI0mZvseSNzHUhclxNl4WGaDgGnEbBjLD0XRNEy2yRrSYg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@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.21", + "@aws/language-server-runtimes-types": "^0.1.28", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/resources": "^1.30.1", - "@opentelemetry/sdk-metrics": "^1.30.1", - "@opentelemetry/sdk-node": "^0.57.2", - "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.30.0", - "@smithy/node-http-handler": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", + "@opentelemetry/api-logs": "^0.200.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.200.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.200.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-logs": "^0.200.0", + "@opentelemetry/sdk-metrics": "^2.0.0", + "@smithy/node-http-handler": "^4.0.4", "ajv": "^8.17.1", - "aws-sdk": "^2.1692.0", - "axios": "^1.8.4", "hpagent": "^1.2.0", "jose": "^5.9.6", "mac-ca": "^3.1.1", @@ -10827,7 +10943,9 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.26", + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.28.tgz", + "integrity": "sha512-eDNcEXGAyD4rzl+eVJ6Ngfbm4iaR8MkoMk1wVcnV+VGqu63TyvV1aVWnZdl9tR4pmC0rIH3tj8FSCjhSU6eJlA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10835,870 +10953,70 @@ "vscode-languageserver-types": "^3.17.5" } }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.768.0", + "node_modules/@aws/language-server-runtimes/node_modules/@smithy/abort-controller": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", + "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/client-sso": { - "version": "3.758.0", + "node_modules/@aws/language-server-runtimes/node_modules/@smithy/node-http-handler": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz", + "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-utf8": "^4.0.0", + "@smithy/abort-controller": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", + "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "node_modules/@aws/language-server-runtimes/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.758.0", - "@aws-sdk/credential-provider-http": "3.758.0", - "@aws-sdk/credential-provider-ini": "3.758.0", - "@aws-sdk/credential-provider-process": "3.758.0", - "@aws-sdk/credential-provider-sso": "3.758.0", - "@aws-sdk/credential-provider-web-identity": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.758.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/token-providers": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/token-providers": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/abort-controller": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/config-resolver": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/core": { - "version": "3.1.5", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/credential-provider-imds": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/hash-node": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/middleware-content-length": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/service-error-classification": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/querystring-builder": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-uri-escape": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/querystring-parser": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/service-error-classification": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/signature-v4": { - "version": "5.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/smithy-client": { - "version": "4.1.6", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/types": { - "version": "4.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.7", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.0.1", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-middleware": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-retry": { - "version": "4.0.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-stream": { - "version": "4.1.2", + "node_modules/@aws/language-server-runtimes/node_modules/@smithy/querystring-builder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz", + "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.2.0", + "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", + "node_modules/@aws/language-server-runtimes/node_modules/@smithy/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz", + "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11708,12 +11026,13 @@ "node": ">=18.0.0" } }, - "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-utf8": { + "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" }, "engines": { @@ -12103,35 +11422,6 @@ "node": ">=10" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.13.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "dev": true, @@ -12307,15 +11597,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "dev": true, @@ -12389,6 +11670,8 @@ }, "node_modules/@opentelemetry/api": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -12396,500 +11679,385 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.57.2", + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz", + "integrity": "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" }, "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "1.30.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "node": ">=8.0.0" } }, "node_modules/@opentelemetry/core": { - "version": "1.30.1", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "1.28.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.57.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-grpc-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/sdk-logs": "0.57.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.57.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/sdk-logs": "0.57.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.57.2", + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.200.0.tgz", + "integrity": "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-logs": "0.57.2", - "@opentelemetry/sdk-trace-base": "1.30.1" + "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/sdk-logs": "0.200.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.57.2", + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.57.2", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-grpc-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-metrics": "1.30.1" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.57.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-metrics": "1.30.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.57.2", + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.200.0.tgz", + "integrity": "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.57.2", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-metrics": "1.30.1" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-metrics": "2.0.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.57.2", + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-metrics": "1.30.1" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.57.2", + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-grpc-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.57.2", + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", + "integrity": "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.57.2", + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.200.0.tgz", + "integrity": "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-transformer": "0.200.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-zipkin": { - "version": "1.30.1", + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.57.2", + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.200.0.tgz", + "integrity": "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" + "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-logs": "0.200.0", + "@opentelemetry/sdk-metrics": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0", + "protobufjs": "^7.3.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.57.2", + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-transformer": "0.57.2" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.57.2", + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/otlp-exporter-base": "0.57.2", - "@opentelemetry/otlp-transformer": "0.57.2" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.57.2", + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", + "integrity": "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-logs": "0.57.2", - "@opentelemetry/sdk-metrics": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1", - "protobufjs": "^7.3.0" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/propagator-b3": { - "version": "1.30.1", + "node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/propagator-jaeger": { - "version": "1.30.1", + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.200.0.tgz", + "integrity": "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1" + "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/resources": { - "version": "1.30.1", + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.57.2", + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "1.30.1", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node": { - "version": "0.57.2", + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.0.tgz", + "integrity": "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/exporter-logs-otlp-grpc": "0.57.2", - "@opentelemetry/exporter-logs-otlp-http": "0.57.2", - "@opentelemetry/exporter-logs-otlp-proto": "0.57.2", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.57.2", - "@opentelemetry/exporter-metrics-otlp-http": "0.57.2", - "@opentelemetry/exporter-metrics-otlp-proto": "0.57.2", - "@opentelemetry/exporter-prometheus": "0.57.2", - "@opentelemetry/exporter-trace-otlp-grpc": "0.57.2", - "@opentelemetry/exporter-trace-otlp-http": "0.57.2", - "@opentelemetry/exporter-trace-otlp-proto": "0.57.2", - "@opentelemetry/exporter-zipkin": "1.30.1", - "@opentelemetry/instrumentation": "0.57.2", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/sdk-logs": "0.57.2", - "@opentelemetry/sdk-metrics": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1", - "@opentelemetry/sdk-trace-node": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.30.1", + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "1.30.1", + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "1.30.1", - "@opentelemetry/core": "1.30.1", - "@opentelemetry/propagator-b3": "1.30.1", - "@opentelemetry/propagator-jaeger": "1.30.1", - "@opentelemetry/sdk-trace-base": "1.30.1", - "semver": "^7.5.2" + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.30.0", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.33.0.tgz", + "integrity": "sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w==", "dev": true, "license": "Apache-2.0", "engines": { @@ -12929,26 +12097,36 @@ }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12958,26 +12136,36 @@ }, "node_modules/@protobufjs/float": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true, "license": "BSD-3-Clause" }, @@ -14438,11 +13626,6 @@ "@types/node": "*" } }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, "node_modules/@types/sinon": { "version": "10.0.5", "dev": true, @@ -15874,16 +15057,6 @@ "resolved": "packages/toolkit", "link": true }, - "node_modules/axios": { - "version": "1.8.4", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/azure-devops-node-api": { "version": "11.2.0", "dev": true, @@ -16595,11 +15768,6 @@ "webpack": ">=4.0.1" } }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "dev": true, - "license": "MIT" - }, "node_modules/clean-regexp": { "version": "1.0.0", "dev": true, @@ -19629,17 +18797,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-in-the-middle": { - "version": "1.13.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.14.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - }, "node_modules/import-local": { "version": "3.0.3", "dev": true, @@ -20821,11 +19978,6 @@ "version": "4.17.21", "license": "MIT" }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.get": { "version": "4.4.2", "dev": true, @@ -20867,7 +20019,9 @@ "license": "MIT" }, "node_modules/long": { - "version": "5.3.1", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "dev": true, "license": "Apache-2.0" }, @@ -21479,11 +20633,6 @@ "node": ">=10" } }, - "node_modules/module-details-from-path": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, "node_modules/morgan": { "version": "1.10.0", "dev": true, @@ -22602,7 +21751,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.4.0", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz", + "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -22644,11 +21795,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, "node_modules/psl": { "version": "1.9.0", "dev": true, @@ -23164,40 +22310,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "7.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/require-in-the-middle/node_modules/debug": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/require-in-the-middle/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, "node_modules/requires-port": { "version": "1.0.0", "dev": true, @@ -23777,11 +22889,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shimmer": { - "version": "1.2.1", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/side-channel": { "version": "1.0.6", "license": "MIT", @@ -26377,7 +25484,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.67.0-SNAPSHOT", + "version": "1.69.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -26477,8 +25584,8 @@ "@aws-sdk/types": "^3.13.1", "@aws/chat-client": "^0.1.4", "@aws/chat-client-ui-types": "^0.1.24", - "@aws/language-server-runtimes": "^0.2.70", - "@aws/language-server-runtimes-types": "^0.1.26", + "@aws/language-server-runtimes": "^0.2.81", + "@aws/language-server-runtimes-types": "^0.1.28", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", @@ -28091,7 +27198,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.61.0-SNAPSHOT", + "version": "3.63.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/package.json b/package.json index 493327237eb..525655b8c35 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.319", + "@aws-toolkits/telemetry": "^1.0.322", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", diff --git a/packages/amazonq/.changes/1.67.0.json b/packages/amazonq/.changes/1.67.0.json new file mode 100644 index 00000000000..59ff03eacdd --- /dev/null +++ b/packages/amazonq/.changes/1.67.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-05-14", + "version": "1.67.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Previous and subsequent cells are used as context for completion in a Jupyter notebook" + }, + { + "type": "Bug Fix", + "description": "Support chat in AL2 aarch64" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.68.0.json b/packages/amazonq/.changes/1.68.0.json new file mode 100644 index 00000000000..2c21170aa0b --- /dev/null +++ b/packages/amazonq/.changes/1.68.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-05-15", + "version": "1.68.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Fix Error: 'Amazon Q service is not signed in'" + }, + { + "type": "Bug Fix", + "description": "Fix Error: 'Amazon Q Profile is not selected for IDC connection type'" + }, + { + "type": "Feature", + "description": "Add inline completion support for abap language" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 197aecdfdf6..9d9546ce6f1 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.68.0 2025-05-15 + +- **Bug Fix** Fix Error: 'Amazon Q service is not signed in' +- **Bug Fix** Fix Error: 'Amazon Q Profile is not selected for IDC connection type' +- **Feature** Add inline completion support for abap language + +## 1.67.0 2025-05-14 + +- **Bug Fix** Previous and subsequent cells are used as context for completion in a Jupyter notebook +- **Bug Fix** Support chat in AL2 aarch64 + ## 1.66.0 2025-05-09 - **Bug Fix** Avoid inline completion 'Improperly formed request' errors when file is too large diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index f9d466f5767..421dff76d4e 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.67.0-SNAPSHOT", + "version": "1.69.0-SNAPSHOT", "extensionKind": [ "workspace" ], diff --git a/packages/amazonq/src/lsp/auth.ts b/packages/amazonq/src/lsp/auth.ts index 816c4b09ab0..d81f464d6a3 100644 --- a/packages/amazonq/src/lsp/auth.ts +++ b/packages/amazonq/src/lsp/auth.ts @@ -18,6 +18,7 @@ import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { Writable } from 'stream' import { onceChanged } from 'aws-core-vscode/utils' import { getLogger, oneMinute } from 'aws-core-vscode/shared' +import { isSsoConnection } from 'aws-core-vscode/auth' export const encryptionKey = crypto.randomBytes(32) @@ -76,8 +77,8 @@ export class AmazonQLspAuth { * @param force bypass memoization, and forcefully update the bearer token */ async refreshConnection(force: boolean = false) { - const activeConnection = this.authUtil.auth.activeConnection - if (activeConnection?.state === 'valid' && activeConnection?.type === 'sso') { + const activeConnection = this.authUtil.conn + if (this.authUtil.isConnectionValid() && isSsoConnection(activeConnection)) { // send the token to the language server const token = await this.authUtil.getBearerToken() await (force ? this._updateBearerToken(token) : this.updateBearerToken(token)) @@ -118,7 +119,7 @@ export class AmazonQLspAuth { data: jwt, metadata: { sso: { - startUrl: AuthUtil.instance.auth.startUrl, + startUrl: AuthUtil.instance.startUrl, }, }, encrypted: true, diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index 3a36377b9b5..f8e3ee16251 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -12,25 +12,11 @@ import { Commands, getLogger, globals, undefinedIfEmpty } from 'aws-core-vscode/ import { activate as registerLegacyChatListeners } from '../../app/chat/activation' import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq' import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' -import { - DidChangeConfigurationNotification, - updateConfigurationRequestType, -} from '@aws/language-server-runtimes/protocol' +import { pushConfigUpdate } from '../config' export async function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) { const disposables = globals.context.subscriptions - // Make sure we've sent an auth profile to the language server before even initializing the UI - await pushConfigUpdate(languageClient, { - type: 'profile', - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) - // We need to push the cached customization on startup explicitly - await pushConfigUpdate(languageClient, { - type: 'customization', - customization: getSelectedCustomization(), - }) - const provider = new AmazonQChatViewProvider(mynahUIPath) disposables.push( @@ -79,10 +65,6 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu disposables.push( AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => { - void pushConfigUpdate(languageClient, { - type: 'profile', - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) await provider.refreshWebview() }), Commands.register('aws.amazonq.updateCustomizations', () => { @@ -99,45 +81,3 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu }) ) } - -/** - * Push a config value to the language server, effectively updating it with the - * latest configuration from the client. - * - * The issue is we need to push certain configs to different places, since there are - * different handlers for specific configs. So this determines the correct place to - * push the given config. - */ -async function pushConfigUpdate(client: LanguageClient, config: QConfigs) { - switch (config.type) { - case 'profile': - await client.sendRequest(updateConfigurationRequestType.method, { - section: 'aws.q', - settings: { profileArn: config.profileArn }, - }) - break - case 'customization': - client.sendNotification(DidChangeConfigurationNotification.type.method, { - section: 'aws.q', - settings: { customization: config.customization }, - }) - break - case 'logLevel': - client.sendNotification(DidChangeConfigurationNotification.type.method, { - section: 'aws.logLevel', - }) - break - } -} -type ProfileConfig = { - type: 'profile' - profileArn: string | undefined -} -type CustomizationConfig = { - type: 'customization' - customization: string | undefined -} -type LogLevelConfig = { - type: 'logLevel' -} -type QConfigs = ProfileConfig | CustomizationConfig | LogLevelConfig diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 5eab1aa17db..977b646cb67 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -32,6 +32,8 @@ import { getSerializedChatRequestType, listConversationsRequestType, conversationClickRequestType, + listMcpServersRequestType, + mcpServerClickRequestType, ShowSaveFileDialogRequestType, ShowSaveFileDialogParams, LSPErrorCodes, @@ -298,6 +300,8 @@ export function registerMessageListeners( } case listConversationsRequestType.method: case conversationClickRequestType.method: + case listMcpServersRequestType.method: + case mcpServerClickRequestType.method: case tabBarActionRequestType.method: await resolveChatResponse(message.command, message.params, languageClient, webview) break diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index f5159328605..9aa2ffbb0d0 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -16,7 +16,6 @@ import { GetConfigurationFromServerParams, RenameFilesParams, ResponseMessage, - updateConfigurationRequestType, WorkspaceFolder, } from '@aws/language-server-runtimes/protocol' import { @@ -43,7 +42,7 @@ import { import { processUtils } from 'aws-core-vscode/shared' import { activate } from './chat/activation' import { AmazonQResourcePaths } from './lspInstaller' -import { ConfigSection, isValidConfigSection, toAmazonQLSPLogLevel } from './config' +import { ConfigSection, isValidConfigSection, pushConfigUpdate, toAmazonQLSPLogLevel } from './config' import { activate as activateInlineChat } from '../inlineChat/activation' import { telemetry } from 'aws-core-vscode/telemetry' import { SessionManager } from '../app/inline/sessionManager' @@ -170,157 +169,167 @@ export async function startLanguageServer( const disposable = client.start() toDispose.push(disposable) + await client.onReady() + const auth = await initializeAuth(client) + + await onLanguageServerReady(extensionContext, auth, client, resourcePaths, toDispose) + + return client +} + +async function initializeAuth(client: LanguageClient): Promise { const auth = new AmazonQLspAuth(client) + await auth.refreshConnection(true) + return auth +} - return client.onReady().then(async () => { - await auth.refreshConnection() - - // session manager for inline - const sessionManager = new SessionManager() - - // keeps track of the line changes - const lineTracker = new LineTracker() - - // tutorial for inline suggestions - const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) - - // tutorial for inline chat - const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) - - const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) - inlineManager.registerInlineCompletion() - toDispose.push( - inlineManager, - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - }), - Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - const editor = vscode.window.activeTextEditor - if (editor) { - if (forceProceed) { - await inlineTutorialAnnotation.refresh(editor, 'codewhisperer', true) - } else { - await inlineTutorialAnnotation.refresh(editor, 'codewhisperer') - } - } - }), - vscode.workspace.onDidCloseTextDocument(async () => { - await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') - }), - Commands.register('aws.amazonq.dismissTutorial', async () => { - const editor = vscode.window.activeTextEditor - if (editor) { - inlineTutorialAnnotation.clear() - try { - telemetry.ui_click.emit({ elementId: `dismiss_${inlineTutorialAnnotation.currentState.id}` }) - } catch (_) {} - await inlineTutorialAnnotation.dismissTutorial() - getLogger().debug(`codewhisperer: user dismiss tutorial.`) - } - }) - ) +async function onLanguageServerReady( + extensionContext: vscode.ExtensionContext, + auth: AmazonQLspAuth, + client: LanguageClient, + resourcePaths: AmazonQResourcePaths, + toDispose: vscode.Disposable[] +) { + const sessionManager = new SessionManager() - if (Experiments.instance.get('amazonqChatLSP', true)) { - await activate(client, encryptionKey, resourcePaths.ui) - } + // keeps track of the line changes + const lineTracker = new LineTracker() - activateInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) + // tutorial for inline suggestions + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) - const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond) + // tutorial for inline chat + const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) - const sendProfileToLsp = async () => { - try { - const result = await client.sendRequest(updateConfigurationRequestType.method, { - section: 'aws.q', - settings: { - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }, - }) - client.info( - `Client: Updated Amazon Q Profile ${AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn} to Amazon Q LSP`, - result - ) - } catch (err) { - client.error('Error when setting Q Developer Profile to Amazon Q LSP', err) - } - } + const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) + inlineManager.registerInlineCompletion() + activateInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) - // send profile to lsp once. - void sendProfileToLsp() - - toDispose.push( - AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { - await auth.refreshConnection() - }), - AuthUtil.instance.auth.onDidDeleteConnection(async () => { - client.sendNotification(notificationTypes.deleteBearerToken.method) - }), - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(sendProfileToLsp), - vscode.commands.registerCommand('aws.amazonq.getWorkspaceId', async () => { - const requestType = new RequestType( - 'aws/getConfigurationFromServer' - ) - const workspaceIdResp = await client.sendRequest(requestType.method, { - section: 'aws.q.workspaceContext', - }) - return workspaceIdResp - }), - vscode.workspace.onDidCreateFiles((e) => { - client.sendNotification('workspace/didCreateFiles', { - files: e.files.map((it) => { - return { uri: it.fsPath } - }), - } as CreateFilesParams) - }), - vscode.workspace.onDidDeleteFiles((e) => { - client.sendNotification('workspace/didDeleteFiles', { - files: e.files.map((it) => { - return { uri: it.fsPath } + if (Experiments.instance.get('amazonqChatLSP', true)) { + await activate(client, encryptionKey, resourcePaths.ui) + } + + const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond) + + // We manually push the cached values the first time since event handlers, which should push, may not have been setup yet. + // Execution order is weird and should be fixed in the flare implementation. + // TODO: Revisit if we need this if we setup the event handlers properly + if (AuthUtil.instance.isConnectionValid()) { + await sendProfileToLsp(client) + + await pushConfigUpdate(client, { + type: 'customization', + customization: getSelectedCustomization(), + }) + } + + toDispose.push( + inlineManager, + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + }), + Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + const editor = vscode.window.activeTextEditor + if (editor) { + if (forceProceed) { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer', true) + } else { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer') + } + } + }), + Commands.register('aws.amazonq.dismissTutorial', async () => { + const editor = vscode.window.activeTextEditor + if (editor) { + inlineTutorialAnnotation.clear() + try { + telemetry.ui_click.emit({ elementId: `dismiss_${inlineTutorialAnnotation.currentState.id}` }) + } catch (_) {} + await inlineTutorialAnnotation.dismissTutorial() + getLogger().debug(`codewhisperer: user dismiss tutorial.`) + } + }), + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }), + AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { + await auth.refreshConnection() + }), + AuthUtil.instance.auth.onDidDeleteConnection(async () => { + client.sendNotification(notificationTypes.deleteBearerToken.method) + }), + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => sendProfileToLsp(client)), + vscode.commands.registerCommand('aws.amazonq.getWorkspaceId', async () => { + const requestType = new RequestType( + 'aws/getConfigurationFromServer' + ) + const workspaceIdResp = await client.sendRequest(requestType.method, { + section: 'aws.q.workspaceContext', + }) + return workspaceIdResp + }), + vscode.workspace.onDidCreateFiles((e) => { + client.sendNotification('workspace/didCreateFiles', { + files: e.files.map((it) => { + return { uri: it.fsPath } + }), + } as CreateFilesParams) + }), + vscode.workspace.onDidDeleteFiles((e) => { + client.sendNotification('workspace/didDeleteFiles', { + files: e.files.map((it) => { + return { uri: it.fsPath } + }), + } as DeleteFilesParams) + }), + vscode.workspace.onDidRenameFiles((e) => { + client.sendNotification('workspace/didRenameFiles', { + files: e.files.map((it) => { + return { oldUri: it.oldUri.fsPath, newUri: it.newUri.fsPath } + }), + } as RenameFilesParams) + }), + vscode.workspace.onDidSaveTextDocument((e) => { + client.sendNotification('workspace/didSaveTextDocument', { + textDocument: { + uri: e.uri.fsPath, + }, + } as DidSaveTextDocumentParams) + }), + vscode.workspace.onDidChangeWorkspaceFolders((e) => { + client.sendNotification('workspace/didChangeWorkspaceFolder', { + event: { + added: e.added.map((it) => { + return { + name: it.name, + uri: it.uri.fsPath, + } as WorkspaceFolder }), - } as DeleteFilesParams) - }), - vscode.workspace.onDidRenameFiles((e) => { - client.sendNotification('workspace/didRenameFiles', { - files: e.files.map((it) => { - return { oldUri: it.oldUri.fsPath, newUri: it.newUri.fsPath } + removed: e.removed.map((it) => { + return { + name: it.name, + uri: it.uri.fsPath, + } as WorkspaceFolder }), - } as RenameFilesParams) - }), - vscode.workspace.onDidSaveTextDocument((e) => { - client.sendNotification('workspace/didSaveTextDocument', { - textDocument: { - uri: e.uri.fsPath, - }, - } as DidSaveTextDocumentParams) - }), - vscode.workspace.onDidChangeWorkspaceFolders((e) => { - client.sendNotification('workspace/didChangeWorkspaceFolder', { - event: { - added: e.added.map((it) => { - return { - name: it.name, - uri: it.uri.fsPath, - } as WorkspaceFolder - }), - removed: e.removed.map((it) => { - return { - name: it.name, - uri: it.uri.fsPath, - } as WorkspaceFolder - }), - }, - } as DidChangeWorkspaceFoldersParams) - }), - { dispose: () => clearInterval(refreshInterval) }, - // Set this inside onReady so that it only triggers on subsequent language server starts (not the first) - onServerRestartHandler(client, auth) - ) - }) + }, + } as DidChangeWorkspaceFoldersParams) + }), + { dispose: () => clearInterval(refreshInterval) }, + // Set this inside onReady so that it only triggers on subsequent language server starts (not the first) + onServerRestartHandler(client, auth) + ) + + async function sendProfileToLsp(client: LanguageClient) { + await pushConfigUpdate(client, { + type: 'profile', + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }) + } } /** diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts index 89a8ff3f714..66edc9ff6f1 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -4,6 +4,11 @@ */ import * as vscode from 'vscode' import { DevSettings, getServiceEnvVarConfig, BaseLspInstaller } from 'aws-core-vscode/shared' +import { LanguageClient } from 'vscode-languageclient' +import { + DidChangeConfigurationNotification, + updateConfigurationRequestType, +} from '@aws/language-server-runtimes/protocol' export interface ExtendedAmazonQLSPConfig extends BaseLspInstaller.LspConfig { ui?: string @@ -53,3 +58,45 @@ export function getAmazonQLspConfig(): ExtendedAmazonQLSPConfig { export function toAmazonQLSPLogLevel(logLevel: vscode.LogLevel): LspLogLevel { return lspLogLevelMapping.get(logLevel) ?? 'info' } + +/** + * Request/Notify a config value to the language server, effectively updating it with the + * latest configuration from the client. + * + * The issue is we need to push certain configs to different places, since there are + * different handlers for specific configs. So this determines the correct place to + * push the given config. + */ +export async function pushConfigUpdate(client: LanguageClient, config: QConfigs) { + switch (config.type) { + case 'profile': + await client.sendRequest(updateConfigurationRequestType.method, { + section: 'aws.q', + settings: { profileArn: config.profileArn }, + }) + break + case 'customization': + client.sendNotification(DidChangeConfigurationNotification.type.method, { + section: 'aws.q', + settings: { customization: config.customization }, + }) + break + case 'logLevel': + client.sendNotification(DidChangeConfigurationNotification.type.method, { + section: 'aws.logLevel', + }) + break + } +} +type ProfileConfig = { + type: 'profile' + profileArn: string | undefined +} +type CustomizationConfig = { + type: 'customization' + customization: string | undefined +} +type LogLevelConfig = { + type: 'logLevel' +} +type QConfigs = ProfileConfig | CustomizationConfig | LogLevelConfig diff --git a/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts b/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts index a5cc430a5a9..9d2dbf7954d 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/runtimeLanguageContext.test.ts @@ -177,7 +177,6 @@ describe('runtimeLanguageContext', function () { 'jsx', 'kotlin', 'php', - 'plaintext', 'python', 'ruby', 'rust', @@ -288,7 +287,6 @@ describe('runtimeLanguageContext', function () { ['jsx', 'jsx'], ['kotlin', 'kt'], ['php', 'php'], - ['plaintext', 'txt'], ['python', 'py'], ['ruby', 'rb'], ['rust', 'rs'], diff --git a/packages/core/package.json b/packages/core/package.json index 0ed5e368121..f35369cc5b9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -443,8 +443,8 @@ "@aws-sdk/types": "^3.13.1", "@aws/chat-client": "^0.1.4", "@aws/chat-client-ui-types": "^0.1.24", - "@aws/language-server-runtimes": "^0.2.70", - "@aws/language-server-runtimes-types": "^0.1.26", + "@aws/language-server-runtimes": "^0.2.81", + "@aws/language-server-runtimes-types": "^0.1.28", "@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/auth/index.ts b/packages/core/src/auth/index.ts index 54dd17d702b..02a0067be45 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -18,6 +18,7 @@ export { isBuilderIdConnection, getTelemetryMetadataForConn, isIamConnection, + isSsoConnection, } from './connection' export { Auth } from './auth' export { CredentialsStore } from './credentials/store' diff --git a/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts b/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts index 3a1403b453e..e1d4802b6f1 100644 --- a/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts +++ b/packages/core/src/codewhisperer/util/runtimeLanguageContext.ts @@ -67,7 +67,7 @@ export class RuntimeLanguageContext { constructor() { this.supportedLanguageMap = createConstantMap< - CodeWhispererConstants.PlatformLanguageId | CodewhispererLanguage, + Exclude, CodewhispererLanguage >({ c: 'c', @@ -85,7 +85,6 @@ export class RuntimeLanguageContext { jsx: 'jsx', kotlin: 'kotlin', packer: 'tf', - plaintext: 'plaintext', php: 'php', python: 'python', ruby: 'ruby', @@ -112,6 +111,7 @@ export class RuntimeLanguageContext { systemverilog: 'systemVerilog', verilog: 'systemVerilog', vue: 'vue', + abap: 'abap', }) this.supportedLanguageExtensionMap = createConstantMap({ c: 'c', @@ -152,6 +152,8 @@ export class RuntimeLanguageContext { ps1: 'powershell', psm1: 'powershell', r: 'r', + abap: 'abap', + acds: 'abap', }) this.languageSingleLineCommentPrefixMap = createConstantMap({ c: '// ', @@ -185,9 +187,14 @@ export class RuntimeLanguageContext { vue: '', // vue lacks a single-line comment prefix yaml: '# ', yml: '# ', + abap: '', }) } + public resolveLang(doc: vscode.TextDocument): CodewhispererLanguage { + return this.normalizeLanguage(doc.languageId) || this.byFileExt(doc) || 'plaintext' + } + /** * To add a new platform language id: * 1. add new platform language ID constant in the file codewhisperer/constant.ts @@ -317,8 +324,7 @@ export class RuntimeLanguageContext { } else { const normalizedLanguageId = this.normalizeLanguage(arg.languageId) const byLanguageId = !normalizedLanguageId || normalizedLanguageId === 'plaintext' ? false : true - const extension = path.extname(arg.uri.fsPath) - const byFileExtension = this.isFileFormatSupported(extension.substring(1)) + const byFileExtension = this.byFileExt(arg) !== undefined return byLanguageId || byFileExtension } @@ -341,6 +347,17 @@ export class RuntimeLanguageContext { public getLanguageFromFileExtension(fileExtension: string) { return this.supportedLanguageExtensionMap.get(fileExtension) } + + private byFileExt(doc: vscode.TextDocument): CodewhispererLanguage | undefined { + const extension = path.extname(doc.uri.fsPath) + const byExt = this.supportedLanguageExtensionMap.get(extension.substring(1)) + + if (byExt === 'plaintext') { + return undefined + } + + return byExt + } } export const runtimeLanguageContext = new RuntimeLanguageContext() diff --git a/packages/toolkit/.changes/3.61.0.json b/packages/toolkit/.changes/3.61.0.json new file mode 100644 index 00000000000..8c9ca524a61 --- /dev/null +++ b/packages/toolkit/.changes/3.61.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-05-14", + "version": "3.61.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.62.0.json b/packages/toolkit/.changes/3.62.0.json new file mode 100644 index 00000000000..7c2d15933be --- /dev/null +++ b/packages/toolkit/.changes/3.62.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-05-15", + "version": "3.62.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index e21988fcd50..7d36b0551ef 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,11 @@ +## 3.62.0 2025-05-15 + +- Miscellaneous non-user-facing changes + +## 3.61.0 2025-05-14 + +- Miscellaneous non-user-facing changes + ## 3.60.0 2025-05-06 - Miscellaneous non-user-facing changes diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 80e554cf53a..bdf4bcdd324 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.61.0-SNAPSHOT", + "version": "3.63.0-SNAPSHOT", "extensionKind": [ "workspace" ], From d2072d246c6e287f3e4772ec3d94b35d0bf9b1e3 Mon Sep 17 00:00:00 2001 From: hkobew Date: Thu, 15 May 2025 19:43:53 -0400 Subject: [PATCH 33/48] refactor: simplify cases --- packages/amazonq/src/app/inline/sessionManager.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 4c7bb05f464..10f436344bc 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -55,10 +55,7 @@ export class SessionManager { } public getActiveRecommendation(): InlineCompletionItemWithReferences[] { - if (!this.activeSession) { - return [] - } - return this.activeSession.suggestions + return this.activeSession?.suggestions ?? [] } public get acceptedSuggestionCount(): number { From daaa16986bda9981cb24c54fb0ed0a5549ccb936 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 16 May 2025 09:03:52 -0400 Subject: [PATCH 34/48] fix: make the codelense work --- packages/amazonq/src/app/inline/completion.ts | 11 +++++++++++ .../core/src/codewhisperer/commands/basicCommands.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 047a78055bc..78e0fa9cd13 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -34,6 +34,7 @@ import { vsCodeState, inlineCompletionsDebounceDelay, noInlineSuggestionsMsg, + ReferenceInlineProvider, } from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' @@ -123,6 +124,16 @@ export class InlineCompletionManager implements Disposable { ) ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) ReferenceHoverProvider.instance.addCodeReferences(item.insertText as string, item.references) + + // Show codelense for 5 seconds. + ReferenceInlineProvider.instance.setInlineReference( + startLine, + item.insertText as string, + item.references + ) + setTimeout(() => { + ReferenceInlineProvider.instance.removeInlineReference() + }, 5000) } if (item.mostRelevantMissingImports?.length) { await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine) diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index 7fe6078a1d7..f4176ff8cfe 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -145,7 +145,7 @@ export const showReferenceLog = Commands.declare( if (_ !== placeholder) { source = 'ellipsesMenu' } - await vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-reference-log') + await vscode.commands.executeCommand(`${ReferenceLogViewProvider.viewType}.focus`) } ) From 41ac5798c9588a1e4a6d87c805bdf99f467cb690 Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Fri, 16 May 2025 09:56:11 -0400 Subject: [PATCH 35/48] merge(amazonq): Merge feature branch Auth on LSP Identity Server --- docs/lsp.md | 5 +- package-lock.json | 1 - ...-2471c584-3904-4d90-bb7c-61efff219e43.json | 4 + ...-91d391d4-3777-4053-9e71-15b36dfa1f67.json | 4 + ...-9b0e6490-39a8-445f-9d67-9d762de7421c.json | 4 + ...-31d91f84-30cb-4acd-9e39-9dc153edf0a6.json | 4 + ...-ad9a8829-efa0-463b-ba2f-c89ff1cb69e4.json | 4 + packages/amazonq/package.json | 1 - packages/amazonq/src/api.ts | 20 +- packages/amazonq/src/app/amazonqScan/app.ts | 5 +- .../amazonqScan/chat/controller/controller.ts | 10 +- .../chat/controller/messenger/messenger.ts | 12 +- packages/amazonq/src/app/chat/activation.ts | 2 +- packages/amazonq/src/extension.ts | 25 +- packages/amazonq/src/extensionNode.ts | 58 +- .../inlineChat/provider/inlineChatProvider.ts | 6 +- packages/amazonq/src/lsp/activation.ts | 4 +- packages/amazonq/src/lsp/auth.ts | 128 --- packages/amazonq/src/lsp/chat/activation.ts | 46 +- packages/amazonq/src/lsp/chat/messages.ts | 22 +- .../amazonq/src/lsp/chat/webviewProvider.ts | 2 +- packages/amazonq/src/lsp/client.ts | 216 ++++-- .../amazonq/test/e2e/amazonq/utils/setup.ts | 6 +- .../test/unit/amazonq/backend_amazonq.test.ts | 120 +++ .../test/unit/amazonq/lsp/auth.test.ts | 33 - .../unit/amazonq/lsp/chat/messages.test.ts | 16 +- .../commands/invokeRecommendation.test.ts | 3 +- .../region/regionProfileManager.test.ts | 127 ++- .../service/codewhisperer.test.ts | 2 +- .../service/inlineCompletionService.test.ts | 16 +- .../service/keyStrokeHandler.test.ts | 4 + .../service/recommendationHandler.test.ts | 6 +- .../tracker/codewhispererTracker.test.ts | 4 +- .../unit/codewhisperer/util/authUtil.test.ts | 615 +++++++-------- .../codewhisperer/util/showSsoPrompt.test.ts | 22 +- .../commons/connector/baseMessenger.ts | 12 +- .../src/amazonq/explorer/amazonQTreeNode.ts | 4 +- packages/core/src/amazonq/extApi.ts | 17 +- .../core/src/amazonq/session/sessionState.ts | 4 +- packages/core/src/amazonq/util/authUtils.ts | 15 +- packages/core/src/amazonq/util/files.ts | 2 +- .../webview/generators/webViewContent.ts | 2 +- packages/core/src/amazonqDoc/app.ts | 5 +- .../amazonqDoc/controllers/chat/controller.ts | 13 +- .../core/src/amazonqDoc/session/session.ts | 5 +- packages/core/src/amazonqFeatureDev/app.ts | 5 +- .../amazonqFeatureDev/client/featureDev.ts | 2 +- .../controllers/chat/controller.ts | 20 +- .../src/amazonqFeatureDev/session/session.ts | 5 +- packages/core/src/amazonqGumby/app.ts | 4 +- .../chat/controller/controller.ts | 14 +- .../chat/controller/messenger/messenger.ts | 12 +- packages/core/src/amazonqTest/app.ts | 4 +- .../amazonqTest/chat/controller/controller.ts | 10 +- .../chat/controller/messenger/messenger.ts | 12 +- packages/core/src/auth/auth2.ts | 362 +++++++++ packages/core/src/auth/index.ts | 6 +- packages/core/src/auth/sso/cache.ts | 4 +- packages/core/src/auth/sso/constants.ts | 1 + .../src/auth/sso/ssoAccessTokenProvider.ts | 23 +- packages/core/src/auth/utils.ts | 24 +- packages/core/src/codewhisperer/activation.ts | 40 +- .../src/codewhisperer/client/codewhisperer.ts | 11 +- .../codewhisperer/commands/basicCommands.ts | 28 +- .../commands/startSecurityScan.ts | 2 +- packages/core/src/codewhisperer/index.ts | 2 + .../region/regionProfileManager.ts | 105 ++- .../core/src/codewhisperer/region/utils.ts | 3 +- .../service/inlineCompletionService.ts | 4 +- .../service/recommendationHandler.ts | 5 +- .../service/recommendationService.ts | 2 +- .../service/referenceLogViewProvider.ts | 2 +- .../service/securityIssueHoverProvider.ts | 2 +- .../service/securityScanHandler.ts | 2 +- .../codewhispererCodeCoverageTracker.ts | 2 +- .../tracker/codewhispererTracker.ts | 4 +- .../codewhisperer/ui/codeWhispererNodes.ts | 5 +- .../src/codewhisperer/ui/statusBarMenu.ts | 10 +- .../core/src/codewhisperer/util/authUtil.ts | 729 ++++++++---------- .../codewhisperer/util/customizationUtil.ts | 46 +- .../src/codewhisperer/util/getStartUrl.ts | 5 +- .../src/codewhisperer/util/showSsoPrompt.ts | 6 +- .../src/codewhisperer/util/telemetryHelper.ts | 10 +- .../views/activeStateController.ts | 9 +- .../views/lineAnnotationController.ts | 9 +- .../securityIssue/securityIssueWebview.ts | 2 +- .../controllers/chat/controller.ts | 13 +- .../controllers/chat/messenger/messenger.ts | 4 +- .../controllers/chat/telemetryHelper.ts | 26 +- .../src/codewhispererChat/editor/codelens.ts | 4 +- .../login/webview/commonAuthViewProvider.ts | 5 +- packages/core/src/login/webview/index.ts | 1 + .../webview/vue/amazonq/backend_amazonq.ts | 59 +- .../core/src/login/webview/vue/backend.ts | 3 - .../webview/vue/toolkit/backend_toolkit.ts | 4 - .../shared/clients/codewhispererChatClient.ts | 2 +- .../shared/clients/qDeveloperChatClient.ts | 5 +- packages/core/src/shared/featureConfig.ts | 5 +- packages/core/src/shared/globalState.ts | 113 +++ packages/core/src/shared/index.ts | 1 + .../test/amazonq/customizationUtil.test.ts | 28 +- .../src/test/amazonqDoc/controller.test.ts | 7 +- packages/core/src/test/amazonqDoc/utils.ts | 12 + .../controllers/chat/controller.test.ts | 6 +- .../commands/basicCommands.test.ts | 9 +- .../codewhisperer/startSecurityScan.test.ts | 5 + .../codewhispererChat/editor/codelens.test.ts | 11 +- .../core/src/test/credentials/auth2.test.ts | 530 +++++++++++++ .../sso/ssoAccessTokenProvider.test.ts | 10 +- .../core/src/test/credentials/utils.test.ts | 25 +- packages/core/src/test/index.ts | 1 + .../login/webview/vue/backend_amazonq.test.ts | 149 ---- .../src/test/shared/featureConfig.test.ts | 3 + packages/core/src/test/testAuthUtil.ts | 47 ++ .../codewhisperer/referenceTracker.test.ts | 5 +- .../codewhisperer/securityScan.test.ts | 5 +- .../codewhisperer/serviceInvocations.test.ts | 5 +- packages/core/src/testE2E/util/connection.ts | 21 - .../amazonQTransform/transformByQ.test.ts | 6 +- 119 files changed, 2510 insertions(+), 1809 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-2471c584-3904-4d90-bb7c-61efff219e43.json create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-91d391d4-3777-4053-9e71-15b36dfa1f67.json create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json create mode 100644 packages/amazonq/.changes/next-release/Feature-31d91f84-30cb-4acd-9e39-9dc153edf0a6.json create mode 100644 packages/amazonq/.changes/next-release/bugfix-ad9a8829-efa0-463b-ba2f-c89ff1cb69e4.json delete mode 100644 packages/amazonq/src/lsp/auth.ts create mode 100644 packages/amazonq/test/unit/amazonq/backend_amazonq.test.ts delete mode 100644 packages/amazonq/test/unit/amazonq/lsp/auth.test.ts create mode 100644 packages/core/src/auth/auth2.ts create mode 100644 packages/core/src/test/credentials/auth2.test.ts delete mode 100644 packages/core/src/test/login/webview/vue/backend_amazonq.test.ts create mode 100644 packages/core/src/test/testAuthUtil.ts diff --git a/docs/lsp.md b/docs/lsp.md index 42d94d334a4..a0c7a25d8cb 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -26,9 +26,7 @@ sequenceDiagram ## Language Server Debugging -1. Clone https://github.com/aws/language-servers.git and set it up in the same workspace as this project by cmd+shift+p and "add folder to workspace" and selecting the language-servers folder that you just cloned. Your VS code folder structure should look like below. - - +1. Clone https://github.com/aws/language-servers.git and set it up in the same workspace as this project by cmd+shift+p and "add folder to workspace" and selecting the language-servers folder that you just cloned. Your VS code folder structure should look like below. ``` /aws-toolkit-vscode @@ -48,7 +46,6 @@ sequenceDiagram 3. Enable the lsp experiment: ``` "aws.experiments": { - "amazonqLSP": true, "amazonqLSPInline": true, // optional: enables inline completion from flare "amazonqLSPChat": true // optional: enables chat from flare } diff --git a/package-lock.json b/package-lock.json index c190e6df27a..ceec5f10ede 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10883,7 +10883,6 @@ "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.322.tgz", "integrity": "sha512-KtLabV3ycRH31EAZ0xoWrdpIBG3ym8CQAqgkHd9DSefndbepPRa07atfXw73Ok9J5aA81VHCFpx1dwrLg39EcQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "ajv": "^6.12.6", "cross-spawn": "^7.0.6", diff --git a/packages/amazonq/.changes/next-release/Bug Fix-2471c584-3904-4d90-bb7c-61efff219e43.json b/packages/amazonq/.changes/next-release/Bug Fix-2471c584-3904-4d90-bb7c-61efff219e43.json new file mode 100644 index 00000000000..f7af0fbb1a4 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-2471c584-3904-4d90-bb7c-61efff219e43.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Fix Error: 'Amazon Q service is not signed in'" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-91d391d4-3777-4053-9e71-15b36dfa1f67.json b/packages/amazonq/.changes/next-release/Bug Fix-91d391d4-3777-4053-9e71-15b36dfa1f67.json new file mode 100644 index 00000000000..e3a608296a0 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-91d391d4-3777-4053-9e71-15b36dfa1f67.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Fix Error: 'Amazon Q Profile is not selected for IDC connection type'" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json b/packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json new file mode 100644 index 00000000000..f17516bb8f4 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-9b0e6490-39a8-445f-9d67-9d762de7421c.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Previous and subsequent cells are used as context for completion in a Jupyter notebook" +} diff --git a/packages/amazonq/.changes/next-release/Feature-31d91f84-30cb-4acd-9e39-9dc153edf0a6.json b/packages/amazonq/.changes/next-release/Feature-31d91f84-30cb-4acd-9e39-9dc153edf0a6.json new file mode 100644 index 00000000000..da0d200410d --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-31d91f84-30cb-4acd-9e39-9dc153edf0a6.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Add inline completion support for abap language" +} diff --git a/packages/amazonq/.changes/next-release/bugfix-ad9a8829-efa0-463b-ba2f-c89ff1cb69e4.json b/packages/amazonq/.changes/next-release/bugfix-ad9a8829-efa0-463b-ba2f-c89ff1cb69e4.json new file mode 100644 index 00000000000..1b84a3f57c0 --- /dev/null +++ b/packages/amazonq/.changes/next-release/bugfix-ad9a8829-efa0-463b-ba2f-c89ff1cb69e4.json @@ -0,0 +1,4 @@ +{ + "type": "bugfix", + "description": "/review: disable auto-review by default" +} diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 421dff76d4e..0b23ae3cc2b 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -59,7 +59,6 @@ "watch": "npm run clean && npm run buildScripts && tsc -watch -p ./", "testCompile": "npm run clean && npm run buildScripts && npm run compileOnly", "test": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts unit dist/test/unit/index.js ../core/dist/src/testFixtures/workspaceFolder", - "testE2E": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts e2e dist/test/e2e/index.js ../core/dist/src/testFixtures/workspaceFolder", "testWeb": "npm run compileDev && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts web dist/test/web/testRunnerWebCore.js", "webRun": "npx @vscode/test-web --open-devtools --browserOption=--disable-web-security --waitForDebugger=9222 --extensionDevelopmentPath=. .", "webWatch": "npm run clean && npm run buildScripts && webpack --mode development --watch", diff --git a/packages/amazonq/src/api.ts b/packages/amazonq/src/api.ts index 03b2a32ea55..bd7d5c6a361 100644 --- a/packages/amazonq/src/api.ts +++ b/packages/amazonq/src/api.ts @@ -8,6 +8,7 @@ import { GenerateAssistantResponseCommandOutput, GenerateAssistantResponseReques import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { ChatSession } from 'aws-core-vscode/codewhispererChat' import { api } from 'aws-core-vscode/amazonq' +import { getLogger } from 'aws-core-vscode/shared' export default { chatApi: { @@ -26,8 +27,25 @@ export default { await AuthUtil.instance.showReauthenticatePrompt() } }, + /** + * @deprecated use getAuthState() instead + * + * Legacy function for callers who expect auth state to be granular amongst Q features. + * Auth state is consistent between features, so getAuthState() can be consumed safely for all features. + * + */ async getChatAuthState() { - return AuthUtil.instance.getChatAuthState() + getLogger().warn('Warning: getChatAuthState() is deprecated. Use getAuthState() instead.') + const state = AuthUtil.instance.getAuthState() + const convertedState = state === 'notConnected' ? 'disconnected' : state + return { + codewhispererCore: convertedState, + codewhispererChat: convertedState, + amazonQ: convertedState, + } + }, + getAuthState() { + return AuthUtil.instance.getAuthState() }, }, } satisfies api diff --git a/packages/amazonq/src/app/amazonqScan/app.ts b/packages/amazonq/src/app/amazonqScan/app.ts index 21857163bd2..66f45246129 100644 --- a/packages/amazonq/src/app/amazonqScan/app.ts +++ b/packages/amazonq/src/app/amazonqScan/app.ts @@ -19,6 +19,7 @@ import { Messenger } from './chat/controller/messenger/messenger' import { UIMessageListener } from './chat/views/actions/uiMessageListener' import { debounce } from 'lodash' import { Commands, placeholder } from 'aws-core-vscode/shared' +import { auth2 } from 'aws-core-vscode/auth' export function init(appContext: AmazonQAppInitContext) { const scanChatControllerEventEmitters: ScanChatControllerEventEmitters = { @@ -52,7 +53,7 @@ export function init(appContext: AmazonQAppInitContext) { appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(scanChatUIInputEventEmitter), 'review') const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' + const authenticated = AuthUtil.instance.getAuthState() === 'connected' let authenticatingSessionID = '' if (authenticated) { @@ -67,7 +68,7 @@ export function init(appContext: AmazonQAppInitContext) { messenger.sendAuthenticationUpdate(authenticated, [authenticatingSessionID]) }, 500) - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { + AuthUtil.instance.onDidChangeConnectionState((e: auth2.AuthStateEvent) => { return debouncedEvent() }) AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { diff --git a/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts b/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts index 72af0a200c5..1262bce32ee 100644 --- a/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts +++ b/packages/amazonq/src/app/amazonqScan/chat/controller/controller.ts @@ -104,7 +104,7 @@ export class ScanController { telemetry.amazonq_feedback.emit({ featureId: 'amazonQReview', amazonqConversationId: this.sessionStorage.getSession().scanUuid, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, interactionType: data.vote, }) }) @@ -122,8 +122,8 @@ export class ScanController { try { getLogger().debug(`Q - Review: Session created with id: ${session.tabID}`) - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) session.isAuthenticating = true return @@ -161,8 +161,8 @@ export class ScanController { return } // check that the session is authenticated - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) session.isAuthenticating = true return diff --git a/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messenger.ts b/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messenger.ts index 18b05e8bb84..d165fb0b46c 100644 --- a/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messenger.ts +++ b/packages/amazonq/src/app/amazonqScan/chat/controller/messenger/messenger.ts @@ -10,7 +10,6 @@ import { AuthFollowUpType, AuthMessageDataMap } from 'aws-core-vscode/amazonq' import { - FeatureAuthState, SecurityScanError, CodeWhispererConstants, SecurityScanStep, @@ -34,6 +33,7 @@ import { import { i18n } from 'aws-core-vscode/shared' import { ScanAction, scanProgressMessage } from '../../../models/constants' import path from 'path' +import { auth2 } from 'aws-core-vscode/auth' export type UnrecoverableErrorType = 'no-project-found' | 'no-open-file-found' | 'invalid-file-type' @@ -78,19 +78,15 @@ export class Messenger { this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, progressField)) } - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { + public async sendAuthNeededExceptionMessage(credentialState: auth2.AuthState, tabID: string) { let authType: AuthFollowUpType = 'full-auth' let message = AuthMessageDataMap[authType].message - switch (credentialState.amazonQ) { - case 'disconnected': + switch (credentialState) { + case 'notConnected': authType = 'full-auth' message = AuthMessageDataMap[authType].message break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break case 'expired': authType = 're-auth' message = AuthMessageDataMap[authType].message diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts index bf6b7cdc3df..19bc5da150d 100644 --- a/packages/amazonq/src/app/chat/activation.ts +++ b/packages/amazonq/src/app/chat/activation.ts @@ -16,7 +16,7 @@ export async function activate(context: ExtensionContext) { const setupLsp = funcUtil.debounce(async () => { void amazonq.LspController.instance.trySetupLsp(context, { - startUrl: AuthUtil.instance.startUrl, + startUrl: AuthUtil.instance.connection?.startUrl, maxIndexSize: CodeWhispererSettings.instance.getMaxIndexSize(), isVectorIndexEnabled: false, }) diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 45641b37440..5cef3994b7a 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Auth, AuthUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth' +import { authUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth' import { activate as activateCodeWhisperer, shutdown as shutdownCodeWhisperer } from 'aws-core-vscode/codewhisperer' import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode' import { CommonAuthWebview } from 'aws-core-vscode/login' @@ -33,7 +33,7 @@ import { maybeShowMinVscodeWarning, Experiments, isSageMaker, - isAmazonLinux2, + Commands, } from 'aws-core-vscode/shared' import { ExtStartUpSources } from 'aws-core-vscode/telemetry' import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' @@ -44,7 +44,6 @@ import { registerCommands } from './commands' import { focusAmazonQPanel } from 'aws-core-vscode/codewhispererChat' import { activate as activateAmazonqLsp } from './lsp/activation' import { activate as activateInlineCompletion } from './app/inline/activation' -import { hasGlibcPatch } from './lsp/client' export const amazonQContextPrefix = 'amazonq' @@ -119,16 +118,12 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is const extContext = { extensionContext: context, } + + // Auth is dependent on LSP, needs to be activated before CW and Inline + await activateAmazonqLsp(context) + // This contains every lsp agnostic things (auth, security scan, code scan) await activateCodeWhisperer(extContext as ExtContext) - if ( - (Experiments.instance.get('amazonqLSP', true) || Auth.instance.isInternalAmazonUser()) && - (!isAmazonLinux2() || hasGlibcPatch()) - ) { - // start the Amazon Q LSP for internal users first - // for AL2, start LSP if glibc patch is found - await activateAmazonqLsp(context) - } if (!Experiments.instance.get('amazonqLSPInline', false)) { await activateInlineCompletion() } @@ -136,6 +131,10 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // Generic extension commands registerGenericCommands(context, amazonQContextPrefix) + // Create status bar and reference log UI elements + void Commands.tryExecute('aws.amazonq.refreshStatusBar') + void Commands.tryExecute('aws.amazonq.updateReferenceLog') + // Amazon Q specific commands registerCommands(context) @@ -162,7 +161,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // reload webviews await vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction') - if (AuthUtils.ExtensionUse.instance.isFirstUse()) { + if (authUtils.ExtensionUse.instance.isFirstUse()) { // Give time for the extension to finish initializing. globals.clock.setTimeout(async () => { CommonAuthWebview.authSource = ExtStartUpSources.firstStartUp @@ -172,7 +171,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is context.subscriptions.push( Experiments.instance.onDidChange(async (event) => { - if (event.key === 'amazonqLSP' || event.key === 'amazonqChatLSP' || event.key === 'amazonqLSPInline') { + if (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/extensionNode.ts b/packages/amazonq/src/extensionNode.ts index 8224b9ce310..2bcf5c152ce 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -7,28 +7,20 @@ import * as vscode from 'vscode' import { activateAmazonQCommon, amazonQContextPrefix, deactivateCommon } from './extension' import { DefaultAmazonQAppInitContext, AmazonQChatViewProvider } from 'aws-core-vscode/amazonq' import { activate as activateTransformationHub } from 'aws-core-vscode/amazonqGumby' -import { - ExtContext, - globals, - CrashMonitoring, - getLogger, - isNetworkError, - isSageMaker, - Experiments, -} from 'aws-core-vscode/shared' +import { ExtContext, globals, CrashMonitoring, getLogger, isSageMaker, Experiments } from 'aws-core-vscode/shared' import { filetypes, SchemaService } from 'aws-core-vscode/sharedNode' import { updateDevMode } from 'aws-core-vscode/dev' import { CommonAuthViewProvider } from 'aws-core-vscode/login' import { isExtensionActive, VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' import { registerSubmitFeedback } from 'aws-core-vscode/feedback' import { DevOptions } from 'aws-core-vscode/dev' -import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth' +import { Auth, authUtils } from 'aws-core-vscode/auth' import api from './api' import { activate as activateCWChat } from './app/chat/activation' import { activate as activateInlineChat } from './inlineChat/activation' import { beta } from 'aws-core-vscode/dev' import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications' -import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { telemetry, AuthUserState } from 'aws-core-vscode/telemetry' import { activateAgents } from './app/chat/node/activateAgents' @@ -95,52 +87,30 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { await setupDevMode(context) await beta.activate(context) - // TODO: Should probably emit for web as well. - // Will the web metric look the same? + await getAuthState() telemetry.auth_userState.emit({ passive: true, result: 'Succeeded', - source: AuthUtils.ExtensionUse.instance.sourceForTelemetry(), - ...(await getAuthState()), + source: authUtils.ExtensionUse.instance.sourceForTelemetry(), + authStatus: AuthUtil.instance.getAuthState(), + authEnabledConnections: (await AuthUtil.instance.getAuthFormIds()).join(','), }) void activateNotifications(context, getAuthState) } async function getAuthState(): Promise> { - let authState: AuthState = 'disconnected' - try { - // May call connection validate functions that try to refresh the token. - // This could result in network errors. - authState = (await AuthUtil.instance._getChatAuthState(false)).codewhispererChat - } catch (err) { - if ( - isNetworkError(err) && - AuthUtil.instance.conn && - AuthUtil.instance.auth.getConnectionState(AuthUtil.instance.conn) === 'valid' - ) { - authState = 'connectedWithNetworkError' - } else { - throw err - } - } - const currConn = AuthUtil.instance.conn - if (currConn !== undefined && !(isAnySsoConnection(currConn) || isSageMaker())) { - getLogger().error(`Current Amazon Q connection is not SSO, type is: %s`, currConn?.type) - } + const state = AuthUtil.instance.getAuthState() - // Pending profile selection state means users already log in with Sso service - if (authState === 'pendingProfileSelection') { - authState = 'connected' + if (AuthUtil.instance.isConnected() && !(AuthUtil.instance.isSsoSession() || isSageMaker())) { + getLogger().error('Current Amazon Q connection is not SSO') } return { - authStatus: - authState === 'connected' || authState === 'expired' || authState === 'connectedWithNetworkError' - ? authState - : 'notConnected', - authEnabledConnections: AuthUtils.getAuthFormIdsFromConnection(currConn).join(','), - ...(await getTelemetryMetadataForConn(currConn)), + // @ts-ignore + authStatus: (state ?? 'notConnected') as AuthStatus, + authEnabledConnections: (await AuthUtil.instance.getAuthFormIds()).join(','), + ...AuthUtil.instance.getTelemetryMetadata(), } } diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index e6534d65532..b6b29d1bafc 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -123,10 +123,8 @@ export class InlineChatProvider { const tabID = triggerEvent.tabID - const credentialsState = await AuthUtil.instance.getChatAuthState() - if ( - !(credentialsState.codewhispererChat === 'connected' && credentialsState.codewhispererCore === 'connected') - ) { + const credentialsState = AuthUtil.instance.getAuthState() + if (credentialsState !== 'connected') { const { message } = extractAuthFollowUp(credentialsState) this.errorEmitter.fire() throw new ToolkitError(message) diff --git a/packages/amazonq/src/lsp/activation.ts b/packages/amazonq/src/lsp/activation.ts index 84bae8a01a6..aebb4a60479 100644 --- a/packages/amazonq/src/lsp/activation.ts +++ b/packages/amazonq/src/lsp/activation.ts @@ -8,11 +8,11 @@ import { startLanguageServer } from './client' import { AmazonQLspInstaller } from './lspInstaller' import { lspSetupStage, ToolkitError, messages } from 'aws-core-vscode/shared' -export async function activate(ctx: vscode.ExtensionContext): Promise { +export async function activate(ctx: vscode.ExtensionContext) { try { await lspSetupStage('all', async () => { const installResult = await new AmazonQLspInstaller().resolve() - await lspSetupStage('launch', async () => await startLanguageServer(ctx, installResult.resourcePaths)) + return await lspSetupStage('launch', () => startLanguageServer(ctx, installResult.resourcePaths)) }) } catch (err) { const e = err as ToolkitError diff --git a/packages/amazonq/src/lsp/auth.ts b/packages/amazonq/src/lsp/auth.ts deleted file mode 100644 index d81f464d6a3..00000000000 --- a/packages/amazonq/src/lsp/auth.ts +++ /dev/null @@ -1,128 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - bearerCredentialsUpdateRequestType, - ConnectionMetadata, - NotificationType, - RequestType, - ResponseMessage, - UpdateCredentialsParams, -} from '@aws/language-server-runtimes/protocol' -import * as jose from 'jose' -import * as crypto from 'crypto' -import { LanguageClient } from 'vscode-languageclient' -import { AuthUtil } from 'aws-core-vscode/codewhisperer' -import { Writable } from 'stream' -import { onceChanged } from 'aws-core-vscode/utils' -import { getLogger, oneMinute } from 'aws-core-vscode/shared' -import { isSsoConnection } from 'aws-core-vscode/auth' - -export const encryptionKey = crypto.randomBytes(32) - -/** - * Sends a json payload to the language server, who is waiting to know what the encryption key is. - * Code reference: https://github.com/aws/language-servers/blob/7da212185a5da75a72ce49a1a7982983f438651a/client/vscode/src/credentialsActivation.ts#L77 - */ -export function writeEncryptionInit(stream: Writable): void { - const request = { - version: '1.0', - mode: 'JWT', - key: encryptionKey.toString('base64'), - } - stream.write(JSON.stringify(request)) - stream.write('\n') -} - -/** - * Request for custom notifications that Update Credentials and tokens. - * See core\aws-lsp-core\src\credentials\updateCredentialsRequest.ts for details - */ -export interface UpdateCredentialsRequest { - /** - * Encrypted token (JWT or PASETO) - * The token's contents differ whether IAM or Bearer token is sent - */ - data: string - /** - * Used by the runtime based language servers. - * Signals that this client will encrypt its credentials payloads. - */ - encrypted: boolean -} - -export const notificationTypes = { - updateBearerToken: new RequestType( - 'aws/credentials/token/update' - ), - deleteBearerToken: new NotificationType('aws/credentials/token/delete'), - getConnectionMetadata: new RequestType( - 'aws/credentials/getConnectionMetadata' - ), -} - -/** - * Facade over our VSCode Auth that does crud operations on the language server auth - */ -export class AmazonQLspAuth { - #logErrorIfChanged = onceChanged((s) => getLogger('amazonqLsp').error(s)) - constructor( - private readonly client: LanguageClient, - private readonly authUtil: AuthUtil = AuthUtil.instance - ) {} - - /** - * @param force bypass memoization, and forcefully update the bearer token - */ - async refreshConnection(force: boolean = false) { - const activeConnection = this.authUtil.conn - if (this.authUtil.isConnectionValid() && isSsoConnection(activeConnection)) { - // send the token to the language server - const token = await this.authUtil.getBearerToken() - await (force ? this._updateBearerToken(token) : this.updateBearerToken(token)) - } - } - - async logRefreshError(e: unknown) { - const err = e as Error - this.#logErrorIfChanged(`Unable to update bearer token: ${err.name}:${err.message}`) - } - - public updateBearerToken = onceChanged(this._updateBearerToken.bind(this)) - private async _updateBearerToken(token: string) { - const request = await this.createUpdateCredentialsRequest({ - token, - }) - - await this.client.sendRequest(bearerCredentialsUpdateRequestType.method, request) - - this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`) - } - - public startTokenRefreshInterval(pollingTime: number = oneMinute / 2) { - const interval = setInterval(async () => { - await this.refreshConnection().catch((e) => this.logRefreshError(e)) - }, pollingTime) - return interval - } - - private async createUpdateCredentialsRequest(data: any): Promise { - const payload = new TextEncoder().encode(JSON.stringify({ data })) - - const jwt = await new jose.CompactEncrypt(payload) - .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) - .encrypt(encryptionKey) - - return { - data: jwt, - metadata: { - sso: { - startUrl: AuthUtil.instance.startUrl, - }, - }, - encrypted: true, - } - } -} diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index f8e3ee16251..f6277068734 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -11,12 +11,20 @@ import { registerLanguageServerEventListener, registerMessageListeners } from '. import { Commands, getLogger, globals, undefinedIfEmpty } from 'aws-core-vscode/shared' import { activate as registerLegacyChatListeners } from '../../app/chat/activation' import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq' -import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { AuthUtil, getSelectedCustomization, notifyNewCustomizations } from 'aws-core-vscode/codewhisperer' import { pushConfigUpdate } from '../config' export async function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) { const disposables = globals.context.subscriptions + // Make sure we've sent an auth profile to the language server before even initializing the UI + await pushConfigUpdate(languageClient, { + type: 'profile', + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }) + + await initializeCustomizations() + const provider = new AmazonQChatViewProvider(mynahUIPath) disposables.push( @@ -68,10 +76,7 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu await provider.refreshWebview() }), Commands.register('aws.amazonq.updateCustomizations', () => { - void pushConfigUpdate(languageClient, { - type: 'customization', - customization: undefinedIfEmpty(getSelectedCustomization().arn), - }) + pushCustomizationToServer(languageClient) }), globals.logOutputChannel.onDidChangeLogLevel((logLevel) => { getLogger('amazonqLsp').info(`Local log level changed to ${logLevel}, notifying LSP`) @@ -80,4 +85,35 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu }) }) ) + + /** + * Initialize customizations on extension startup + */ + async function initializeCustomizations() { + /** + * Even though this function is called "notify", it has a side effect that first restores the + * cached customization. So for {@link getSelectedCustomization()} to work as expected, we must + * call {@link notifyNewCustomizations} first. + * + * TODO: Separate restoring and notifying, or just rename the function to something better + */ + if (AuthUtil.instance.isIdcConnection() && AuthUtil.instance.isConnected()) { + await notifyNewCustomizations() + } + + /** + * HACK: We must explicitly push the customization here since restoring the customization from cache + * does not currently trigger a push to server. + * + * TODO: Always push to server whenever restoring from cache. + */ + pushCustomizationToServer(languageClient) + } + + function pushCustomizationToServer(languageClient: LanguageClient) { + void pushConfigUpdate(languageClient, { + type: 'customization', + customization: undefinedIfEmpty(getSelectedCustomization().arn), + }) + } } diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 89d221e9442..f9da9eefeb7 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -38,9 +38,6 @@ import { ShowSaveFileDialogParams, LSPErrorCodes, tabBarActionRequestType, - ShowDocumentParams, - ShowDocumentResult, - ShowDocumentRequest, contextCommandsNotificationType, ContextCommandParams, openFileDiffNotificationType, @@ -179,7 +176,7 @@ export function registerMessageListeners( if (fullAuthTypes.includes(authType)) { try { - await AuthUtil.instance.secondaryAuth.deleteConnection() + await AuthUtil.instance.logout() } catch (e) { languageClient.error( `[VSCode Client] Failed to authenticate after AUTH_FOLLOW_UP_CLICKED: ${(e as Error).message}` @@ -428,23 +425,6 @@ export function registerMessageListeners( } }) - languageClient.onRequest( - ShowDocumentRequest.method, - async (params: ShowDocumentParams): Promise> => { - try { - const uri = vscode.Uri.parse(params.uri) - 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, diff --git a/packages/amazonq/src/lsp/chat/webviewProvider.ts b/packages/amazonq/src/lsp/chat/webviewProvider.ts index 1a513f1df3f..3f9d273ab96 100644 --- a/packages/amazonq/src/lsp/chat/webviewProvider.ts +++ b/packages/amazonq/src/lsp/chat/webviewProvider.ts @@ -137,7 +137,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { let qChat = undefined const init = () => { const vscodeApi = acquireVsCodeApi() - const hybridChatConnector = new HybridChatAdapter(${(await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected'},${featureConfigData},${welcomeCount},${disclaimerAcknowledged},${regionProfileString},${disabledCommands},${isSMUS},${isSM},vscodeApi.postMessage) + const hybridChatConnector = new HybridChatAdapter(${AuthUtil.instance.isConnected()},${featureConfigData},${welcomeCount},${disclaimerAcknowledged},${regionProfileString},${disabledCommands},${isSMUS},${isSM},vscodeApi.postMessage) const commands = [hybridChatConnector.initialQuickActions[0]] qChat = amazonQChat.createChat(vscodeApi, {disclaimerAcknowledged: ${disclaimerAcknowledged}, pairProgrammingAcknowledged: ${pairProgrammingAcknowledged}, agenticMode: true, quickActionCommands: commands}, hybridChatConnector, ${JSON.stringify(featureConfigData)}); } diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 549b0ac7dad..9c9fc8a8883 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -5,9 +5,10 @@ import vscode, { env, version } from 'vscode' import * as nls from 'vscode-nls' +import * as crypto from 'crypto' +import * as jose from 'jose' import { LanguageClient, LanguageClientOptions, RequestType, State } from 'vscode-languageclient' import { InlineCompletionManager } from '../app/inline/completion' -import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth' import { CreateFilesParams, DeleteFilesParams, @@ -17,6 +18,19 @@ import { RenameFilesParams, ResponseMessage, WorkspaceFolder, + GetSsoTokenProgress, + GetSsoTokenProgressToken, + GetSsoTokenProgressType, + MessageActionItem, + ShowMessageRequest, + ShowMessageRequestParams, + ConnectionMetadata, + ShowDocumentRequest, + ShowDocumentParams, + ShowDocumentResult, + ResponseError, + LSPErrorCodes, + updateConfigurationRequestType, } from '@aws/language-server-runtimes/protocol' import { AuthUtil, CodeWhispererSettings, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' import { @@ -25,18 +39,20 @@ import { globals, Experiments, Commands, - oneSecond, validateNodeExe, getLogger, undefinedIfEmpty, getOptOutPreference, isAmazonLinux2, + oidcClientName, + openUrl, getClientId, extensionVersion, } from 'aws-core-vscode/shared' import { processUtils } from 'aws-core-vscode/shared' -import { activate } from './chat/activation' +import { activate as activateChat } from './chat/activation' import { AmazonQResourcePaths } from './lspInstaller' +import { auth2 } from 'aws-core-vscode/auth' import { ConfigSection, isValidConfigSection, pushConfigUpdate, toAmazonQLSPLogLevel } from './config' import { telemetry } from 'aws-core-vscode/telemetry' @@ -45,15 +61,18 @@ const logger = getLogger('amazonqLsp.lspClient') export const glibcLinker: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER || '' export const glibcPath: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH || '' - export function hasGlibcPatch(): boolean { return glibcLinker.length > 0 && glibcPath.length > 0 } +export const clientId = 'amazonq' +export const clientName = oidcClientName() +export const encryptionKey = crypto.randomBytes(32) + export async function startLanguageServer( extensionContext: vscode.ExtensionContext, resourcePaths: AmazonQResourcePaths -) { +): Promise { const toDispose = extensionContext.subscriptions const serverModule = resourcePaths.lsp @@ -67,8 +86,6 @@ export async function startLanguageServer( ] const documentSelector = [{ scheme: 'file', language: '*' }] - - const clientId = 'amazonq' const traceServerEnabled = Settings.instance.isSet(`${clientId}.trace.server`) let executable: string[] = [] // apply the GLIBC 2.28 path to node js runtime binary @@ -91,6 +108,7 @@ export async function startLanguageServer( await validateNodeExe(executable, resourcePaths.lsp, argv, logger) // Options to control the language client + const clientName = 'AmazonQ-For-VSCode' const clientOptions: LanguageClientOptions = { // Register the server for json documents documentSelector, @@ -115,7 +133,7 @@ export async function startLanguageServer( name: env.appName, version: version, extension: { - name: 'AmazonQ-For-VSCode', + name: clientName, version: extensionVersion, }, clientId: getClientId(globals.globalState), @@ -150,36 +168,149 @@ export async function startLanguageServer( }), } - const client = new LanguageClient( - clientId, - localize('amazonq.server.name', 'Amazon Q Language Server'), - serverOptions, - clientOptions - ) + const lspName = localize('amazonq.server.name', 'Amazon Q Language Server') + const client = new LanguageClient(clientId, lspName, serverOptions, clientOptions) const disposable = client.start() toDispose.push(disposable) await client.onReady() - const auth = await initializeAuth(client) + await client.onReady() + + /** + * We use the Flare Auth language server, and our Auth client depends on it. + * Because of this we initialize our Auth client **immediately** after the language server is ready. + * Doing this removes the chance of something else attempting to use the Auth client before it is ready. + */ + await initializeAuth(client) - await onLanguageServerReady(auth, client, resourcePaths, toDispose) + await postStartLanguageServer(client, resourcePaths, toDispose) return client -} -async function initializeAuth(client: LanguageClient): Promise { - const auth = new AmazonQLspAuth(client) - await auth.refreshConnection(true) - return auth + async function initializeAuth(client: LanguageClient) { + AuthUtil.create(new auth2.LanguageClientAuth(client, clientId, encryptionKey)) + + // Migrate SSO connections from old Auth to the LSP identity server + // This function only migrates connections once + // This call can be removed once all/most users have updated to the latest AmazonQ version + try { + await AuthUtil.instance.migrateSsoConnectionToLsp(clientName) + } catch (e) { + getLogger().error(`Error while migration SSO connection to Amazon Q LSP: ${e}`) + } + + /** All must be setup before {@link AuthUtil.restore} otherwise they may not trigger when expected */ + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => { + void pushConfigUpdate(client, { + type: 'profile', + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }) + }) + + // Try and restore a cached connection if exists + await AuthUtil.instance.restore() + } } -async function onLanguageServerReady( - auth: AmazonQLspAuth, +async function postStartLanguageServer( client: LanguageClient, resourcePaths: AmazonQResourcePaths, toDispose: vscode.Disposable[] ) { + // Request handler for when the server wants to know about the clients auth connnection. Must be registered before the initial auth init call + client.onRequest(auth2.notificationTypes.getConnectionMetadata.method, () => { + return { + sso: { + startUrl: AuthUtil.instance.connection?.startUrl, + }, + } + }) + + client.onRequest( + ShowMessageRequest.method, + async (params: ShowMessageRequestParams) => { + const actions = params.actions?.map((a) => a.title) ?? [] + const response = await vscode.window.showInformationMessage(params.message, { modal: true }, ...actions) + return params.actions?.find((a) => a.title === response) ?? (undefined as unknown as null) + } + ) + + client.onRequest( + ShowDocumentRequest.method, + async (params: ShowDocumentParams): Promise> => { + const uri = vscode.Uri.parse(params.uri) + getLogger().info(`Processing ShowDocumentRequest for URI scheme: ${uri.scheme}`) + try { + if (uri.scheme.startsWith('http')) { + getLogger().info('Opening URL...') + await openUrl(vscode.Uri.parse(params.uri)) + } else { + getLogger().info('Opening text document...') + 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 process ShowDocumentRequest: ${(e as Error).message}` + ) + } + } + ) + + const sendProfileToLsp = async () => { + try { + const result = await client.sendRequest(updateConfigurationRequestType.method, { + section: 'aws.q', + settings: { + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }, + }) + client.info( + `Client: Updated Amazon Q Profile ${AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn} to Amazon Q LSP`, + result + ) + } catch (err) { + client.error('Error when setting Q Developer Profile to Amazon Q LSP', err) + } + } + + let promise: Promise | undefined + let resolver: () => void = () => {} + client.onProgress(GetSsoTokenProgressType, GetSsoTokenProgressToken, async (partialResult: GetSsoTokenProgress) => { + const decryptedKey = await jose.compactDecrypt(partialResult as unknown as string, encryptionKey) + const val: GetSsoTokenProgress = JSON.parse(decryptedKey.plaintext.toString()) + + if (val.state === 'InProgress') { + if (promise) { + resolver() + } + promise = new Promise((resolve) => { + resolver = resolve + }) + } else { + resolver() + promise = undefined + return + } + + // send profile to lsp once. + void sendProfileToLsp() + + void vscode.window.withProgress( + { + cancellable: true, + location: vscode.ProgressLocation.Notification, + title: val.message, + }, + async (_) => { + await promise + } + ) + }) + if (Experiments.instance.get('amazonqLSPInline', false)) { const inlineManager = new InlineCompletionManager(client) inlineManager.registerInlineCompletion() @@ -193,33 +324,12 @@ async function onLanguageServerReady( }) ) } - if (Experiments.instance.get('amazonqChatLSP', true)) { - await activate(client, encryptionKey, resourcePaths.ui) - } - - const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond) - - // We manually push the cached values the first time since event handlers, which should push, may not have been setup yet. - // Execution order is weird and should be fixed in the flare implementation. - // TODO: Revisit if we need this if we setup the event handlers properly - if (AuthUtil.instance.isConnectionValid()) { - await sendProfileToLsp(client) - - await pushConfigUpdate(client, { - type: 'customization', - customization: getSelectedCustomization(), - }) + await activateChat(client, encryptionKey, resourcePaths.ui) } toDispose.push( - AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { - await auth.refreshConnection() - }), - AuthUtil.instance.auth.onDidDeleteConnection(async () => { - client.sendNotification(notificationTypes.deleteBearerToken.method) - }), - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => sendProfileToLsp(client)), + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(sendProfileToLsp), vscode.commands.registerCommand('aws.amazonq.getWorkspaceId', async () => { const requestType = new RequestType( 'aws/getConfigurationFromServer' @@ -275,24 +385,16 @@ async function onLanguageServerReady( }, } as DidChangeWorkspaceFoldersParams) }), - { dispose: () => clearInterval(refreshInterval) }, // Set this inside onReady so that it only triggers on subsequent language server starts (not the first) - onServerRestartHandler(client, auth) + onServerRestartHandler(client) ) - - async function sendProfileToLsp(client: LanguageClient) { - await pushConfigUpdate(client, { - type: 'profile', - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) - } } /** * When the server restarts (likely due to a crash, then the LanguageClient automatically starts it again) * we need to run some server intialization again. */ -function onServerRestartHandler(client: LanguageClient, auth: AmazonQLspAuth) { +function onServerRestartHandler(client: LanguageClient) { return client.onDidChangeState(async (e) => { // Ensure we are in a "restart" state if (!(e.oldState === State.Starting && e.newState === State.Running)) { @@ -306,7 +408,7 @@ function onServerRestartHandler(client: LanguageClient, auth: AmazonQLspAuth) { telemetry.languageServer_crash.emit({ id: 'AmazonQ' }) // Need to set the auth token in the again - await auth.refreshConnection(true) + await AuthUtil.instance.restore() }) } diff --git a/packages/amazonq/test/e2e/amazonq/utils/setup.ts b/packages/amazonq/test/e2e/amazonq/utils/setup.ts index dd82b1f0b19..ef7ba540198 100644 --- a/packages/amazonq/test/e2e/amazonq/utils/setup.ts +++ b/packages/amazonq/test/e2e/amazonq/utils/setup.ts @@ -5,9 +5,9 @@ import { AuthUtil } from 'aws-core-vscode/codewhisperer' export async function loginToIdC() { - const authState = await AuthUtil.instance.getChatAuthState() + const authState = AuthUtil.instance.getAuthState() if (process.env['AWS_TOOLKIT_AUTOMATION'] === 'local') { - if (authState.amazonQ !== 'connected') { + if (authState !== 'connected') { throw new Error('You will need to login manually before running tests.') } return @@ -22,5 +22,5 @@ export async function loginToIdC() { ) } - await AuthUtil.instance.connectToEnterpriseSso(startUrl, region) + await AuthUtil.instance.login(startUrl, region) } diff --git a/packages/amazonq/test/unit/amazonq/backend_amazonq.test.ts b/packages/amazonq/test/unit/amazonq/backend_amazonq.test.ts new file mode 100644 index 00000000000..5d9972019f4 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/backend_amazonq.test.ts @@ -0,0 +1,120 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { assertTelemetry, createTestAuthUtil } from 'aws-core-vscode/test' +import { AuthUtil, awsIdSignIn, getStartUrl } from 'aws-core-vscode/codewhisperer' +import { backendAmazonQ } from 'aws-core-vscode/login' + +describe('Amazon Q Login', async function () { + const region = 'fakeRegion' + const startUrl = 'fakeUrl' + + let sandbox: sinon.SinonSandbox + let backend: backendAmazonQ.AmazonQLoginWebview + + beforeEach(async function () { + await createTestAuthUtil() + sandbox = sinon.createSandbox() + backend = new backendAmazonQ.AmazonQLoginWebview() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('signs into builder ID and emits telemetry', async function () { + await backend.startBuilderIdSetup() + + assert.ok(AuthUtil.instance.isConnected()) + assert.ok(AuthUtil.instance.isBuilderIdConnection()) + + assertTelemetry('auth_addConnection', { + result: 'Succeeded', + credentialSourceId: 'awsId', + authEnabledFeatures: 'codewhisperer', + isReAuth: false, + ssoRegistrationExpiresAt: undefined, + ssoRegistrationClientId: undefined, + }) + }) + + it('signs into IdC and emits telemetry', async function () { + await backend.startEnterpriseSetup(startUrl, region) + + assert.ok(AuthUtil.instance.isConnected()) + assert.ok(AuthUtil.instance.isIdcConnection()) + assert.ok(AuthUtil.instance.isSsoSession()) + assert.deepStrictEqual(AuthUtil.instance.connection?.startUrl, startUrl) + assert.deepStrictEqual(AuthUtil.instance.connection?.region, region) + + assertTelemetry('auth_addConnection', { + result: 'Succeeded', + credentialSourceId: 'iamIdentityCenter', + authEnabledFeatures: 'codewhisperer', + credentialStartUrl: startUrl, + awsRegion: region, + isReAuth: false, + ssoRegistrationExpiresAt: undefined, + ssoRegistrationClientId: undefined, + }) + }) + + it('reauths builder ID and emits telemetry', async function () { + await awsIdSignIn() + + await backend.reauthenticateConnection() + + assert.ok(AuthUtil.instance.isConnected()) + + assertTelemetry('auth_addConnection', { + result: 'Succeeded', + credentialSourceId: 'awsId', + authEnabledFeatures: 'codewhisperer', + isReAuth: true, + ssoRegistrationExpiresAt: undefined, + ssoRegistrationClientId: undefined, + }) + }) + + it('reauths IdC and emits telemetry', async function () { + await getStartUrl.connectToEnterpriseSso(startUrl, region) + + await backend.reauthenticateConnection() + + assert.ok(AuthUtil.instance.isConnected()) + + assertTelemetry('auth_addConnection', { + result: 'Succeeded', + credentialSourceId: 'iamIdentityCenter', + authEnabledFeatures: 'codewhisperer', + credentialStartUrl: startUrl, + awsRegion: region, + isReAuth: true, + ssoRegistrationExpiresAt: undefined, + ssoRegistrationClientId: undefined, + }) + }) + + it('signs out of reauth and emits telemetry', async function () { + await getStartUrl.connectToEnterpriseSso(startUrl, region) + + await backend.signout() + + assert.ok(!AuthUtil.instance.isConnected()) + + assertTelemetry('auth_addConnection', { + result: 'Cancelled', + credentialSourceId: 'iamIdentityCenter', + authEnabledFeatures: 'codewhisperer', + credentialStartUrl: startUrl, + awsRegion: region, + isReAuth: true, + ssoRegistrationExpiresAt: undefined, + ssoRegistrationClientId: undefined, + }) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/auth.test.ts b/packages/amazonq/test/unit/amazonq/lsp/auth.test.ts deleted file mode 100644 index d55fef85f39..00000000000 --- a/packages/amazonq/test/unit/amazonq/lsp/auth.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import assert from 'assert' -import { AmazonQLspAuth } from '../../../../src/lsp/auth' -import { LanguageClient } from 'vscode-languageclient' - -describe('AmazonQLspAuth', function () { - describe('updateBearerToken', function () { - it('makes request to LSP when token changes', async function () { - // Note: this token will be encrypted - let lastSentToken = {} - const auth = new AmazonQLspAuth({ - sendRequest: (_method: string, param: any) => { - lastSentToken = param - }, - info: (_message: string, _data: any) => {}, - } as LanguageClient) - - await auth.updateBearerToken('firstToken') - assert.notDeepStrictEqual(lastSentToken, {}) - const encryptedFirstToken = lastSentToken - - await auth.updateBearerToken('secondToken') - assert.notDeepStrictEqual(lastSentToken, encryptedFirstToken) - const encryptedSecondToken = lastSentToken - - await auth.updateBearerToken('secondToken') - assert.deepStrictEqual(lastSentToken, encryptedSecondToken) - }) - }) -}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts index b2f5958f52b..caf74bda6e0 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts @@ -8,7 +8,7 @@ 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' +import { AuthFollowUpType } from 'aws-core-vscode/amazonq' import { messages } from 'aws-core-vscode/shared' describe('registerMessageListeners', () => { @@ -50,7 +50,7 @@ describe('registerMessageListeners', () => { describe('AUTH_FOLLOW_UP_CLICKED', () => { let mockAuthUtil: AuthUtil - let deleteConnectionStub: sinon.SinonStub + let logoutStub: sinon.SinonStub let reauthenticateStub: sinon.SinonStub const authFollowUpClickedCommand = 'authFollowUpClicked' @@ -76,14 +76,12 @@ describe('registerMessageListeners', () => { } beforeEach(() => { - deleteConnectionStub = sandbox.stub().resolves() reauthenticateStub = sandbox.stub().resolves() + logoutStub = sandbox.stub().resolves() mockAuthUtil = { reauthenticate: reauthenticateStub, - secondaryAuth: { - deleteConnection: deleteConnectionStub, - } as unknown as secondaryAuth.SecondaryAuth, + logout: logoutStub, } as unknown as AuthUtil sandbox.replaceGetter(AuthUtil, 'instance', () => mockAuthUtil) @@ -98,7 +96,7 @@ describe('registerMessageListeners', () => { }) sinon.assert.calledOnce(reauthenticateStub) - sinon.assert.notCalled(deleteConnectionStub) + sinon.assert.notCalled(logoutStub) }) it('handles full authentication request', async () => { @@ -110,7 +108,7 @@ describe('registerMessageListeners', () => { }) sinon.assert.notCalled(reauthenticateStub) - sinon.assert.calledOnce(deleteConnectionStub) + sinon.assert.calledOnce(logoutStub) }) it('logs error if re-authentication fails', async () => { @@ -124,7 +122,7 @@ describe('registerMessageListeners', () => { it('logs error if full authentication fails', async () => { await testFailure({ authType: 'full-auth', - stubToReject: deleteConnectionStub, + stubToReject: logoutStub, errorMessage: 'Failed to authenticate', }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts index 68cebe37bb1..56f72edfd3f 100644 --- a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' import * as sinon from 'sinon' -import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' +import { resetCodeWhispererGlobalVariables, createMockTextEditor, createTestAuthUtil } from 'aws-core-vscode/test' import { ConfigurationEntry, invokeRecommendation, @@ -20,6 +20,7 @@ describe('invokeRecommendation', function () { let mockClient: DefaultCodeWhispererClient beforeEach(async function () { + await createTestAuthUtil() await resetCodeWhispererGlobalVariables() getRecommendationStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') }) diff --git a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts index 11441b9bf6f..aa79e9052bd 100644 --- a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts @@ -7,41 +7,37 @@ import * as sinon from 'sinon' import assert, { fail } from 'assert' import { AuthUtil, RegionProfile, RegionProfileManager, defaultServiceConfig } from 'aws-core-vscode/codewhisperer' import { globals } from 'aws-core-vscode/shared' -import { createTestAuth } from 'aws-core-vscode/test' -import { SsoConnection } from 'aws-core-vscode/auth' +import { constants } from 'aws-core-vscode/auth' +import { createTestAuthUtil } from 'aws-core-vscode/test' const enterpriseSsoStartUrl = 'https://enterprise.awsapps.com/start' +const region = 'us-east-1' -describe('RegionProfileManager', function () { - let sut: RegionProfileManager - let auth: ReturnType - let authUtil: AuthUtil +describe('RegionProfileManager', async function () { + let regionProfileManager: RegionProfileManager const profileFoo: RegionProfile = { name: 'foo', - region: 'us-east-1', + region, arn: 'foo arn', description: 'foo description', } async function setupConnection(type: 'builderId' | 'idc') { if (type === 'builderId') { - await authUtil.connectToAwsBuilderId() - const conn = authUtil.conn - assert.strictEqual(conn?.type, 'sso') - assert.strictEqual(conn.label, 'AWS Builder ID') + await AuthUtil.instance.login(constants.builderIdStartUrl, region) + assert.ok(AuthUtil.instance.isSsoSession()) + assert.ok(AuthUtil.instance.isBuilderIdConnection()) } else if (type === 'idc') { - await authUtil.connectToEnterpriseSso(enterpriseSsoStartUrl, 'us-east-1') - const conn = authUtil.conn - assert.strictEqual(conn?.type, 'sso') - assert.strictEqual(conn.label, 'IAM Identity Center (enterprise)') + await AuthUtil.instance.login(enterpriseSsoStartUrl, region) + assert.ok(AuthUtil.instance.isSsoSession()) + assert.ok(AuthUtil.instance.isIdcConnection()) } } - beforeEach(function () { - auth = createTestAuth(globals.globalState) - authUtil = new AuthUtil(auth) - sut = new RegionProfileManager(() => authUtil.conn) + beforeEach(async function () { + await createTestAuthUtil() + regionProfileManager = new RegionProfileManager(AuthUtil.instance) }) afterEach(function () { @@ -65,12 +61,12 @@ describe('RegionProfileManager', function () { const mockClient = { listAvailableProfiles: listProfilesStub, } - const createClientStub = sinon.stub(sut, '_createQClient').resolves(mockClient) + const createClientStub = sinon.stub(regionProfileManager, '_createQClient').resolves(mockClient) - const r = await sut.listRegionProfile() + const profileList = await regionProfileManager.listRegionProfile() - assert.strictEqual(r.length, 2) - assert.deepStrictEqual(r, [ + assert.strictEqual(profileList.length, 2) + assert.deepStrictEqual(profileList, [ { name: 'foo', arn: 'arn', @@ -93,41 +89,42 @@ describe('RegionProfileManager', function () { describe('switch and get profile', function () { it('should switch if connection is IdC', async function () { await setupConnection('idc') - await sut.switchRegionProfile(profileFoo, 'user') - assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) + await regionProfileManager.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(regionProfileManager.activeRegionProfile, profileFoo) }) it('should do nothing and return undefined if connection is builder id', async function () { await setupConnection('builderId') - await sut.switchRegionProfile(profileFoo, 'user') - assert.deepStrictEqual(sut.activeRegionProfile, undefined) + await regionProfileManager.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(regionProfileManager.activeRegionProfile, undefined) }) }) describe(`client config`, function () { it(`no valid credential should throw`, async function () { - assert.ok(authUtil.conn === undefined) + await AuthUtil.instance.logout() + + assert.ok(!AuthUtil.instance.isConnected()) assert.throws(() => { - sut.clientConfig + regionProfileManager.clientConfig }, /trying to get client configuration without credential/) }) it(`builder id should always use default profile IAD`, async function () { await setupConnection('builderId') - await sut.switchRegionProfile(profileFoo, 'user') - assert.deepStrictEqual(sut.activeRegionProfile, undefined) - const conn = authUtil.conn - if (!conn) { + await regionProfileManager.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(regionProfileManager.activeRegionProfile, undefined) + if (!AuthUtil.instance.isConnected()) { fail('connection should not be undefined') } - assert.deepStrictEqual(sut.clientConfig, defaultServiceConfig) + assert.deepStrictEqual(regionProfileManager.clientConfig, defaultServiceConfig) }) it(`idc should return correct endpoint corresponding to profile region`, async function () { await setupConnection('idc') - await sut.switchRegionProfile( + await regionProfileManager.switchRegionProfile( { name: 'foo', region: 'eu-central-1', @@ -136,8 +133,8 @@ describe('RegionProfileManager', function () { }, 'user' ) - assert.ok(sut.activeRegionProfile) - assert.deepStrictEqual(sut.clientConfig, { + assert.ok(regionProfileManager.activeRegionProfile) + assert.deepStrictEqual(regionProfileManager.clientConfig, { region: 'eu-central-1', endpoint: 'https://q.eu-central-1.amazonaws.com/', }) @@ -145,7 +142,7 @@ describe('RegionProfileManager', function () { it(`idc should throw if corresponding endpoint is not defined`, async function () { await setupConnection('idc') - await sut.switchRegionProfile( + await regionProfileManager.switchRegionProfile( { name: 'foo', region: 'unknown region', @@ -156,7 +153,7 @@ describe('RegionProfileManager', function () { ) assert.throws(() => { - sut.clientConfig + regionProfileManager.clientConfig }, /Q client configuration error, endpoint not found for region*/) }) }) @@ -164,14 +161,13 @@ describe('RegionProfileManager', function () { describe('persistence', function () { it('persistSelectedRegionProfile', async function () { await setupConnection('idc') - await sut.switchRegionProfile(profileFoo, 'user') - assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) - const conn = authUtil.conn - if (!conn) { + await regionProfileManager.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(regionProfileManager.activeRegionProfile, profileFoo) + if (!AuthUtil.instance.isConnected()) { fail('connection should not be undefined') } - await sut.persistSelectRegionProfile() + await regionProfileManager.persistSelectRegionProfile() const state = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', @@ -179,25 +175,24 @@ describe('RegionProfileManager', function () { {} ) - assert.strictEqual(state[conn.id], profileFoo) + assert.strictEqual(state[AuthUtil.instance.profileName], profileFoo) }) it(`restoreRegionProfile`, async function () { - sinon.stub(sut, 'listRegionProfile').resolves([profileFoo]) + sinon.stub(regionProfileManager, 'listRegionProfile').resolves([profileFoo]) await setupConnection('idc') - const conn = authUtil.conn - if (!conn) { + if (!AuthUtil.instance.isConnected()) { fail('connection should not be undefined') } const state = {} as any - state[conn.id] = profileFoo + state[AuthUtil.instance.profileName] = profileFoo await globals.globalState.update('aws.amazonq.regionProfiles', state) - await sut.restoreRegionProfile(conn) + await regionProfileManager.restoreRegionProfile() - assert.strictEqual(sut.activeRegionProfile, profileFoo) + assert.strictEqual(regionProfileManager.activeRegionProfile, profileFoo) }) }) @@ -205,25 +200,24 @@ describe('RegionProfileManager', function () { it('should reset activeProfile and global state', async function () { // setup await setupConnection('idc') - await sut.switchRegionProfile(profileFoo, 'user') - assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) - const conn = authUtil.conn - if (!conn) { + await regionProfileManager.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(regionProfileManager.activeRegionProfile, profileFoo) + if (!AuthUtil.instance.isConnected()) { fail('connection should not be undefined') } - await sut.persistSelectRegionProfile() + await regionProfileManager.persistSelectRegionProfile() const state = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', Object, {} ) - assert.strictEqual(state[conn.id], profileFoo) + assert.strictEqual(state[AuthUtil.instance.profileName], profileFoo) // subject to test - await sut.invalidateProfile(profileFoo.arn) + await regionProfileManager.invalidateProfile(profileFoo.arn) // assertion - assert.strictEqual(sut.activeRegionProfile, undefined) + assert.strictEqual(regionProfileManager.activeRegionProfile, undefined) const actualGlobalState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', Object, @@ -237,7 +231,7 @@ describe('RegionProfileManager', function () { it(`should configure the endpoint and region from a profile`, async function () { await setupConnection('idc') - const iadClient = await sut.createQClient({ + const iadClient = await regionProfileManager.createQClient({ name: 'foo', region: 'us-east-1', arn: 'arn', @@ -247,7 +241,7 @@ describe('RegionProfileManager', function () { assert.deepStrictEqual(iadClient.config.region, 'us-east-1') assert.deepStrictEqual(iadClient.endpoint.href, 'https://q.us-east-1.amazonaws.com/') - const fraClient = await sut.createQClient({ + const fraClient = await regionProfileManager.createQClient({ name: 'bar', region: 'eu-central-1', arn: 'arn', @@ -263,7 +257,7 @@ describe('RegionProfileManager', function () { await assert.rejects( async () => { - await sut.createQClient({ + await regionProfileManager.createQClient({ name: 'foo', region: 'ap-east-1', arn: 'arn', @@ -275,7 +269,7 @@ describe('RegionProfileManager', function () { await assert.rejects( async () => { - await sut.createQClient({ + await regionProfileManager.createQClient({ name: 'foo', region: 'unknown-somewhere', arn: 'arn', @@ -288,11 +282,10 @@ describe('RegionProfileManager', function () { it(`should configure the endpoint and region correspondingly`, async function () { await setupConnection('idc') - await sut.switchRegionProfile(profileFoo, 'user') - assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) - const conn = authUtil.conn as SsoConnection + await regionProfileManager.switchRegionProfile(profileFoo, 'user') + assert.deepStrictEqual(regionProfileManager.activeRegionProfile, profileFoo) - const client = await sut._createQClient('eu-central-1', 'https://amazon.com/', conn) + const client = await regionProfileManager._createQClient('eu-central-1', 'https://amazon.com/') assert.deepStrictEqual(client.config.region, 'eu-central-1') assert.deepStrictEqual(client.endpoint.href, 'https://amazon.com/') diff --git a/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts b/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts index f30d92de496..e222fb9bda4 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts @@ -136,7 +136,7 @@ describe('codewhisperer', async function () { }), } as Request) - const authUtilStub = sinon.stub(AuthUtil.instance, 'isValidEnterpriseSsoInUse').returns(isSso) + const authUtilStub = sinon.stub(AuthUtil.instance, 'isIdcConnection').returns(isSso) await globals.telemetry.setTelemetryEnabled(isTelemetryEnabled) await codeWhispererClient.sendTelemetryEvent({ telemetryEvent: payload }) const expectedOptOutPreference = isTelemetryEnabled ? 'OPTIN' : 'OPTOUT' diff --git a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts index 18fd7d2f21b..dd0bd65505f 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts @@ -19,7 +19,12 @@ import { listCodeWhispererCommandsId, DefaultCodeWhispererClient, } from 'aws-core-vscode/codewhisperer' -import { createMockTextEditor, resetCodeWhispererGlobalVariables, createMockDocument } from 'aws-core-vscode/test' +import { + createMockTextEditor, + resetCodeWhispererGlobalVariables, + createMockDocument, + createTestAuthUtil, +} from 'aws-core-vscode/test' describe('inlineCompletionService', function () { beforeEach(async function () { @@ -192,6 +197,7 @@ describe('codewhisperer status bar', function () { } beforeEach(async function () { + await createTestAuthUtil() await resetCodeWhispererGlobalVariables() sandbox = sinon.createSandbox() statusBar = new TestStatusBar() @@ -203,7 +209,7 @@ describe('codewhisperer status bar', function () { }) it('shows correct status bar when auth is not connected', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(false) + sandbox.stub(AuthUtil.instance, 'isConnected').returns(false) sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) await service.refreshStatusBar() @@ -215,7 +221,7 @@ describe('codewhisperer status bar', function () { }) it('shows correct status bar when auth is connected', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(true) + sandbox.stub(AuthUtil.instance, 'isConnected').returns(true) sandbox.stub(CodeSuggestionsState.instance, 'isSuggestionsEnabled').returns(true) await service.refreshStatusBar() @@ -227,7 +233,7 @@ describe('codewhisperer status bar', function () { }) it('shows correct status bar when auth is connected but paused', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(true) + sandbox.stub(AuthUtil.instance, 'isConnected').returns(true) sandbox.stub(CodeSuggestionsState.instance, 'isSuggestionsEnabled').returns(false) await service.refreshStatusBar() @@ -239,7 +245,7 @@ describe('codewhisperer status bar', function () { }) it('shows correct status bar when auth is expired', async function () { - sandbox.stub(AuthUtil.instance, 'isConnectionValid').returns(false) + sandbox.stub(AuthUtil.instance, 'isConnected').returns(false) sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(true) await service.refreshStatusBar() diff --git a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts index 4b6a5291f22..f3fa7b399d1 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts @@ -9,6 +9,7 @@ import * as sinon from 'sinon' import * as codewhispererSdkClient from 'aws-core-vscode/codewhisperer' import { createMockTextEditor, + createTestAuthUtil, createTextDocumentChangeEvent, resetCodeWhispererGlobalVariables, } from 'aws-core-vscode/test' @@ -160,13 +161,16 @@ describe('keyStrokeHandler', function () { describe('invokeAutomatedTrigger', function () { let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient + beforeEach(async function () { + await createTestAuthUtil() sinon.restore() mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() await resetCodeWhispererGlobalVariables() sinon.stub(mockClient, 'listRecommendations') sinon.stub(mockClient, 'generateRecommendations') }) + afterEach(function () { sinon.restore() }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts index 86dfc5e514c..d8d04516e85 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts @@ -9,13 +9,13 @@ import * as sinon from 'sinon' import { ReferenceInlineProvider, session, - AuthUtil, DefaultCodeWhispererClient, RecommendationsList, ConfigurationEntry, RecommendationHandler, CodeWhispererCodeCoverageTracker, supplementalContextUtil, + AuthUtil, } from 'aws-core-vscode/codewhisperer' import { assertTelemetryCurried, @@ -39,7 +39,6 @@ describe('recommendationHandler', function () { describe('getRecommendations', async function () { const mockClient = stub(DefaultCodeWhispererClient) const mockEditor = createMockTextEditor() - const testStartUrl = 'testStartUrl' beforeEach(async function () { sinon.restore() @@ -47,7 +46,6 @@ describe('recommendationHandler', function () { mockClient.listRecommendations.resolves({}) mockClient.generateRecommendations.resolves({}) RecommendationHandler.instance.clearRecommendations() - sinon.stub(AuthUtil.instance, 'startUrl').value(testStartUrl) }) afterEach(function () { @@ -143,7 +141,7 @@ describe('recommendationHandler', function () { codewhispererLineNumber: 1, codewhispererCursorOffset: 38, codewhispererLanguage: 'python', - credentialStartUrl: testStartUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, codewhispererSupplementalContextIsUtg: false, codewhispererSupplementalContextTimeout: false, codewhispererSupplementalContextLatency: 0, diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts index ed17c181ee5..a43720c81be 100644 --- a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts @@ -82,8 +82,6 @@ describe('codewhispererTracker', function () { describe('emitTelemetryOnSuggestion', function () { it('Should call recordCodewhispererUserModification with suggestion event', async function () { - const testStartUrl = 'testStartUrl' - sinon.stub(AuthUtil.instance, 'startUrl').value(testStartUrl) const suggestion = createAcceptedSuggestionEntry() const assertTelemetry = assertTelemetryCurried('codewhisperer_userModification') await CodeWhispererTracker.getTracker().emitTelemetryOnSuggestion(suggestion) @@ -95,7 +93,7 @@ describe('codewhispererTracker', function () { codewhispererModificationPercentage: 1, codewhispererCompletionType: 'Line', codewhispererLanguage: 'java', - credentialStartUrl: testStartUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, codewhispererCharactersAccepted: suggestion.originalString.length, codewhispererCharactersModified: 0, }) diff --git a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts index 500eaf23080..97e245fecd3 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts @@ -4,419 +4,364 @@ */ import assert from 'assert' -import { - AuthStates, - AuthUtil, - amazonQScopes, - codeWhispererChatScopes, - codeWhispererCoreScopes, -} from 'aws-core-vscode/codewhisperer' -import { - assertTelemetry, - getTestWindow, - SeverityLevel, - createBuilderIdProfile, - createSsoProfile, - createTestAuth, - captureEventNTimes, -} from 'aws-core-vscode/test' -import { Auth, Connection, isAnySsoConnection, isBuilderIdConnection } from 'aws-core-vscode/auth' -import { globals, vscodeComponent } from 'aws-core-vscode/shared' - -const enterpriseSsoStartUrl = 'https://enterprise.awsapps.com/start' +import * as sinon from 'sinon' +import * as path from 'path' +import { AuthUtil, amazonQScopes } from 'aws-core-vscode/codewhisperer' +import { createTestAuthUtil, TestFolder } from 'aws-core-vscode/test' +import { constants, cache } from 'aws-core-vscode/auth' +import { auth2 } from 'aws-core-vscode/auth' +import { mementoUtils, fs } from 'aws-core-vscode/shared' describe('AuthUtil', async function () { - let auth: ReturnType - let authUtil: AuthUtil + let auth: any beforeEach(async function () { - auth = createTestAuth(globals.globalState) - authUtil = new AuthUtil(auth) + await createTestAuthUtil() + auth = AuthUtil.instance }) afterEach(async function () { - await auth.logout() + sinon.restore() }) - it('if there is no valid AwsBuilderID conn, it will create one and use it', async function () { - getTestWindow().onDidShowQuickPick(async (picker) => { - await picker.untilReady() - picker.acceptItem(picker.items[1]) + describe('Auth state', function () { + it('login with BuilderId', async function () { + await auth.login(constants.builderIdStartUrl, constants.builderIdRegion) + assert.ok(auth.isConnected()) + assert.ok(auth.isBuilderIdConnection()) }) - await authUtil.connectToAwsBuilderId() - const conn = authUtil.conn - assert.strictEqual(conn?.type, 'sso') - assert.strictEqual(conn.label, 'AWS Builder ID') - assert.deepStrictEqual(conn.scopes, amazonQScopes) - }) + it('login with IDC', async function () { + await auth.login('https://example.awsapps.com/start', 'us-east-1') + assert.ok(auth.isConnected()) + assert.ok(auth.isIdcConnection()) + }) - it('if there IS an existing AwsBuilderID conn, it will upgrade the scopes and use it', async function () { - const existingBuilderId = await auth.createConnection( - createBuilderIdProfile({ scopes: codeWhispererCoreScopes }) - ) - getTestWindow().onDidShowQuickPick(async (picker) => { - await picker.untilReady() - picker.acceptItem(picker.items[1]) + it('identifies internal users', async function () { + await auth.login(constants.internalStartUrl, 'us-east-1') + assert.ok(auth.isInternalAmazonUser()) }) - await authUtil.connectToAwsBuilderId() + it('identifies SSO session', function () { + ;(auth as any).session = { loginType: auth2.LoginTypes.SSO } + assert.strictEqual(auth.isSsoSession(), true) + }) - const conn = authUtil.conn - assert.strictEqual(conn?.type, 'sso') - assert.strictEqual(conn.id, existingBuilderId.id) - assert.deepStrictEqual(conn.scopes, amazonQScopes) + it('identifies non-SSO session', function () { + ;(auth as any).session = { loginType: auth2.LoginTypes.IAM } + assert.strictEqual(auth.isSsoSession(), false) + }) }) - it('if there is no valid enterprise SSO conn, will create and use one', async function () { - getTestWindow().onDidShowQuickPick(async (picker) => { - await picker.untilReady() - picker.acceptItem(picker.items[1]) + describe('Token management', function () { + it('can get token when connected with SSO', async function () { + await auth.login(constants.builderIdStartUrl, constants.builderIdRegion) + const token = await auth.getToken() + assert.ok(token) }) - await authUtil.connectToEnterpriseSso(enterpriseSsoStartUrl, 'us-east-1') - const conn = authUtil.conn - assert.strictEqual(conn?.type, 'sso') - assert.strictEqual(conn.label, 'IAM Identity Center (enterprise)') + it('throws when getting token without SSO connection', async function () { + sinon.stub(AuthUtil.instance, 'isSsoSession').returns(false) + await assert.rejects(async () => await auth.getToken()) + }) }) - it('should add scopes + connect to existing IAM Identity Center connection', async function () { - getTestWindow().onDidShowMessage(async (message) => { - assert.ok(message.modal) - message.selectItem('Proceed') + describe('getTelemetryMetadata', function () { + it('returns valid metadata for BuilderId connection', async function () { + await auth.login(constants.builderIdStartUrl, constants.builderIdRegion) + const metadata = await auth.getTelemetryMetadata() + assert.strictEqual(metadata.credentialSourceId, 'awsId') + assert.strictEqual(metadata.credentialStartUrl, constants.builderIdStartUrl) + }) + + it('returns valid metadata for IDC connection', async function () { + await auth.login('https://example.awsapps.com/start', 'us-east-1') + const metadata = await auth.getTelemetryMetadata() + assert.strictEqual(metadata.credentialSourceId, 'iamIdentityCenter') + assert.strictEqual(metadata.credentialStartUrl, 'https://example.awsapps.com/start') }) - const randomScope = 'my:random:scope' - const ssoConn = await auth.createInvalidSsoConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: [randomScope] }) - ) - - // Method under test - await authUtil.connectToEnterpriseSso(ssoConn.startUrl, 'us-east-1') - - const cwConn = authUtil.conn - assert.strictEqual(cwConn?.type, 'sso') - assert.strictEqual(cwConn.label, 'IAM Identity Center (enterprise)') - assert.deepStrictEqual(cwConn.scopes, [randomScope, ...amazonQScopes]) - }) - it('reauthenticates an existing BUT invalid Amazon Q IAM Identity Center connection', async function () { - const ssoConn = await auth.createInvalidSsoConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: amazonQScopes }) - ) - await auth.refreshConnectionState(ssoConn) - assert.strictEqual(auth.getConnectionState(ssoConn), 'invalid') - - // Method under test - await authUtil.connectToEnterpriseSso(ssoConn.startUrl, 'us-east-1') - - const cwConn = authUtil.conn - assert.strictEqual(cwConn?.type, 'sso') - assert.strictEqual(cwConn.id, ssoConn.id) - assert.deepStrictEqual(cwConn.scopes, amazonQScopes) - assert.strictEqual(auth.getConnectionState(cwConn), 'valid') + it('returns undefined metadata when not connected', async function () { + await auth.logout() + const metadata = await auth.getTelemetryMetadata() + assert.strictEqual(metadata.id, 'undefined') + }) }) - it('should show reauthenticate prompt', async function () { - getTestWindow().onDidShowMessage((m) => { - if (m.severity === SeverityLevel.Information) { - m.close() - } + describe('getAuthFormIds', function () { + it('returns empty array when not connected', async function () { + await auth.logout() + const forms = await auth.getAuthFormIds() + assert.deepStrictEqual(forms, []) }) - await authUtil.showReauthenticatePrompt() + it('returns BuilderId forms when using BuilderId', async function () { + await auth.login(constants.builderIdStartUrl, constants.builderIdRegion) + const forms = await auth.getAuthFormIds() + assert.deepStrictEqual(forms, ['builderIdCodeWhisperer']) + }) - const warningMessage = getTestWindow().shownMessages.filter((m) => m.severity === SeverityLevel.Information) - assert.strictEqual(warningMessage.length, 1) - assert.strictEqual(warningMessage[0].message, `Your Amazon Q connection has expired. Please re-authenticate.`) - warningMessage[0].close() - assertTelemetry('toolkit_showNotification', { - id: 'codeWhispererConnectionExpired', - result: 'Succeeded', - source: vscodeComponent, + it('returns IDC forms when using IDC without SSO account access', async function () { + const session = (auth as any).session + sinon.stub(session, 'getProfile').resolves({ + ssoSession: { + settings: { + sso_registration_scopes: ['codewhisperer:*'], + }, + }, + }) + + await auth.login('https://example.awsapps.com/start', 'us-east-1') + const forms = await auth.getAuthFormIds() + assert.deepStrictEqual(forms, ['identityCenterCodeWhisperer']) }) - assertTelemetry('toolkit_invokeAction', { - id: 'codeWhispererConnectionExpired', - action: 'dismiss', - result: 'Succeeded', - source: vscodeComponent, + + it('returns IDC forms with explorer when using IDC with SSO account access', async function () { + const session = (auth as any).session + sinon.stub(session, 'getProfile').resolves({ + ssoSession: { + settings: { + sso_registration_scopes: ['codewhisperer:*', 'sso:account:access'], + }, + }, + }) + + await auth.login('https://example.awsapps.com/start', 'us-east-1') + const forms = await auth.getAuthFormIds() + assert.deepStrictEqual(forms.sort(), ['identityCenterCodeWhisperer', 'identityCenterExplorer'].sort()) + }) + + it('returns credentials form for IAM credentials', async function () { + sinon.stub(auth, 'isSsoSession').returns(false) + sinon.stub(auth, 'isConnected').returns(true) + + const forms = await auth.getAuthFormIds() + assert.deepStrictEqual(forms, ['credentials']) }) }) - it('reauthenticate prompt reauthenticates invalid connection', async function () { - const conn = await auth.createInvalidSsoConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: codeWhispererChatScopes }) - ) - await auth.useConnection(conn) - getTestWindow().onDidShowMessage((m) => { - m.selectItem('Re-authenticate') + describe('cacheChangedHandler', function () { + it('calls logout when event is delete', async function () { + const logoutSpy = sinon.spy(auth, 'logout') + + await (auth as any).cacheChangedHandler('delete') + + assert.ok(logoutSpy.calledOnce) }) - assert.strictEqual(auth.getConnectionState(conn), 'invalid') + it('calls restore when event is create', async function () { + const restoreSpy = sinon.spy(auth, 'restore') - await authUtil.showReauthenticatePrompt() + await (auth as any).cacheChangedHandler('create') - assert.strictEqual(authUtil.conn?.type, 'sso') - assert.strictEqual(auth.getConnectionState(conn), 'valid') - assertTelemetry('toolkit_showNotification', { - id: 'codeWhispererConnectionExpired', - result: 'Succeeded', - source: vscodeComponent, + assert.ok(restoreSpy.calledOnce) }) - assertTelemetry('toolkit_invokeAction', { - id: 'codeWhispererConnectionExpired', - action: 'connect', - result: 'Succeeded', - source: vscodeComponent, + + it('does nothing for other events', async function () { + const logoutSpy = sinon.spy(auth, 'logout') + const restoreSpy = sinon.spy(auth, 'restore') + + await (auth as any).cacheChangedHandler('unknown') + + assert.ok(logoutSpy.notCalled) + assert.ok(restoreSpy.notCalled) }) }) - it('reauthenticates Builder ID connection that already has all scopes', async function () { - const conn = await auth.createInvalidSsoConnection(createBuilderIdProfile({ scopes: amazonQScopes })) - await auth.useConnection(conn) + describe('stateChangeHandler', function () { + let mockLspAuth: any + let regionProfileManager: any - // method under test - await authUtil.reauthenticate() + beforeEach(function () { + mockLspAuth = (auth as any).lspAuth + regionProfileManager = (auth as any).regionProfileManager + }) - assert.strictEqual(authUtil.conn?.type, 'sso') - assert.deepStrictEqual(authUtil.conn?.scopes, amazonQScopes) - assert.strictEqual(auth.getConnectionState(conn), 'valid') - }) + it('updates bearer token when state is refreshed', async function () { + await auth.login(constants.builderIdStartUrl, 'us-east-1') - it('reauthenticates IdC connection that already has all scopes', async function () { - const conn = await auth.createInvalidSsoConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: codeWhispererCoreScopes }) - ) - await auth.useConnection(conn) + await (auth as any).stateChangeHandler({ state: 'refreshed' }) - // method under test - await authUtil.reauthenticate() + assert.ok(mockLspAuth.updateBearerToken.called) + assert.strictEqual(mockLspAuth.updateBearerToken.firstCall.args[0].data, 'fake-data') + }) - assert.strictEqual(authUtil.conn?.type, 'sso') - assert.deepStrictEqual(authUtil.conn?.scopes, amazonQScopes) - assert.strictEqual(auth.getConnectionState(conn), 'valid') - }) + it('cleans up when connection expires', async function () { + await auth.login(constants.builderIdStartUrl, 'us-east-1') - it('reauthenticate adds missing Builder ID scopes', async function () { - const conn = await auth.createInvalidSsoConnection(createBuilderIdProfile({ scopes: codeWhispererCoreScopes })) - await auth.useConnection(conn) + await (auth as any).stateChangeHandler({ state: 'expired' }) - // method under test - await authUtil.reauthenticate() + assert.ok(mockLspAuth.deleteBearerToken.called) + }) - assert.strictEqual(authUtil.conn?.type, 'sso') - assert.deepStrictEqual(authUtil.conn?.scopes, amazonQScopes) - assert.strictEqual(auth.getConnectionState(conn), 'valid') - }) + it('deletes bearer token when disconnected', async function () { + await (auth as any).stateChangeHandler({ state: 'notConnected' }) - it('reauthenticate adds missing Amazon Q IdC scopes', async function () { - const conn = await auth.createInvalidSsoConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: codeWhispererCoreScopes }) - ) - await auth.useConnection(conn) + assert.ok(mockLspAuth.deleteBearerToken.called) + }) - // method under test - await authUtil.reauthenticate() + it('updates bearer token and restores profile on reconnection', async function () { + const restoreProfileSelectionSpy = sinon.spy(regionProfileManager, 'restoreProfileSelection') - assert.strictEqual(authUtil.conn?.type, 'sso') - assert.deepStrictEqual(authUtil.conn?.scopes, amazonQScopes) - assert.strictEqual(auth.getConnectionState(conn), 'valid') - }) + await auth.login('https://example.awsapps.com/start', 'us-east-1') - it('CodeWhisperer uses fallback connection when switching to an unsupported connection', async function () { - const supportedConn = await auth.createConnection(createBuilderIdProfile({ scopes: codeWhispererChatScopes })) - const unsupportedConn = await auth.createConnection(createSsoProfile()) - - await auth.useConnection(supportedConn) - assert.ok(authUtil.isConnected()) - assert.strictEqual(auth.activeConnection?.id, authUtil.conn?.id) - - // Switch to unsupported connection - const cwAuthUpdatedConnection = captureEventNTimes(authUtil.secondaryAuth.onDidChangeActiveConnection, 2) - await auth.useConnection(unsupportedConn) - // - This is triggered when the main Auth connection is switched - // - This is triggered by registerAuthListener() when it saves the previous active connection as a fallback. - await cwAuthUpdatedConnection - - // TODO in a refactor see if we can simplify multiple multiple triggers on the same event. - assert.ok(authUtil.isConnected()) - assert.ok(authUtil.isUsingSavedConnection) - assert.notStrictEqual(auth.activeConnection?.id, authUtil.conn?.id) - assert.strictEqual(authUtil.conn?.type, 'sso') - assert.deepStrictEqual(authUtil.conn?.scopes, codeWhispererChatScopes) - }) + await (auth as any).stateChangeHandler({ state: 'connected' }) - it('does not prompt to sign out of duplicate builder ID connections', async function () { - await authUtil.connectToAwsBuilderId() - await authUtil.connectToAwsBuilderId() - assert.ok(authUtil.isConnected()) + assert.ok(mockLspAuth.updateBearerToken.called) + assert.ok(restoreProfileSelectionSpy.called) + }) - const ssoConnectionIds = new Set(auth.activeConnectionEvents.emits.filter(isAnySsoConnection).map((c) => c.id)) - assert.strictEqual(ssoConnectionIds.size, 1, 'Expected exactly 1 unique SSO connection id') - assert.strictEqual((await auth.listConnections()).filter(isAnySsoConnection).length, 1) - }) + it('clears region profile cache and invalidates profile on IDC connection expiration', async function () { + const invalidateProfileSpy = sinon.spy(regionProfileManager, 'invalidateProfile') + const clearCacheSpy = sinon.spy(regionProfileManager, 'clearCache') - it('automatically upgrades connections if they do not have the required scopes', async function () { - const upgradeableConn = await auth.createConnection(createBuilderIdProfile()) - await auth.useConnection(upgradeableConn) - assert.strictEqual(authUtil.isConnected(), false) - - await authUtil.connectToAwsBuilderId() - assert.ok(authUtil.isConnected()) - assert.ok(authUtil.isConnectionValid()) - assert.ok(isBuilderIdConnection(authUtil.conn)) - assert.strictEqual(authUtil.conn?.id, upgradeableConn.id) - assert.strictEqual(authUtil.conn.startUrl, upgradeableConn.startUrl) - assert.strictEqual(authUtil.conn.ssoRegion, upgradeableConn.ssoRegion) - assert.deepStrictEqual(authUtil.conn.scopes, amazonQScopes) - assert.strictEqual((await auth.listConnections()).filter(isAnySsoConnection).length, 1) - }) + await auth.login('https://example.awsapps.com/start', 'us-east-1') - it('test reformatStartUrl should remove trailing slash and hash', function () { - const expected = 'https://view.awsapps.com/start' - assert.strictEqual(authUtil.reformatStartUrl(expected + '/'), expected) - assert.strictEqual(authUtil.reformatStartUrl(undefined), undefined) - assert.strictEqual(authUtil.reformatStartUrl(expected + '/#'), expected) - assert.strictEqual(authUtil.reformatStartUrl(expected + '#/'), expected) - assert.strictEqual(authUtil.reformatStartUrl(expected + '/#/'), expected) - assert.strictEqual(authUtil.reformatStartUrl(expected + '####'), expected) - }) + await (auth as any).stateChangeHandler({ state: 'expired' }) - it(`clearExtraConnections()`, async function () { - const conn1 = await auth.createConnection(createBuilderIdProfile()) - const conn2 = await auth.createConnection(createSsoProfile({ startUrl: enterpriseSsoStartUrl })) - const conn3 = await auth.createConnection(createSsoProfile({ startUrl: enterpriseSsoStartUrl + 1 })) - // validate listConnections shows all connections - assert.deepStrictEqual( - (await authUtil.auth.listConnections()).map((conn) => conn.id).sort((a, b) => a.localeCompare(b)), - [conn1, conn2, conn3].map((conn) => conn.id).sort((a, b) => a.localeCompare(b)) - ) - await authUtil.secondaryAuth.useNewConnection(conn3) - - await authUtil.clearExtraConnections() // method under test - - // Only the conn that AuthUtil is using is remaining - assert.deepStrictEqual( - (await authUtil.auth.listConnections()).map((conn) => conn.id), - [conn3.id] - ) + assert.ok(invalidateProfileSpy.called) + assert.ok(clearCacheSpy.called) + }) }) -}) -describe('getChatAuthState()', function () { - let auth: ReturnType - let authUtil: AuthUtil - let laterDate: Date + describe('migrateSsoConnectionToLsp', function () { + let memento: any + let cacheDir: string + let fromRegistrationFile: string + let fromTokenFile: string + + const validProfile = { + type: 'sso', + startUrl: 'https://test2.com', + ssoRegion: 'us-east-1', + scopes: amazonQScopes, + metadata: { + connectionState: 'valid', + }, + } + + beforeEach(async function () { + memento = { + get: sinon.stub(), + update: sinon.stub().resolves(), + } + cacheDir = (await TestFolder.create()).path - beforeEach(async function () { - auth = createTestAuth(globals.globalState) - authUtil = new AuthUtil(auth) + sinon.stub(mementoUtils, 'getEnvironmentSpecificMemento').returns(memento) + sinon.stub(cache, 'getCacheDir').returns(cacheDir) - laterDate = new Date(Date.now() + 10_000_000) - }) + fromTokenFile = cache.getTokenCacheFile(cacheDir, 'profile1') + const registrationKey = { + startUrl: validProfile.startUrl, + region: validProfile.ssoRegion, + scopes: amazonQScopes, + } + fromRegistrationFile = cache.getRegistrationCacheFile(cacheDir, registrationKey) - afterEach(async function () { - await auth.logout() - }) + const registrationData = { test: 'registration' } + const tokenData = { test: 'token' } - it('indicates nothing connected when no auth connection exists', async function () { - const result = await authUtil.getChatAuthState() - assert.deepStrictEqual(result, { - codewhispererChat: AuthStates.disconnected, - codewhispererCore: AuthStates.disconnected, - amazonQ: AuthStates.disconnected, + await fs.writeFile(fromRegistrationFile, JSON.stringify(registrationData)) + await fs.writeFile(fromTokenFile, JSON.stringify(tokenData)) }) - }) - /** Affects {@link Auth.refreshConnectionState} */ - function createToken(conn: Connection) { - auth.getTestTokenProvider(conn).getToken.resolves({ accessToken: 'myAccessToken', expiresAt: laterDate }) - } - - describe('Builder ID', function () { - it('indicates only CodeWhisperer core is connected when only CW core scopes are set', async function () { - const conn = await auth.createConnection(createBuilderIdProfile({ scopes: codeWhispererCoreScopes })) - createToken(conn) - await auth.useConnection(conn) - - const result = await authUtil.getChatAuthState() - assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.connected, - codewhispererChat: AuthStates.expired, - amazonQ: AuthStates.expired, - }) + afterEach(async function () { + sinon.restore() }) - it('indicates all SUPPORTED features connected when all scopes are set', async function () { - const conn = await auth.createConnection(createBuilderIdProfile({ scopes: amazonQScopes })) - createToken(conn) - await auth.useConnection(conn) + it('migrates valid SSO connection', async function () { + memento.get.returns({ profile1: validProfile }) - const result = await authUtil.getChatAuthState() - assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.connected, - codewhispererChat: AuthStates.connected, - amazonQ: AuthStates.connected, - }) - }) + const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves() - it('indicates all SUPPORTED features expired when connection is invalid', async function () { - const conn = await auth.createInvalidSsoConnection( - createBuilderIdProfile({ scopes: codeWhispererChatScopes }) - ) - await auth.useConnection(conn) + await auth.migrateSsoConnectionToLsp('test-client') - const result = await authUtil.getChatAuthState() - assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.expired, - codewhispererChat: AuthStates.expired, - amazonQ: AuthStates.expired, - }) + assert.ok(updateProfileStub.calledOnce) + assert.ok(memento.update.calledWith('auth.profiles', undefined)) + + const files = await fs.readdir(cacheDir) + assert.strictEqual(files.length, 2) // Should have both the token and registration file + + // Verify file contents were preserved + const newFiles = files.map((f) => path.join(cacheDir, f[0])) + for (const file of newFiles) { + const content = await fs.readFileText(file) + const parsed = JSON.parse(content) + assert.ok(parsed.test === 'registration' || parsed.test === 'token') + } }) - }) - describe('Identity Center', function () { - it('indicates only CW core is connected when only CW core scopes are set', async function () { - const conn = await auth.createConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: codeWhispererCoreScopes }) - ) - createToken(conn) - await auth.useConnection(conn) - - const result = await authUtil.getChatAuthState() - assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.pendingProfileSelection, - codewhispererChat: AuthStates.expired, - amazonQ: AuthStates.expired, - }) + it('does not migrate if no matching SSO profile exists', async function () { + const mockProfiles = { + 'test-profile': { + type: 'iam', + startUrl: 'https://test.com', + ssoRegion: 'us-east-1', + }, + } + memento.get.returns(mockProfiles) + + await auth.migrateSsoConnectionToLsp('test-client') + + // Assert that the file names have not updated + const files = await fs.readdir(cacheDir) + assert.ok(files.length === 2) + assert.ok(await fs.exists(fromRegistrationFile)) + assert.ok(await fs.exists(fromTokenFile)) + assert.ok(!memento.update.called) }) - it('indicates all features connected when all scopes are set', async function () { - const conn = await auth.createConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: amazonQScopes }) - ) - createToken(conn) - await auth.useConnection(conn) - - const result = await authUtil.getChatAuthState() - assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.pendingProfileSelection, - codewhispererChat: AuthStates.pendingProfileSelection, - amazonQ: AuthStates.pendingProfileSelection, + it('migrates only profile with matching scopes', async function () { + const mockProfiles = { + profile1: validProfile, + profile2: { + type: 'sso', + startUrl: 'https://test.com', + ssoRegion: 'us-east-1', + scopes: ['different:scope'], + metadata: { + connectionState: 'valid', + }, + }, + } + memento.get.returns(mockProfiles) + + const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves() + + await auth.migrateSsoConnectionToLsp('test-client') + + assert.ok(updateProfileStub.calledOnce) + assert.ok(memento.update.calledWith('auth.profiles', undefined)) + assert.deepStrictEqual(updateProfileStub.firstCall.args[0], { + startUrl: validProfile.startUrl, + region: validProfile.ssoRegion, + scopes: validProfile.scopes, }) }) - it('indicates all features expired when connection is invalid', async function () { - const conn = await auth.createInvalidSsoConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: amazonQScopes }) - ) - await auth.useConnection(conn) + it('uses valid connection state when multiple profiles exist', async function () { + const mockProfiles = { + profile2: { + ...validProfile, + metadata: { + connectionState: 'invalid', + }, + }, + profile1: validProfile, + } + memento.get.returns(mockProfiles) - const result = await authUtil.getChatAuthState() - assert.deepStrictEqual(result, { - codewhispererCore: AuthStates.expired, - codewhispererChat: AuthStates.expired, - amazonQ: AuthStates.expired, - }) + const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves() + + await auth.migrateSsoConnectionToLsp('test-client') + + assert.ok( + updateProfileStub.calledWith({ + startUrl: validProfile.startUrl, + region: validProfile.ssoRegion, + scopes: validProfile.scopes, + }) + ) }) }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/util/showSsoPrompt.test.ts b/packages/amazonq/test/unit/codewhisperer/util/showSsoPrompt.test.ts index 7d004e8ede5..1d67db60efc 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/showSsoPrompt.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/showSsoPrompt.test.ts @@ -7,12 +7,20 @@ import * as vscode from 'vscode' import assert from 'assert' import * as sinon from 'sinon' import { resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' -import { assertTelemetryCurried, getTestWindow, getTestLogger } from 'aws-core-vscode/test' +import { assertTelemetryCurried, getTestWindow } from 'aws-core-vscode/test' import { AuthUtil, awsIdSignIn, showCodeWhispererConnectionPrompt } from 'aws-core-vscode/codewhisperer' +import { SsoAccessTokenProvider, constants } from 'aws-core-vscode/auth' describe('showConnectionPrompt', function () { + let isBuilderIdConnection: sinon.SinonStub + beforeEach(async function () { await resetCodeWhispererGlobalVariables() + isBuilderIdConnection = sinon.stub(AuthUtil.instance, 'isBuilderIdConnection') + isBuilderIdConnection.resolves() + + // Stub useDeviceFlow so we always use DeviceFlow for auth + sinon.stub(SsoAccessTokenProvider, 'useDeviceFlow').returns(true) }) afterEach(function () { @@ -20,7 +28,7 @@ describe('showConnectionPrompt', function () { }) it('can select connect to AwsBuilderId', async function () { - const authUtilSpy = sinon.stub(AuthUtil.instance, 'connectToAwsBuilderId') + sinon.stub(AuthUtil.instance, 'login').resolves() getTestWindow().onDidShowQuickPick(async (picker) => { await picker.untilReady() @@ -29,18 +37,18 @@ describe('showConnectionPrompt', function () { await showCodeWhispererConnectionPrompt() - assert.ok(authUtilSpy.called) const assertTelemetry = assertTelemetryCurried('ui_click') assertTelemetry({ elementId: 'connection_optionBuilderID' }) + assert.ok(isBuilderIdConnection) }) - it('connectToAwsBuilderId logs that AWS ID sign in was selected', async function () { - sinon.stub(AuthUtil.instance, 'connectToAwsBuilderId').resolves() + it('connectToAwsBuilderId calls AuthUtil login with builderIdStartUrl', async function () { sinon.stub(vscode.commands, 'executeCommand') + const loginStub = sinon.stub(AuthUtil.instance, 'login').resolves() await awsIdSignIn() - const loggedEntries = getTestLogger().getLoggedEntries() - assert.ok(loggedEntries.find((entry) => entry === 'selected AWS ID sign in')) + assert.strictEqual(loginStub.called, true) + assert.strictEqual(loginStub.firstCall.args[0], constants.builderIdStartUrl) }) }) diff --git a/packages/core/src/amazonq/commons/connector/baseMessenger.ts b/packages/core/src/amazonq/commons/connector/baseMessenger.ts index c26834c6fff..ea368965ea1 100644 --- a/packages/core/src/amazonq/commons/connector/baseMessenger.ts +++ b/packages/core/src/amazonq/commons/connector/baseMessenger.ts @@ -26,7 +26,7 @@ import { } from './connectorMessages' import { DeletedFileInfo, FollowUpTypes, NewFileInfo } from '../types' import { messageWithConversationId } from '../../../amazonqFeatureDev/userFacingText' -import { FeatureAuthState } from '../../../codewhisperer/util/authUtil' +import { auth2 } from 'aws-core-vscode/auth' export class Messenger { public constructor( @@ -191,19 +191,15 @@ export class Messenger { ) } - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { + public async sendAuthNeededExceptionMessage(credentialState: auth2.AuthState, tabID: string) { let authType: AuthFollowUpType = 'full-auth' let message = AuthMessageDataMap[authType].message - switch (credentialState.amazonQ) { - case 'disconnected': + switch (credentialState) { + case 'notConnected': authType = 'full-auth' message = AuthMessageDataMap[authType].message break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break case 'expired': authType = 're-auth' message = AuthMessageDataMap[authType].message diff --git a/packages/core/src/amazonq/explorer/amazonQTreeNode.ts b/packages/core/src/amazonq/explorer/amazonQTreeNode.ts index bbfd1bc1ff2..9b28fa80fe3 100644 --- a/packages/core/src/amazonq/explorer/amazonQTreeNode.ts +++ b/packages/core/src/amazonq/explorer/amazonQTreeNode.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode' import { ResourceTreeDataProvider, TreeNode } from '../../shared/treeview/resourceTreeDataProvider' -import { AuthState } from '../../codewhisperer/util/authUtil' import { createLearnMoreNode, createInstallQNode, createDismissNode } from './amazonQChildrenNodes' import { Commands } from '../../shared/vscode/commands2' +import { auth2 } from 'aws-core-vscode/auth' export class AmazonQNode implements TreeNode { public readonly id = 'amazonq' @@ -19,7 +19,7 @@ export class AmazonQNode implements TreeNode { private readonly onDidChangeVisibilityEmitter = new vscode.EventEmitter() public readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event - public static amazonQState: AuthState + public static amazonQState: auth2.AuthState private constructor() {} diff --git a/packages/core/src/amazonq/extApi.ts b/packages/core/src/amazonq/extApi.ts index 2eb16e4cde2..af98b73e59a 100644 --- a/packages/core/src/amazonq/extApi.ts +++ b/packages/core/src/amazonq/extApi.ts @@ -7,9 +7,14 @@ import vscode from 'vscode' import { VSCODE_EXTENSION_ID } from '../shared/extensions' import { SendMessageCommandOutput, SendMessageRequest } from '@amzn/amazon-q-developer-streaming-client' import { GenerateAssistantResponseCommandOutput, GenerateAssistantResponseRequest } from '@amzn/codewhisperer-streaming' -import { FeatureAuthState } from '../codewhisperer/util/authUtil' +import { auth2 } from 'aws-core-vscode/auth' import { ToolkitError } from '../shared/errors' +/** + * @deprecated, for backwards comaptibility only. + */ +type OldAuthState = 'disconnected' | 'expired' | 'connected' + /** * This interface is used and exported by the amazon q extension. If you make a change here then * update the corresponding api implementation in packages/amazonq/src/api.ts @@ -21,7 +26,15 @@ export interface api { } authApi: { reauthIfNeeded(): Promise - getChatAuthState(): Promise + /** + * @deprecated, for backwards comaptibility only. + */ + getChatAuthState(): Promise<{ + codewhispererCore: OldAuthState + codewhispererChat: OldAuthState + amazonQ: OldAuthState + }> + getAuthState(): auth2.AuthState } } diff --git a/packages/core/src/amazonq/session/sessionState.ts b/packages/core/src/amazonq/session/sessionState.ts index 1f206c23159..10e7eb67fdd 100644 --- a/packages/core/src/amazonq/session/sessionState.ts +++ b/packages/core/src/amazonq/session/sessionState.ts @@ -238,7 +238,7 @@ export abstract class BasePrepareCodeGenState implements SessionState { const uploadId = await telemetry.amazonq_createUpload.run(async (span) => { span.record({ amazonqConversationId: this.config.conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) const { zipFileBuffer, zipFileChecksum } = await this.prepareProjectZip( this.config.workspaceRoots, @@ -357,7 +357,7 @@ export abstract class BaseCodeGenState extends CodeGenBase implements SessionSta span.record({ amazonqConversationId: this.config.conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) action.telemetry.setGenerateCodeIteration(this.currentIteration) diff --git a/packages/core/src/amazonq/util/authUtils.ts b/packages/core/src/amazonq/util/authUtils.ts index e310dbe9823..f3e48a22cc0 100644 --- a/packages/core/src/amazonq/util/authUtils.ts +++ b/packages/core/src/amazonq/util/authUtils.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { FeatureAuthState } from '../../codewhisperer/util/authUtil' +import { AuthState } from '../../auth/auth2' import { AuthFollowUpType, AuthMessageDataMap } from '../auth/model' /** @@ -15,20 +15,13 @@ import { AuthFollowUpType, AuthMessageDataMap } from '../auth/model' * - authType: The type of authentication follow-up required (AuthFollowUpType) * - message: The corresponding message for the determined auth type */ -export function extractAuthFollowUp(credentialState: FeatureAuthState) { +export function extractAuthFollowUp(credentialState: AuthState) { let authType: AuthFollowUpType = 'full-auth' let message = AuthMessageDataMap[authType].message - if (credentialState.codewhispererChat === 'disconnected' && credentialState.codewhispererCore === 'disconnected') { + if (credentialState === 'notConnected') { authType = 'full-auth' message = AuthMessageDataMap[authType].message - } - - if (credentialState.codewhispererCore === 'connected' && credentialState.codewhispererChat === 'expired') { - authType = 'missing_scopes' - message = AuthMessageDataMap[authType].message - } - - if (credentialState.codewhispererChat === 'expired' && credentialState.codewhispererCore === 'expired') { + } else if (credentialState === 'expired') { authType = 're-auth' message = AuthMessageDataMap[authType].message } diff --git a/packages/core/src/amazonq/util/files.ts b/packages/core/src/amazonq/util/files.ts index afa0b674928..fce56e37a08 100644 --- a/packages/core/src/amazonq/util/files.ts +++ b/packages/core/src/amazonq/util/files.ts @@ -278,7 +278,7 @@ export function registerNewFiles( telemetry.toolkit_trackScenario.emit({ count: 1, amazonqConversationId: conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, scenario: 'wsOrphanedDocuments', }) getLogger().error(`No workspace folder found for file: ${zipFilePath} and prefix: ${prefix}`) diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts index bc637a8f6c6..ddbee4f0e9b 100644 --- a/packages/core/src/amazonq/webview/generators/webViewContent.ts +++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts @@ -70,7 +70,7 @@ export class WebViewContentGenerator { : AuthUtil.instance.regionProfileManager.activeRegionProfile const regionProfileString: string = JSON.stringify(regionProfile) - const authState = (await AuthUtil.instance.getChatAuthState()).amazonQ + const authState = AuthUtil.instance.getAuthState() return ` diff --git a/packages/core/src/amazonqDoc/app.ts b/packages/core/src/amazonqDoc/app.ts index 929cf1d45de..d348583a798 100644 --- a/packages/core/src/amazonqDoc/app.ts +++ b/packages/core/src/amazonqDoc/app.ts @@ -81,7 +81,7 @@ export function init(appContext: AmazonQAppInitContext) { appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(docChatUIInputEventEmitter), 'doc') const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' + const authenticated = AuthUtil.instance.getAuthState() === 'connected' let authenticatingSessionIDs: string[] = [] if (authenticated) { const authenticatingSessions = sessionStorage.getAuthenticatingSessions() @@ -97,9 +97,10 @@ export function init(appContext: AmazonQAppInitContext) { messenger.sendAuthenticationUpdate(authenticated, authenticatingSessionIDs) }, 500) - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { + AuthUtil.instance.onDidChangeConnectionState(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { return debouncedEvent() }) diff --git a/packages/core/src/amazonqDoc/controllers/chat/controller.ts b/packages/core/src/amazonqDoc/controllers/chat/controller.ts index ab6045e75ce..390a959b937 100644 --- a/packages/core/src/amazonqDoc/controllers/chat/controller.ts +++ b/packages/core/src/amazonqDoc/controllers/chat/controller.ts @@ -232,9 +232,8 @@ export class DocController { const workspaceFolderName = vscode.workspace.workspaceFolders?.[0].name || '' - const authState = await AuthUtil.instance.getChatAuthState() - - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { await this.messenger.sendAuthNeededExceptionMessage(authState, data.tabID) session.isAuthenticating = true return @@ -465,8 +464,8 @@ export class DocController { try { getLogger().debug(`${featureName}: Processing message: ${message.message}`) - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) session.isAuthenticating = true return @@ -501,8 +500,8 @@ export class DocController { docGenerationTask.folderPath = '' docGenerationTask.mode = Mode.NONE - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) session.isAuthenticating = true return diff --git a/packages/core/src/amazonqDoc/session/session.ts b/packages/core/src/amazonqDoc/session/session.ts index e3eb29d6d32..8cfb5283626 100644 --- a/packages/core/src/amazonqDoc/session/session.ts +++ b/packages/core/src/amazonqDoc/session/session.ts @@ -91,7 +91,10 @@ export class Session { this._conversationId = await this.proxyClient.createConversation() getLogger().info(logWithConversationId(this.conversationId)) - span.record({ amazonqConversationId: this._conversationId, credentialStartUrl: AuthUtil.instance.startUrl }) + span.record({ + amazonqConversationId: this._conversationId, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, + }) }) this._state = new DocPrepareCodeGenState( diff --git a/packages/core/src/amazonqFeatureDev/app.ts b/packages/core/src/amazonqFeatureDev/app.ts index a016d2ba481..a3130ca4f74 100644 --- a/packages/core/src/amazonqFeatureDev/app.ts +++ b/packages/core/src/amazonqFeatureDev/app.ts @@ -87,7 +87,7 @@ export function init(appContext: AmazonQAppInitContext) { ) const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' + const authenticated = AuthUtil.instance.getAuthState() === 'connected' let authenticatingSessionIDs: string[] = [] if (authenticated) { const authenticatingSessions = sessionStorage.getAuthenticatingSessions() @@ -103,9 +103,10 @@ export function init(appContext: AmazonQAppInitContext) { messenger.sendAuthenticationUpdate(authenticated, authenticatingSessionIDs) }, 500) - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { + AuthUtil.instance.onDidChangeConnectionState(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { return debouncedEvent() }) diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts index 62e870b51fe..419f6969cc6 100644 --- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts +++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts @@ -51,7 +51,7 @@ const writeAPIRetryOptions = { // Create a client for featureDev proxy client based off of aws sdk v2 export async function createFeatureDevProxyClient(options?: Partial): Promise { - const bearerToken = await AuthUtil.instance.getBearerToken() + const bearerToken = await AuthUtil.instance.getToken() const cwsprConfig = getCodewhispererConfig() return (await globals.sdkClientBuilder.createAwsService( Service, diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index bdf73eada07..4843b3d6793 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -216,14 +216,14 @@ export class FeatureDevController { amazonqConversationId: session?.conversationId, value: 1, result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) } else if (vote === 'downvote') { telemetry.amazonq_codeGenerationThumbsDown.emit({ amazonqConversationId: session?.conversationId, value: 1, result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) } } @@ -395,8 +395,8 @@ export class FeatureDevController { session.latestMessage = message.message await session.disableFileList() - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) session.isAuthenticating = true return @@ -717,7 +717,7 @@ export class FeatureDevController { amazonqConversationId: session.conversationId, enabled: true, result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) // Unblock the message button this.messenger.sendAsyncEventProgress(message.tabID, false, undefined) @@ -828,7 +828,7 @@ export class FeatureDevController { } telemetry.amazonq_modifySourceFolder.emit({ - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, amazonqConversationId: session.conversationId, ...metricData, }) @@ -920,7 +920,7 @@ export class FeatureDevController { amazonqConversationId: session.conversationId, enabled: true, result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) const workspacePrefixMapping = getWorkspaceFoldersByPrefixes(session.config.workspaceFolders) @@ -970,8 +970,8 @@ export class FeatureDevController { session = await this.sessionStorage.getSession(message.tabID) getLogger().debug(`${featureName}: Session created with id: ${session.tabID}`) - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) session.isAuthenticating = true return @@ -1078,7 +1078,7 @@ export class FeatureDevController { if (amazonqNumberOfFilesAccepted > 0 && !session.acceptCodeTelemetrySent) { session.updateAcceptCodeTelemetrySent(true) telemetry.amazonq_isAcceptedCodeChanges.emit({ - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, amazonqConversationId: session.conversationId, amazonqNumberOfFilesAccepted, enabled: true, diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts index c1fc81a4701..f89db0aa9e7 100644 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ b/packages/core/src/amazonqFeatureDev/session/session.ts @@ -92,7 +92,10 @@ export class Session { this._conversationId = await this.proxyClient.createConversation() getLogger().info(logWithConversationId(this.conversationId)) - span.record({ amazonqConversationId: this._conversationId, credentialStartUrl: AuthUtil.instance.startUrl }) + span.record({ + amazonqConversationId: this._conversationId, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, + }) }) this._state = new FeatureDevPrepareCodeGenState( diff --git a/packages/core/src/amazonqGumby/app.ts b/packages/core/src/amazonqGumby/app.ts index 21182b38155..022d2f58f30 100644 --- a/packages/core/src/amazonqGumby/app.ts +++ b/packages/core/src/amazonqGumby/app.ts @@ -49,7 +49,7 @@ export function init(appContext: AmazonQAppInitContext) { appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(gumbyChatUIInputEventEmitter), 'gumby') const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' + const authenticated = AuthUtil.instance.getAuthState() === 'connected' let authenticatingSessionID = '' if (authenticated) { @@ -64,7 +64,7 @@ export function init(appContext: AmazonQAppInitContext) { messenger.sendAuthenticationUpdate(authenticated, [authenticatingSessionID]) }, 500) - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { + AuthUtil.instance.onDidChangeConnectionState(() => { return debouncedEvent() }) AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index af3f462bf95..188418eb8bb 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -159,8 +159,8 @@ export class GumbyController { try { getLogger().debug(`${featureName}: Session created with id: ${session.tabID}`) - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) session.isAuthenticating = true return @@ -259,11 +259,11 @@ export class GumbyController { credentialSourceId: authType, }) - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { this.sessionStorage.getSession().isAuthenticating = true await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - throw new AuthError('Not connected to Amazon Q', `AuthState=${authState.amazonQ}`) + throw new AuthError('Not connected to Amazon Q', `AuthState=${authState}`) } this.messenger.sendTransformationIntroduction(message.tabID) }) @@ -565,8 +565,8 @@ export class GumbyController { this.messenger.sendCompilationFinished(tabID) // since compilation can potentially take a long time, double check auth - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) this.sessionStorage.getSession().isAuthenticating = true return diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 30324bab06f..b12be675406 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -11,7 +11,7 @@ import vscode from 'vscode' import { AuthFollowUpType, AuthMessageDataMap } from '../../../../amazonq/auth/model' import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../../codewhisperer/models/model' -import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' +import { AuthState } from '../../../../auth/auth2' import * as CodeWhispererConstants from '../../../../codewhisperer/models/constants' import { AppToWebViewMessageDispatcher, @@ -92,19 +92,15 @@ export class Messenger { this.dispatcher.sendUpdatePlaceholder(new UpdatePlaceholderMessage(tabID, newPlaceholder)) } - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { + public async sendAuthNeededExceptionMessage(credentialState: AuthState, tabID: string) { let authType: AuthFollowUpType = 'full-auth' let message = AuthMessageDataMap[authType].message - switch (credentialState.amazonQ) { - case 'disconnected': + switch (credentialState) { + case 'notConnected': authType = 'full-auth' message = AuthMessageDataMap[authType].message break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break case 'expired': authType = 're-auth' message = AuthMessageDataMap[authType].message diff --git a/packages/core/src/amazonqTest/app.ts b/packages/core/src/amazonqTest/app.ts index 6c638c13b71..1988ceb1d17 100644 --- a/packages/core/src/amazonqTest/app.ts +++ b/packages/core/src/amazonqTest/app.ts @@ -50,7 +50,7 @@ export function init(appContext: AmazonQAppInitContext) { appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(testChatUIInputEventEmitter), 'testgen') const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' + const authenticated = AuthUtil.instance.getAuthState() === 'connected' let authenticatingSessionID = '' if (authenticated) { @@ -65,7 +65,7 @@ export function init(appContext: AmazonQAppInitContext) { messenger.sendAuthenticationUpdate(authenticated, [authenticatingSessionID]) }, 500) - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { + AuthUtil.instance.onDidChangeConnectionState(() => { return debouncedEvent() }) AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts index 747cca57e8e..286ce8644ff 100644 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ b/packages/core/src/amazonqTest/chat/controller/controller.ts @@ -221,8 +221,8 @@ export class TestController { try { logger.debug(`Q - Test: Session created with id: ${session.tabID}`) - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) session.isAuthenticating = true return @@ -239,7 +239,7 @@ export class TestController { telemetry.amazonq_feedback.emit({ featureId: 'amazonQTest', amazonqConversationId: session.startTestGenerationRequestId, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, interactionType: message.vote, }) } @@ -487,8 +487,8 @@ export class TestController { userPrompt = message.prompt.slice(0, maxUserPromptLength) // check that the session is authenticated - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { + const authState = AuthUtil.instance.getAuthState() + if (authState !== 'connected') { void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) session.isAuthenticating = true return diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts index 5541ef389c5..17e88c2621b 100644 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts @@ -9,7 +9,6 @@ */ import { AuthFollowUpType, AuthMessageDataMap } from '../../../../amazonq/auth/model' -import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' import { AppToWebViewMessageDispatcher, AuthNeededException, @@ -39,6 +38,7 @@ import { keys } from '../../../../shared/utilities/tsUtils' import { cancellingProgressField, testGenCompletedField } from '../../../models/constants' import { testGenState } from '../../../../codewhisperer/models/model' import { TelemetryHelper } from '../../../../codewhisperer/util/telemetryHelper' +import { AuthState } from '../../../../auth/auth2' export type UnrecoverableErrorType = 'no-project-found' | 'no-open-file-found' | 'invalid-file-type' @@ -122,19 +122,15 @@ export class Messenger { this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, progressField)) } - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { + public async sendAuthNeededExceptionMessage(credentialState: AuthState, tabID: string) { let authType: AuthFollowUpType = 'full-auth' let message = AuthMessageDataMap[authType].message - switch (credentialState.amazonQ) { - case 'disconnected': + switch (credentialState) { + case 'notConnected': authType = 'full-auth' message = AuthMessageDataMap[authType].message break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break case 'expired': authType = 're-auth' message = AuthMessageDataMap[authType].message diff --git a/packages/core/src/auth/auth2.ts b/packages/core/src/auth/auth2.ts new file mode 100644 index 00000000000..bff664b7e7b --- /dev/null +++ b/packages/core/src/auth/auth2.ts @@ -0,0 +1,362 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as jose from 'jose' +import { + GetSsoTokenParams, + getSsoTokenRequestType, + GetSsoTokenResult, + IamIdentityCenterSsoTokenSource, + InvalidateSsoTokenParams, + invalidateSsoTokenRequestType, + ProfileKind, + UpdateProfileParams, + updateProfileRequestType, + SsoTokenChangedParams, + ssoTokenChangedRequestType, + AwsBuilderIdSsoTokenSource, + UpdateCredentialsParams, + AwsErrorCodes, + SsoTokenSourceKind, + listProfilesRequestType, + ListProfilesResult, + UpdateProfileResult, + InvalidateSsoTokenResult, + AuthorizationFlowKind, + CancellationToken, + CancellationTokenSource, + bearerCredentialsDeleteNotificationType, + bearerCredentialsUpdateRequestType, + SsoTokenChangedKind, + RequestType, + ResponseMessage, + NotificationType, + ConnectionMetadata, + getConnectionMetadataRequestType, +} from '@aws/language-server-runtimes/protocol' +import { LanguageClient } from 'vscode-languageclient' +import { getLogger } from '../shared/logger/logger' +import { ToolkitError } from '../shared/errors' +import { useDeviceFlow } from './sso/ssoAccessTokenProvider' +import { getCacheFileWatcher } from './sso/cache' + +export const notificationTypes = { + updateBearerToken: new RequestType( + bearerCredentialsUpdateRequestType.method + ), + deleteBearerToken: new NotificationType(bearerCredentialsDeleteNotificationType.method), + getConnectionMetadata: new RequestType( + getConnectionMetadataRequestType.method + ), +} + +export type AuthState = 'notConnected' | 'connected' | 'expired' + +export type AuthStateEvent = { id: string; state: AuthState | 'refreshed' } + +export const LoginTypes = { + SSO: 'sso', + IAM: 'iam', +} as const +export type LoginType = (typeof LoginTypes)[keyof typeof LoginTypes] + +interface BaseLogin { + readonly loginType: LoginType +} + +export type cacheChangedEvent = 'delete' | 'create' + +export type Login = SsoLogin // TODO: add IamLogin type when supported + +export type TokenSource = IamIdentityCenterSsoTokenSource | AwsBuilderIdSsoTokenSource + +/** + * Handles auth requests to the Identity Server in the Amazon Q LSP. + */ +export class LanguageClientAuth { + readonly #ssoCacheWatcher = getCacheFileWatcher() + + constructor( + private readonly client: LanguageClient, + private readonly clientName: string, + public readonly encryptionKey: Buffer + ) {} + + public get cacheWatcher() { + return this.#ssoCacheWatcher + } + + getSsoToken( + tokenSource: TokenSource, + login: boolean = false, + cancellationToken?: CancellationToken + ): Promise { + return this.client.sendRequest( + getSsoTokenRequestType.method, + { + clientName: this.clientName, + source: tokenSource, + options: { + loginOnInvalidToken: login, + authorizationFlow: useDeviceFlow() ? AuthorizationFlowKind.DeviceCode : AuthorizationFlowKind.Pkce, + }, + } satisfies GetSsoTokenParams, + cancellationToken + ) + } + + updateProfile( + profileName: string, + startUrl: string, + region: string, + scopes: string[] + ): Promise { + return this.client.sendRequest(updateProfileRequestType.method, { + profile: { + kinds: [ProfileKind.SsoTokenProfile], + name: profileName, + settings: { + region, + sso_session: profileName, + }, + }, + ssoSession: { + name: profileName, + settings: { + sso_region: region, + sso_start_url: startUrl, + sso_registration_scopes: scopes, + }, + }, + } satisfies UpdateProfileParams) + } + + listProfiles() { + return this.client.sendRequest(listProfilesRequestType.method, {}) as Promise + } + + /** + * Returns a profile by name along with its linked sso_session. + * Does not currently exist as an API in the Identity Service. + */ + async getProfile(profileName: string) { + const response = await this.listProfiles() + const profile = response.profiles.find((profile) => profile.name === profileName) + const ssoSession = profile?.settings?.sso_session + ? response.ssoSessions.find((session) => session.name === profile!.settings!.sso_session) + : undefined + + return { profile, ssoSession } + } + + updateBearerToken(request: UpdateCredentialsParams) { + return this.client.sendRequest(bearerCredentialsUpdateRequestType.method, request) + } + + deleteBearerToken() { + return this.client.sendNotification(bearerCredentialsDeleteNotificationType.method) + } + + invalidateSsoToken(tokenId: string) { + return this.client.sendRequest(invalidateSsoTokenRequestType.method, { + ssoTokenId: tokenId, + } satisfies InvalidateSsoTokenParams) as Promise + } + + registerSsoTokenChangedHandler(ssoTokenChangedHandler: (params: SsoTokenChangedParams) => any) { + this.client.onNotification(ssoTokenChangedRequestType.method, ssoTokenChangedHandler) + } + + registerCacheWatcher(cacheChangedHandler: (event: cacheChangedEvent) => any) { + this.cacheWatcher.onDidCreate(() => cacheChangedHandler('create')) + this.cacheWatcher.onDidDelete(() => cacheChangedHandler('delete')) + } +} + +/** + * Manages an SSO connection. + */ +export class SsoLogin implements BaseLogin { + readonly loginType = LoginTypes.SSO + private readonly eventEmitter = new vscode.EventEmitter() + + // Cached information from the identity server for easy reference + private ssoTokenId: string | undefined + private connectionState: AuthState = 'notConnected' + private _data: { startUrl: string; region: string } | undefined + + private cancellationToken: CancellationTokenSource | undefined + + constructor( + public readonly profileName: string, + private readonly lspAuth: LanguageClientAuth + ) { + lspAuth.registerSsoTokenChangedHandler((params: SsoTokenChangedParams) => this.ssoTokenChangedHandler(params)) + } + + get data() { + return this._data + } + + async login(opts: { startUrl: string; region: string; scopes: string[] }) { + await this.updateProfile(opts) + return this._getSsoToken(true) + } + + async reauthenticate() { + if (this.connectionState === 'notConnected') { + throw new ToolkitError('Cannot reauthenticate when not connected.') + } + return this._getSsoToken(true) + } + + async logout() { + if (this.ssoTokenId) { + await this.lspAuth.invalidateSsoToken(this.ssoTokenId) + } + this.updateConnectionState('notConnected') + this._data = undefined + // TODO: DeleteProfile api in Identity Service (this doesn't exist yet) + } + + async getProfile() { + return await this.lspAuth.getProfile(this.profileName) + } + + async updateProfile(opts: { startUrl: string; region: string; scopes: string[] }) { + await this.lspAuth.updateProfile(this.profileName, opts.startUrl, opts.region, opts.scopes) + this._data = { + startUrl: opts.startUrl, + region: opts.region, + } + } + + /** + * Restore the connection state and connection details to memory, if they exist. + */ + async restore() { + const sessionData = await this.getProfile() + const ssoSession = sessionData?.ssoSession?.settings + if (ssoSession?.sso_region && ssoSession?.sso_start_url) { + this._data = { + startUrl: ssoSession.sso_start_url, + region: ssoSession.sso_region, + } + } + + try { + await this._getSsoToken(false) + } catch (err) { + getLogger().error('Restoring connection failed: %s', err) + } + } + + /** + * Cancels running active login flows. + */ + cancelLogin() { + this.cancellationToken?.cancel() + this.cancellationToken?.dispose() + this.cancellationToken = undefined + } + + /** + * Returns both the decrypted access token and the payload to send to the `updateCredentials` LSP API + * with encrypted token + */ + async getToken() { + const response = await this._getSsoToken(false) + const decryptedKey = await jose.compactDecrypt(response.ssoToken.accessToken, this.lspAuth.encryptionKey) + return { + token: decryptedKey.plaintext.toString().replaceAll('"', ''), + updateCredentialsParams: response.updateCredentialsParams, + } + } + + /** + * Returns the response from `getSsoToken` LSP API and sets the connection state based on the errors/result + * of the call. + */ + private async _getSsoToken(login: boolean) { + let response: GetSsoTokenResult + this.cancellationToken = new CancellationTokenSource() + + try { + response = await this.lspAuth.getSsoToken( + { + /** + * Note that we do not use SsoTokenSourceKind.AwsBuilderId here. + * This is because it does not leave any state behind on disk, so + * we cannot infer that a builder ID connection exists via the + * Identity Server alone. + */ + kind: SsoTokenSourceKind.IamIdentityCenter, + profileName: this.profileName, + } satisfies IamIdentityCenterSsoTokenSource, + login, + this.cancellationToken.token + ) + } catch (err: any) { + switch (err.data?.awsErrorCode) { + case AwsErrorCodes.E_CANCELLED: + case AwsErrorCodes.E_SSO_SESSION_NOT_FOUND: + case AwsErrorCodes.E_PROFILE_NOT_FOUND: + case AwsErrorCodes.E_INVALID_SSO_TOKEN: + this.updateConnectionState('notConnected') + break + case AwsErrorCodes.E_CANNOT_REFRESH_SSO_TOKEN: + this.updateConnectionState('expired') + break + // TODO: implement when identity server emits E_NETWORK_ERROR, E_FILESYSTEM_ERROR + // case AwsErrorCodes.E_NETWORK_ERROR: + // case AwsErrorCodes.E_FILESYSTEM_ERROR: + // // do stuff, probably nothing at all + // break + default: + getLogger().error('SsoLogin: unknown error when requesting token: %s', err) + break + } + throw err + } finally { + this.cancellationToken?.dispose() + this.cancellationToken = undefined + } + + this.ssoTokenId = response.ssoToken.id + this.updateConnectionState('connected') + return response + } + + getConnectionState() { + return this.connectionState + } + + onDidChangeConnectionState(handler: (e: AuthStateEvent) => any) { + return this.eventEmitter.event(handler) + } + + private updateConnectionState(state: AuthState) { + const oldState = this.connectionState + const newState = state + + this.connectionState = newState + + if (oldState !== newState) { + this.eventEmitter.fire({ id: this.profileName, state: this.connectionState }) + } + } + + private ssoTokenChangedHandler(params: SsoTokenChangedParams) { + if (params.ssoTokenId === this.ssoTokenId) { + if (params.kind === SsoTokenChangedKind.Expired) { + this.updateConnectionState('expired') + return + } else if (params.kind === SsoTokenChangedKind.Refreshed) { + this.eventEmitter.fire({ id: this.profileName, state: 'refreshed' }) + } + } + } +} diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 02a0067be45..87a6877844e 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -23,4 +23,8 @@ export { export { Auth } from './auth' export { CredentialsStore } from './credentials/store' export { LoginManager } from './deprecated/loginManager' -export * as AuthUtils from './utils' +export * as constants from './sso/constants' +export * as cache from './sso/cache' +export * as authUtils from './utils' +export * as auth2 from './auth2' +export * as SsoAccessTokenProvider from './sso/ssoAccessTokenProvider' diff --git a/packages/core/src/auth/sso/cache.ts b/packages/core/src/auth/sso/cache.ts index 7d43c07da35..ce9c246ee7a 100644 --- a/packages/core/src/auth/sso/cache.ts +++ b/packages/core/src/auth/sso/cache.ts @@ -126,7 +126,7 @@ export function getTokenCache(directory = getCacheDir()): KeyedCache return mapCache(cache, read, write) } -function getTokenCacheFile(ssoCacheDir: string, key: string): string { +export function getTokenCacheFile(ssoCacheDir: string, key: string): string { const encoded = encodeURI(key) // Per the spec: 'SSO Login Token Flow' the access token must be // cached as the SHA1 hash of the bytes of the UTF-8 encoded @@ -145,7 +145,7 @@ function getTokenCacheFile(ssoCacheDir: string, key: string): string { return path.join(ssoCacheDir, `${hashedKey}.json`) } -function getRegistrationCacheFile(ssoCacheDir: string, key: RegistrationKey): string { +export function getRegistrationCacheFile(ssoCacheDir: string, key: RegistrationKey): string { const hash = (startUrl: string, scopes: string[]) => { const shasum = crypto.createHash('sha256') shasum.update(startUrl) diff --git a/packages/core/src/auth/sso/constants.ts b/packages/core/src/auth/sso/constants.ts index 0e6bb082d7e..14d2382a692 100644 --- a/packages/core/src/auth/sso/constants.ts +++ b/packages/core/src/auth/sso/constants.ts @@ -10,6 +10,7 @@ export const builderIdStartUrl = 'https://view.awsapps.com/start' export const internalStartUrl = 'https://amzn.awsapps.com/start' +export const builderIdRegion = 'us-east-1' /** * Doc: https://docs.aws.amazon.com/singlesignon/latest/userguide/howtochangeURL.html diff --git a/packages/core/src/auth/sso/ssoAccessTokenProvider.ts b/packages/core/src/auth/sso/ssoAccessTokenProvider.ts index e753fb2ef90..7caf2638e1b 100644 --- a/packages/core/src/auth/sso/ssoAccessTokenProvider.ts +++ b/packages/core/src/auth/sso/ssoAccessTokenProvider.ts @@ -289,17 +289,7 @@ export abstract class SsoAccessTokenProvider { profile: Pick, cache = getCache(), oidc: OidcClient = OidcClient.create(profile.region), - reAuthState?: ReAuthState, - useDeviceFlow: () => boolean = () => { - /** - * Device code flow is neccessary when: - * 1. We are in a workspace connected through ssh (codecatalyst, etc) - * 2. We are connected to a remote backend through the web browser (code server, openshift dev spaces) - * - * Since we are unable to serve the final authorization page - */ - return getExtRuntimeContext().extensionHost === 'remote' - } + reAuthState?: ReAuthState ) { if (DevSettings.instance.get('webAuth', false) && getExtRuntimeContext().extensionHost === 'webworker') { return new WebAuthorization(profile, cache, oidc, reAuthState) @@ -400,6 +390,17 @@ function getSessionDuration(id: string) { return creationDate !== undefined ? globals.clock.Date.now() - creationDate : undefined } +export function useDeviceFlow(): boolean { + /** + * Device code flow is neccessary when: + * 1. We are in a workspace connected through ssh (codecatalyst, etc) + * 2. We are connected to a remote backend through the web browser (code server, openshift dev spaces) + * + * Since we are unable to serve the final authorization page + */ + return getExtRuntimeContext().extensionHost === 'remote' +} + /** * SSO "device code" flow (RFC: https://tools.ietf.org/html/rfc8628) * 1. Get a client id (SSO-OIDC identifier, formatted per RFC6749). diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index 9da35fa06e5..1592774a338 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -45,8 +45,8 @@ import { Commands, placeholder } from '../shared/vscode/commands2' import { Auth } from './auth' import { validateIsNewSsoUrl, validateSsoUrlFormat } from './sso/validation' import { getLogger } from '../shared/logger/logger' -import { AuthUtil, isValidAmazonQConnection, isValidCodeWhispererCoreConnection } from '../codewhisperer/util/authUtil' import { AuthFormId } from '../login/webview/vue/types' +import { amazonQScopes, AuthUtil } from '../codewhisperer/util/authUtil' import { extensionVersion } from '../shared/vscode/env' import { CommonAuthWebview } from '../login/webview/vue/backend' import { AuthSource } from '../login/webview/util' @@ -585,7 +585,7 @@ export async function hasIamCredentials( return (await allConnections()).some(isIamConnection) } -export type SsoKind = 'any' | 'codewhisperer' | 'codecatalyst' +export type SsoKind = 'any' | 'codecatalyst' /** * Returns true if an Identity Center SSO connection exists. @@ -606,11 +606,6 @@ export async function findSsoConnections( ): Promise { let predicate: (c?: Connection) => boolean switch (kind) { - case 'codewhisperer': - predicate = (conn?: Connection) => { - return isIdcSsoConnection(conn) && isValidCodeWhispererCoreConnection(conn) - } - break case 'codecatalyst': predicate = (conn?: Connection) => { return isIdcSsoConnection(conn) && isValidCodeCatalystConnection(conn) @@ -622,7 +617,7 @@ export async function findSsoConnections( return (await allConnections()).filter(predicate).filter(isIdcSsoConnection) } -export type BuilderIdKind = 'any' | 'codewhisperer' | 'codecatalyst' +export type BuilderIdKind = 'any' | 'codecatalyst' /** * Returns true if a Builder ID connection exists. @@ -643,11 +638,6 @@ async function findBuilderIdConnections( ): Promise { let predicate: (c?: Connection) => boolean switch (kind) { - case 'codewhisperer': - predicate = (conn?: Connection) => { - return isBuilderIdConnection(conn) && isValidCodeWhispererCoreConnection(conn) - } - break case 'codecatalyst': predicate = (conn?: Connection) => { return isBuilderIdConnection(conn) && isValidCodeCatalystConnection(conn) @@ -656,7 +646,7 @@ async function findBuilderIdConnections( case 'any': predicate = isBuilderIdConnection } - return (await allConnections()).filter(predicate).filter(isAnySsoConnection) + return (await allConnections()).filter(predicate!).filter(isAnySsoConnection) } /** @@ -803,7 +793,7 @@ export function getAuthFormIdsFromConnection(conn?: Connection): AuthFormId[] { if (isValidCodeCatalystConnection(conn)) { authIds.push(`${connType}CodeCatalyst`) } - if (isValidAmazonQConnection(conn)) { + if (hasScopes(conn, amazonQScopes)) { authIds.push(`${connType}CodeWhisperer`) } @@ -818,9 +808,9 @@ export function initializeCredentialsProviderManager() { export async function getAuthType() { let authType: CredentialSourceId | undefined = undefined - if (AuthUtil.instance.isEnterpriseSsoInUse() && AuthUtil.instance.isConnectionValid()) { + if (AuthUtil.instance.isIdcConnection()) { authType = 'iamIdentityCenter' - } else if (AuthUtil.instance.isBuilderIdInUse() && AuthUtil.instance.isConnectionValid()) { + } else if (AuthUtil.instance.isBuilderIdConnection()) { authType = 'awsId' } return authType diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index e52e08bb98b..e11c7f5fe80 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -72,7 +72,7 @@ import { AuthUtil } from './util/authUtil' import { ImportAdderProvider } from './service/importAdderProvider' import { TelemetryHelper } from './util/telemetryHelper' import { openUrl } from '../shared/utilities/vsCodeUtils' -import { notifyNewCustomizations, onProfileChangedListener } from './util/customizationUtil' +import { onProfileChangedListener } from './util/customizationUtil' import { CodeWhispererCommandBackend, CodeWhispererCommandDeclarations } from './commands/gettingStartedPageCommands' import { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' import { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' @@ -91,7 +91,6 @@ import { setContext } from '../shared/vscode/setContext' import { syncSecurityIssueWebview } from './views/securityIssue/securityIssueWebview' import { detectCommentAboveLine } from '../shared/utilities/commentUtils' import { activateEditTracking } from './nextEditPrediction/activation' -import { notifySelectDeveloperProfile } from './region/utils' let localize: nls.LocalizeFunc @@ -101,10 +100,6 @@ export async function activate(context: ExtContext): Promise { // Import old CodeWhisperer settings into Amazon Q await CodeWhispererSettings.instance.importSettings() - // initialize AuthUtil earlier to make sure it can listen to connection change events. - const auth = AuthUtil.instance - auth.initCodeWhispererHooks() - // TODO: is this indirection useful? registerDeclaredCommands( context.extensionContext.subscriptions, @@ -144,7 +139,7 @@ export async function activate(context: ExtContext): Promise { context.extensionContext.subscriptions.push( // register toolkit api callback registerToolkitApiCallback.register(), - signoutCodeWhisperer.register(auth), + signoutCodeWhisperer.register(), /** * Configuration change */ @@ -155,7 +150,7 @@ export async function activate(context: ExtContext): Promise { if (configurationChangeEvent.affectsConfiguration('amazonQ.showCodeWithReferences')) { ReferenceLogViewProvider.instance.update() - if (auth.isEnterpriseSsoInUse()) { + if (AuthUtil.instance.isIdcConnection()) { await vscode.window .showInformationMessage( CodeWhispererConstants.ssoConfigAlertMessage, @@ -170,7 +165,7 @@ export async function activate(context: ExtContext): Promise { } if (configurationChangeEvent.affectsConfiguration('amazonQ.shareContentWithAWS')) { - if (auth.isEnterpriseSsoInUse()) { + if (AuthUtil.instance.isIdcConnection()) { await vscode.window .showInformationMessage( CodeWhispererConstants.ssoConfigAlertMessageShareData, @@ -340,36 +335,26 @@ export async function activate(context: ExtContext): Promise { SecurityIssueCodeActionProvider.instance ), vscode.commands.registerCommand('aws.amazonq.openEditorAtRange', openEditorAtRange), - auth.regionProfileManager.onDidChangeRegionProfile(onProfileChangedListener) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(onProfileChangedListener) ) // run the auth startup code with context for telemetry await telemetry.function_call.run( async () => { - await auth.restore() - await auth.clearExtraConnections() - - if (auth.isConnectionExpired()) { - auth.showReauthenticatePrompt().catch((e) => { + if (AuthUtil.instance.isConnectionExpired()) { + AuthUtil.instance.showReauthenticatePrompt().catch((e) => { const defaulMsg = localize('AWS.generic.message.error', 'Failed to reauth:') void logAndShowError(localize, e, 'showReauthenticatePrompt', defaulMsg) }) - if (auth.isEnterpriseSsoInUse()) { - await auth.notifySessionConfiguration() + if (AuthUtil.instance.isIdcConnection()) { + await AuthUtil.instance.notifySessionConfiguration() } } - - if (auth.requireProfileSelection()) { - await notifySelectDeveloperProfile() - } }, { emit: false, functionId: { name: 'activateCwCore' } } ) - if (auth.isValidEnterpriseSsoInUse()) { - await notifyNewCustomizations() - } - if (auth.isBuilderIdInUse()) { + if (AuthUtil.instance.isBuilderIdConnection()) { await CodeScansState.instance.setScansEnabled(false) } @@ -384,8 +369,8 @@ export async function activate(context: ExtContext): Promise { return ( (isScansEnabled ?? CodeScansState.instance.isScansEnabled()) && !CodeScansState.instance.isMonthlyQuotaExceeded() && - auth.isConnectionValid() && - !auth.isBuilderIdInUse() && + AuthUtil.instance.isConnected() && + !AuthUtil.instance.isBuilderIdConnection() && editor && editor.document.uri.scheme === 'file' && securityScanLanguageContext.isLanguageSupported(editor.document.languageId) @@ -513,6 +498,7 @@ export async function activate(context: ExtContext): Promise { export async function shutdown() { RecommendationHandler.instance.reportUserDecisions(-1) await CodeWhispererTracker.getTracker().shutdown() + AuthUtil.instance.regionProfileManager.globalStatePoller.kill() } function toggleIssuesVisibility(visibleCondition: (issue: CodeScanIssue, filePath: string) => boolean) { diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 35f699b24c2..0a473dfdccd 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -13,7 +13,6 @@ import { hasVendedIamCredentials } from '../../auth/auth' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { PromiseResult } from 'aws-sdk/lib/request' import { AuthUtil } from '../util/authUtil' -import { isSsoConnection } from '../../auth/connection' import apiConfig = require('./service-2.json') import userApiConfig = require('./user-service-2.json') import { session } from '../util/codeWhispererSession' @@ -84,6 +83,8 @@ export type Imports = CodeWhispererUserClient.Imports export class DefaultCodeWhispererClient { private async createSdkClient(): Promise { + throw new Error('Do not call this function until IAM is supported by LSP identity server') + const isOptedOut = CodeWhispererSettings.instance.isOptoutEnabled() const cwsprConfig = getCodewhispererConfig() return (await globals.sdkClientBuilder.createAwsService( @@ -91,7 +92,7 @@ export class DefaultCodeWhispererClient { { apiConfig: apiConfig, region: cwsprConfig.region, - credentials: await AuthUtil.instance.getCredentials(), + credentials: undefined, endpoint: cwsprConfig.endpoint, onRequestSetup: [ (req) => { @@ -126,7 +127,7 @@ export class DefaultCodeWhispererClient { async createUserSdkClient(maxRetries?: number): Promise { const isOptedOut = CodeWhispererSettings.instance.isOptoutEnabled() session.setFetchCredentialStart() - const bearerToken = await AuthUtil.instance.getBearerToken() + const bearerToken = await AuthUtil.instance.getToken() session.setSdkApiCallStart() const cwsprConfig = getCodewhispererConfig() return (await globals.sdkClientBuilder.createAwsService( @@ -156,7 +157,7 @@ export class DefaultCodeWhispererClient { } private isBearerTokenAuth(): boolean { - return isSsoConnection(AuthUtil.instance.conn) + return AuthUtil.instance.isConnected() // TODO: Handle IAM credentials } public async generateRecommendations( @@ -230,7 +231,7 @@ export class DefaultCodeWhispererClient { }, profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, } - if (!AuthUtil.instance.isValidEnterpriseSsoInUse() && !globals.telemetry.telemetryEnabled) { + if (!AuthUtil.instance.isIdcConnection() && !globals.telemetry.telemetryEnabled) { return } const response = await (await this.createUserSdkClient()).sendTelemetryEvent(requestWithCommonFields).promise() diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index 7fe6078a1d7..82bcba7e9e4 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -52,7 +52,6 @@ import { removeDiagnostic } from '../service/diagnosticsProvider' import { SsoAccessTokenProvider } from '../../auth/sso/ssoAccessTokenProvider' import { ToolkitError, getErrorMsg, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' import { isRemoteWorkspace } from '../../shared/vscode/env' -import { isBuilderIdConnection } from '../../auth/connection' import globals from '../../shared/extensionGlobals' import { getVscodeCliPath } from '../../shared/utilities/pathFind' import { tryRun } from '../../shared/utilities/pathFind' @@ -114,7 +113,7 @@ export const toggleCodeScans = Commands.declare( { id: 'aws.codeWhisperer.toggleCodeScan', compositeKey: { 1: 'source' } }, (scansState: CodeScansState) => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { await telemetry.aws_modifySetting.run(async (span) => { - if (isBuilderIdConnection(AuthUtil.instance.conn)) { + if (AuthUtil.instance.isBuilderIdConnection()) { throw new Error(`Auto-scans are not supported with the Amazon Builder ID connection.`) } span.record({ @@ -239,7 +238,7 @@ export const showFileScan = Commands.declare( export const selectCustomizationPrompt = Commands.declare( { id: 'aws.amazonq.selectCustomization', compositeKey: { 1: 'source' } }, () => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { - if (isBuilderIdConnection(AuthUtil.instance.conn)) { + if (AuthUtil.instance.isBuilderIdConnection()) { throw new Error(`Select Customizations are not supported with the Amazon Builder ID connection.`) } telemetry.ui_click.emit({ elementId: 'cw_selectCustomization_Cta' }) @@ -376,7 +375,7 @@ export const openSecurityIssuePanel = Commands.declare( findingId: targetIssue.findingId, detectorId: targetIssue.detectorId, ruleId: targetIssue.ruleId, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, autoDetected: targetIssue.autoDetected, }) TelemetryHelper.instance.sendCodeScanRemediationsEvent( @@ -465,7 +464,7 @@ export const applySecurityFix = Commands.declare( ruleId: targetIssue.ruleId, component: targetSource, result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, codeFixAction: 'applyFix', autoDetected: targetIssue.autoDetected, codewhispererCodeScanJobId: targetIssue.scanJobId, @@ -594,8 +593,8 @@ export const applySecurityFix = Commands.declare( export const signoutCodeWhisperer = Commands.declare( { id: 'aws.amazonq.signout', compositeKey: { 1: 'source' } }, - (auth: AuthUtil) => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { - await auth.secondaryAuth.deleteConnection() + () => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { + await AuthUtil.instance.logout() SecurityIssueTreeViewProvider.instance.refresh() return focusAmazonQPanel.execute(placeholder, source) } @@ -652,14 +651,13 @@ export const registerToolkitApiCallback = Commands.declare( if (_toolkitApi) { registerToolkitApiCallbackOnce() // Declare current conn immediately - const currentConn = AuthUtil.instance.conn - if (currentConn?.type === 'sso') { + if (AuthUtil.instance.isConnected() && AuthUtil.instance.isSsoSession()) { _toolkitApi.declareConnection( { - type: currentConn.type, - ssoRegion: currentConn.ssoRegion, - startUrl: currentConn.startUrl, - id: currentConn.id, + type: 'sso', + ssoRegion: AuthUtil.instance.connection?.region, + startUrl: AuthUtil.instance.connection?.startUrl, + id: AuthUtil.instance.profileName, } as AwsConnection, 'Amazon Q' ) @@ -855,7 +853,7 @@ export const ignoreAllIssues = Commands.declare( telemetry.record({ component: targetSource, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, detectorId: targetIssue.detectorId, findingId: targetIssue.findingId, ruleId: targetIssue.ruleId, @@ -890,7 +888,7 @@ export const ignoreIssue = Commands.declare( telemetry.record({ component: targetSource, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, detectorId: targetIssue.detectorId, findingId: targetIssue.findingId, ruleId: targetIssue.ruleId, diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index d04fe6effc3..344f3b0eb58 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -135,7 +135,7 @@ export async function startSecurityScan( result: 'Succeeded', codewhispererCodeScanTotalIssues: 0, codewhispererCodeScanIssuesWithFixes: 0, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, codewhispererCodeScanScope: scope, source: initiatedByChat ? 'chat' : 'menu', } diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 4235ae28668..9416f9caa04 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -94,6 +94,7 @@ export * from './util/commonUtil' export * from './util/supplementalContext/codeParsingUtil' export * from './util/supplementalContext/supplementalContextUtil' export * from './util/codewhispererSettings' +export * as getStartUrl from './util/getStartUrl' export * as supplementalContextUtil from './util/supplementalContext/supplementalContextUtil' export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' @@ -106,6 +107,7 @@ export { baseCustomization, onProfileChangedListener, CustomizationProvider, + notifyNewCustomizations, } from './util/customizationUtil' export { Container } from './service/serviceContainer' export * from './util/gitUtil' diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index 149a78391f8..4ba64570521 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -8,13 +8,6 @@ import { getIcon } from '../../shared/icons' import { DataQuickPickItem } from '../../shared/ui/pickerPrompter' import { CodeWhispererConfig, RegionProfile } from '../models/model' import { showConfirmationMessage } from '../../shared/utilities/messages' -import { - Connection, - isBuilderIdConnection, - isIdcSsoConnection, - isSsoConnection, - SsoConnection, -} from '../../auth/connection' import globals from '../../shared/extensionGlobals' import { once } from '../../shared/utilities/functionUtils' import CodeWhispererUserClient from '../client/codewhispereruserclient' @@ -28,8 +21,10 @@ import { parse } from '@aws-sdk/util-arn-parser' import { isAwsError, ToolkitError } from '../../shared/errors' import { telemetry } from '../../shared/telemetry/telemetry' import { localize } from '../../shared/utilities/vsCodeUtils' +import { IAuthProvider } from '../util/authUtil' import { Commands } from '../../shared/vscode/commands2' import { CachedResource } from '../../shared/utilities/resourceCache' +import { GlobalStatePoller } from '../../shared/globalState' // TODO: is there a better way to manage all endpoint strings in one place? export const defaultServiceConfig: CodeWhispererConfig = { @@ -43,6 +38,9 @@ const endpoints = createConstantMap({ 'eu-central-1': 'https://q.eu-central-1.amazonaws.com/', }) +const getRegionProfile = () => + globals.globalState.tryGet<{ [label: string]: RegionProfile }>('aws.amazonq.regionProfiles', Object, {}) + /** * 'user' -> users change the profile through Q menu * 'auth' -> users change the profile through webview profile selector page @@ -61,7 +59,6 @@ export class RegionProfileManager { private _activeRegionProfile: RegionProfile | undefined private _onDidChangeRegionProfile = new vscode.EventEmitter() public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event - // Store the last API results (for UI propuse) so we don't need to call service again if doesn't require "latest" result private _profiles: RegionProfile[] = [] @@ -86,22 +83,31 @@ export class RegionProfileManager { } })(this.listRegionProfile.bind(this)) + // This is a poller that handles synchornization of selected region profiles between different IDE windows. + // It checks for changes in global state of region profile, invoking the change handler to switch profiles + public globalStatePoller = GlobalStatePoller.create({ + getState: getRegionProfile, + changeHandler: async () => { + const profile = this.loadPersistedRegionProfle() + void this._switchRegionProfile(profile[this.authProvider.profileName], 'reload') + }, + pollIntervalInMs: 2000, + }) + get activeRegionProfile() { - const conn = this.connectionProvider() - if (isBuilderIdConnection(conn)) { + if (this.authProvider.isBuilderIdConnection()) { return undefined } return this._activeRegionProfile } get clientConfig(): CodeWhispererConfig { - const conn = this.connectionProvider() - if (!conn) { + if (!this.authProvider.isConnected()) { throw new ToolkitError('trying to get client configuration without credential') } // builder id should simply use default IAD - if (isBuilderIdConnection(conn)) { + if (this.authProvider.isBuilderIdConnection()) { return defaultServiceConfig } @@ -129,7 +135,7 @@ export class RegionProfileManager { return this._profiles } - constructor(private readonly connectionProvider: () => Connection | undefined) {} + constructor(private readonly authProvider: IAuthProvider) {} async getProfiles(): Promise { return this.cache.getResource() @@ -138,15 +144,14 @@ export class RegionProfileManager { async listRegionProfile(): Promise { this._profiles = [] - const conn = this.connectionProvider() - if (conn === undefined || !isSsoConnection(conn)) { + if (!this.authProvider.isConnected() || !this.authProvider.isSsoSession()) { return [] } const availableProfiles: RegionProfile[] = [] const failedRegions: string[] = [] for (const [region, endpoint] of endpoints.entries()) { - const client = await this._createQClient(region, endpoint, conn as SsoConnection) + const client = await this._createQClient(region, endpoint) const requester = async (request: CodeWhispererUserClient.ListAvailableProfilesRequest) => client.listAvailableProfiles(request).promise() const request: CodeWhispererUserClient.ListAvailableProfilesRequest = {} @@ -187,8 +192,7 @@ export class RegionProfileManager { } async switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) { - const conn = this.connectionProvider() - if (conn === undefined || !isIdcSsoConnection(conn)) { + if (!this.authProvider.isConnected() || !this.authProvider.isIdcConnection()) { return } @@ -196,9 +200,6 @@ export class RegionProfileManager { return } - // TODO: make it typesafe - const ssoConn = this.connectionProvider() as SsoConnection - // only prompt to users when users switch from A profile to B profile if (source !== 'customization' && this.activeRegionProfile !== undefined && regionProfile !== undefined) { const response = await showConfirmationMessage({ @@ -216,9 +217,9 @@ export class RegionProfileManager { telemetry.amazonq_didSelectProfile.emit({ source: source, amazonQProfileRegion: this.activeRegionProfile?.region ?? 'not-set', - ssoRegion: ssoConn.ssoRegion, + ssoRegion: this.authProvider.connection?.region, result: 'Cancelled', - credentialStartUrl: ssoConn.startUrl, + credentialStartUrl: this.authProvider.connection?.startUrl, profileCount: this.profiles.length, }) return @@ -235,9 +236,9 @@ export class RegionProfileManager { telemetry.amazonq_didSelectProfile.emit({ source: source, amazonQProfileRegion: regionProfile?.region ?? 'not-set', - ssoRegion: ssoConn.ssoRegion, + ssoRegion: this.authProvider.connection?.region, result: 'Succeeded', - credentialStartUrl: ssoConn.startUrl, + credentialStartUrl: this.authProvider.connection?.startUrl, profileCount: this.profiles.length, }) } @@ -246,6 +247,10 @@ export class RegionProfileManager { } private async _switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) { + if (this._activeRegionProfile?.arn === regionProfile?.arn) { + return + } + this._activeRegionProfile = regionProfile this._onDidChangeRegionProfile.fire({ @@ -265,15 +270,14 @@ export class RegionProfileManager { } restoreProfileSelection = once(async () => { - const conn = this.connectionProvider() - if (conn) { - await this.restoreRegionProfile(conn) + if (this.authProvider.isConnected()) { + await this.restoreRegionProfile() } }) - // Note: should be called after [AuthUtil.instance.conn] returns non null - async restoreRegionProfile(conn: Connection) { - const previousSelected = this.loadPersistedRegionProfle()[conn.id] || undefined + // Note: should be called after [this.authProvider.isConnected()] returns non null + async restoreRegionProfile() { + const previousSelected = this.loadPersistedRegionProfle()[this.authProvider.profileName] || undefined if (!previousSelected) { return } @@ -308,31 +312,19 @@ export class RegionProfileManager { } private loadPersistedRegionProfle(): { [label: string]: RegionProfile } { - const previousPersistedState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( - 'aws.amazonq.regionProfiles', - Object, - {} - ) - - return previousPersistedState + return getRegionProfile() } async persistSelectRegionProfile() { - const conn = this.connectionProvider() - // default has empty arn and shouldn't be persisted because it's just a fallback - if (!conn || this.activeRegionProfile === undefined) { + if (!this.authProvider.isConnected() || this.activeRegionProfile === undefined) { return } // persist connectionId to profileArn - const previousPersistedState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( - 'aws.amazonq.regionProfiles', - Object, - {} - ) + const previousPersistedState = getRegionProfile() - previousPersistedState[conn.id] = this.activeRegionProfile + previousPersistedState[this.authProvider.profileName] = this.activeRegionProfile await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState) } @@ -387,27 +379,32 @@ export class RegionProfileManager { } } - // Should be called on connection changed in case users change to a differnet connection and use the wrong resultset. + requireProfileSelection(): boolean { + if (this.authProvider.isBuilderIdConnection()) { + return false + } + return this.authProvider.isIdcConnection() && this.activeRegionProfile === undefined + } + async clearCache() { await this.cache.clearCache() } // TODO: Should maintain sdk client in a better way async createQClient(profile: RegionProfile): Promise { - const conn = this.connectionProvider() - if (conn === undefined || !isSsoConnection(conn)) { + if (!this.authProvider.isConnected() || !this.authProvider.isSsoSession()) { throw new Error('No valid SSO connection') } const endpoint = endpoints.get(profile.region) if (!endpoint) { throw new Error(`trying to initiatize Q client with unrecognizable region ${profile.region}`) } - return this._createQClient(profile.region, endpoint, conn) + return this._createQClient(profile.region, endpoint) } // Visible for testing only, do not use this directly, please use createQClient(profile) - async _createQClient(region: string, endpoint: string, conn: SsoConnection): Promise { - const token = (await conn.getToken()).accessToken + async _createQClient(region: string, endpoint: string): Promise { + const token = await this.authProvider.getToken() const serviceOption: ServiceOptions = { apiConfig: userApiConfig, region: region, diff --git a/packages/core/src/codewhisperer/region/utils.ts b/packages/core/src/codewhisperer/region/utils.ts index dd988f74a30..fb768e3b710 100644 --- a/packages/core/src/codewhisperer/region/utils.ts +++ b/packages/core/src/codewhisperer/region/utils.ts @@ -7,7 +7,6 @@ const localize = nls.loadMessageBundle() import { AmazonQPromptSettings } from '../../shared/settings' import { telemetry } from '../../shared/telemetry/telemetry' import vscode from 'vscode' -import { selectRegionProfileCommand } from '../commands/basicCommands' import { placeholder } from '../../shared/vscode/commands2' import { toastMessage } from '../commands/types' @@ -36,7 +35,7 @@ export async function notifySelectDeveloperProfile() { if (resp === selectProfile) { // Show Profile telemetry.record({ action: 'select' }) - void selectRegionProfileCommand.execute(placeholder, toastMessage) + void vscode.commands.executeCommand('aws.amazonq.selectRegionProfile', placeholder, toastMessage) } else if (resp === dontShowAgain) { telemetry.record({ action: 'dontShowAgain' }) await settings.disablePrompt(suppressId) diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts index cc9887adb1f..4c3b93425be 100644 --- a/packages/core/src/codewhisperer/service/inlineCompletionService.ts +++ b/packages/core/src/codewhisperer/service/inlineCompletionService.ts @@ -166,8 +166,8 @@ export class InlineCompletionService { /** Updates the status bar to represent the latest CW state */ refreshStatusBar() { - if (AuthUtil.instance.isConnectionValid()) { - if (AuthUtil.instance.requireProfileSelection()) { + if (AuthUtil.instance.isConnected()) { + if (AuthUtil.instance.regionProfileManager.requireProfileSelection()) { return this.setState('needsProfile') } return this.setState('ok') diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts index 8ab491b32e0..cccdacfcbd2 100644 --- a/packages/core/src/codewhisperer/service/recommendationHandler.ts +++ b/packages/core/src/codewhisperer/service/recommendationHandler.ts @@ -42,7 +42,6 @@ import { application } from '../util/codeWhispererApplication' import { openUrl } from '../../shared/utilities/vsCodeUtils' import { indent } from '../../shared/utilities/textUtilities' import path from 'path' -import { isIamConnection } from '../../auth/connection' import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' /** @@ -172,7 +171,7 @@ export class RecommendationHandler { autoTriggerType?: CodewhispererAutomatedTriggerType, pagination: boolean = true, page: number = 0, - generate: boolean = isIamConnection(AuthUtil.instance.conn) + generate: boolean = false ): Promise { let invocationResult: 'Succeeded' | 'Failed' = 'Failed' let errorMessage: string | undefined = undefined @@ -716,7 +715,7 @@ export class RecommendationHandler { codewhispererLanguage: languageContext.language, duration: performance.now() - this.lastInvocationTime, passive: true, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, result: 'Succeeded', }) } diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts index de78b435913..b7a5aa143a6 100644 --- a/packages/core/src/codewhisperer/service/recommendationService.ts +++ b/packages/core/src/codewhisperer/service/recommendationService.ts @@ -68,7 +68,7 @@ export class RecommendationService { event?: vscode.TextDocumentChangeEvent ) { // TODO: should move all downstream auth check(inlineCompletionService, recommendationHandler etc) to here(upstream) instead of spreading everywhere - if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { + if (AuthUtil.instance.isConnected() && AuthUtil.instance.regionProfileManager.requireProfileSelection()) { return } diff --git a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts index 9ec20b8cb44..30b51e628eb 100644 --- a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts @@ -119,7 +119,7 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { let prompt = '' if (showPrompt) { - if (AuthUtil.instance.isEnterpriseSsoInUse()) { + if (AuthUtil.instance.isIdcConnection()) { prompt = CodeWhispererConstants.referenceLogPromptTextEnterpriseSSO } else { prompt = CodeWhispererConstants.referenceLogPromptText diff --git a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts index b82c10063e6..7934ff8f26c 100644 --- a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts @@ -44,7 +44,7 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { detectorId: issue.detectorId, ruleId: issue.ruleId, includesFix: !!issue.suggestedFixes.length, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, autoDetected: issue.autoDetected, }) TelemetryHelper.instance.sendCodeScanRemediationsEvent( diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index b83fdbebb1a..5195e8a3e4c 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -300,7 +300,7 @@ export async function getPresignedUrlAndUpload( span.record({ amazonqUploadIntent: uploadIntent, amazonqRepositorySize: zipMetadata.srcPayloadSizeInBytes, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) const srcReq: CreateUploadUrlRequest = { contentMd5: getMd5(zipMetadata.zipFilePath), diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts index 0989f022245..3e03b98b054 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts @@ -128,7 +128,7 @@ export class CodeWhispererCodeCoverageTracker { codewhispererPercentage: percentage ? percentage : 0, successCount: this._serviceInvocationCount, codewhispererCustomizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) client diff --git a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts index ca19c87505f..c20dfb3c900 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts @@ -111,7 +111,7 @@ export class CodeWhispererTracker { cwsprChatConversationId: suggestion.conversationID, cwsprChatMessageId: suggestion.messageID, cwsprChatModificationPercentage: percentage ? percentage : 0, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, } telemetry.amazonq_modifyCode.emit(event) @@ -138,7 +138,7 @@ export class CodeWhispererTracker { codewhispererModificationPercentage: percentage ? percentage : 0, codewhispererCompletionType: suggestion.completionType, codewhispererLanguage: suggestion.language, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, codewhispererCharactersAccepted: suggestion.originalString.length, codewhispererCharactersModified: 0, // TODO: currently we don't have an accurate number for this field with existing implementation }) diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts index c3e46bdc78e..1b887e587d5 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -28,6 +28,7 @@ import { AuthUtil } from '../util/authUtil' import { submitFeedback } from '../../feedback/vue/submitFeedback' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { isWeb } from '../../shared/extensionGlobals' +import { builderIdRegion, builderIdStartUrl } from '../../auth/sso/constants' export function createAutoSuggestions(running: boolean): DataQuickPickItem<'autoSuggestions'> { const labelResume = localize('AWS.codewhisperer.resumeCodeWhispererNode.label', 'Resume Auto-Suggestions') @@ -178,7 +179,7 @@ export function createGettingStarted(): DataQuickPickItem<'gettingStarted'> { export function createSignout(): DataQuickPickItem<'signout'> { const label = localize('AWS.codewhisperer.signoutNode.label', 'Sign Out') const icon = getIcon('vscode-export') - const connection = AuthUtil.instance.isBuilderIdInUse() ? 'AWS Builder ID' : 'IAM Identity Center' + const connection = AuthUtil.instance.isBuilderIdConnection() ? 'AWS Builder ID' : 'IAM Identity Center' return { data: 'signout', @@ -252,7 +253,7 @@ export function createSignIn(): DataQuickPickItem<'signIn'> { if (isWeb()) { // TODO: nkomonen, call a Command instead onClick = () => { - void AuthUtil.instance.connectToAwsBuilderId() + void AuthUtil.instance.login(builderIdStartUrl, builderIdRegion) } } diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index 2ad14a81df0..d8c05270073 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -43,7 +43,7 @@ function getAmazonQCodeWhispererNodes() { return [createSignIn(), createLearnMore()] } - if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { + if (AuthUtil.instance.isConnected() && AuthUtil.instance.regionProfileManager.requireProfileSelection()) { return [] } @@ -72,12 +72,12 @@ function getAmazonQCodeWhispererNodes() { // Security scans createSeparator('Code Reviews'), - ...(AuthUtil.instance.isBuilderIdInUse() ? [] : [createAutoScans(autoScansEnabled)]), + ...(AuthUtil.instance.isBuilderIdConnection() ? [] : [createAutoScans(autoScansEnabled)]), createSecurityScan(), // Amazon Q + others createSeparator('Other Features'), - ...(AuthUtil.instance.isValidEnterpriseSsoInUse() && AuthUtil.instance.isCustomizationFeatureEnabled + ...(AuthUtil.instance.isIdcConnection() && AuthUtil.instance.isCustomizationFeatureEnabled ? [createSelectCustomization()] : []), switchToAmazonQNode(), @@ -85,7 +85,7 @@ function getAmazonQCodeWhispererNodes() { } export function getQuickPickItems(): DataQuickPickItem[] { - const isUsingEnterpriseSso = AuthUtil.instance.isValidEnterpriseSsoInUse() + const isUsingEnterpriseSso = AuthUtil.instance.isIdcConnection() const regionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile const children = [ @@ -104,7 +104,7 @@ export function getQuickPickItems(): DataQuickPickItem[] { // Add settings and signout createSeparator(), createSettingsNode(), - ...(isUsingEnterpriseSso && regionProfile ? [createSelectRegionProfileNode(regionProfile)] : []), + ...(AuthUtil.instance.isIdcConnection() && regionProfile ? [createSelectRegionProfileNode(regionProfile)] : []), ...(AuthUtil.instance.isConnected() && !hasVendedIamCredentials() && !hasVendedCredentialsFromMetadata() ? [createSignout()] : []), diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index 10acbe16424..ec9a2ff91e1 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -5,360 +5,206 @@ import * as vscode from 'vscode' import * as localizedText from '../../shared/localizedText' -import { Auth } from '../../auth/auth' -import { ToolkitError, isNetworkError, tryRun } from '../../shared/errors' -import { getSecondaryAuth, setScopes } from '../../auth/secondaryAuth' -import { isSageMaker } from '../../shared/extensionUtilities' +import * as nls from 'vscode-nls' +import { fs } from '../../shared/fs/fs' +import * as path from 'path' +import * as crypto from 'crypto' +import { ToolkitError } from '../../shared/errors' import { AmazonQPromptSettings } from '../../shared/settings' import { scopesCodeWhispererCore, - createBuilderIdProfile, - hasScopes, - SsoConnection, - createSsoProfile, - Connection, - isIamConnection, - isSsoConnection, - isBuilderIdConnection, scopesCodeWhispererChat, scopesFeatureDev, scopesGumby, - isIdcSsoConnection, + TelemetryMetadata, + scopesSsoAccountAccess, + hasScopes, + SsoProfile, + StoredProfile, hasExactScopes, - getTelemetryMetadataForConn, - ProfileNotFoundError, } from '../../auth/connection' import { getLogger } from '../../shared/logger/logger' -import { Commands, placeholder } from '../../shared/vscode/commands2' +import { Commands } from '../../shared/vscode/commands2' import { vsCodeState } from '../models/model' -import { onceChanged, once } from '../../shared/utilities/functionUtils' -import { indent } from '../../shared/utilities/textUtilities' import { showReauthenticateMessage } from '../../shared/utilities/messages' import { showAmazonQWalkthroughOnce } from '../../amazonq/onboardingPage/walkthrough' import { setContext } from '../../shared/vscode/setContext' -import { isInDevEnv } from '../../shared/vscode/env' import { openUrl } from '../../shared/utilities/vsCodeUtils' -import * as nls from 'vscode-nls' -const localize = nls.loadMessageBundle() import { telemetry } from '../../shared/telemetry/telemetry' -import { asStringifiedStack } from '../../shared/telemetry/spans' -import { withTelemetryContext } from '../../shared/telemetry/util' -import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' -import { throttle } from 'lodash' +import { AuthStateEvent, cacheChangedEvent, LanguageClientAuth, LoginTypes, SsoLogin } from '../../auth/auth2' +import { builderIdStartUrl, internalStartUrl } from '../../auth/sso/constants' +import { VSCODE_EXTENSION_ID } from '../../shared/extensions' import { RegionProfileManager } from '../region/regionProfileManager' +import { AuthFormId } from '../../login/webview/vue/types' +import { getEnvironmentSpecificMemento } from '../../shared/utilities/mementos' +import { getCacheDir, getRegistrationCacheFile, getTokenCacheFile } from '../../auth/sso/cache' +import { notifySelectDeveloperProfile } from '../region/utils' +import { once } from '../../shared/utilities/functionUtils' + +const localize = nls.loadMessageBundle() /** Backwards compatibility for connections w pre-chat scopes */ export const codeWhispererCoreScopes = [...scopesCodeWhispererCore] export const codeWhispererChatScopes = [...codeWhispererCoreScopes, ...scopesCodeWhispererChat] export const amazonQScopes = [...codeWhispererChatScopes, ...scopesGumby, ...scopesFeatureDev] -/** - * "Core" are the CW scopes that existed before the addition of new scopes - * for Amazon Q. - */ -export const isValidCodeWhispererCoreConnection = (conn?: Connection): conn is Connection => { - return ( - (isSageMaker() && isIamConnection(conn)) || (isSsoConnection(conn) && hasScopes(conn, codeWhispererCoreScopes)) - ) +/** AuthProvider interface for the auth functionality needed by RegionProfileManager */ +export interface IAuthProvider { + isConnected(): boolean + isBuilderIdConnection(): boolean + isIdcConnection(): boolean + isSsoSession(): boolean + getToken(): Promise + readonly profileName: string + readonly connection?: { region: string; startUrl: string } } -/** Superset that includes all of CodeWhisperer + Amazon Q */ -export const isValidAmazonQConnection = (conn?: Connection): conn is Connection => { - return ( - (isSageMaker() && isIamConnection(conn)) || - ((isSsoConnection(conn) || isBuilderIdConnection(conn)) && - isValidCodeWhispererCoreConnection(conn) && - hasScopes(conn, amazonQScopes)) - ) -} - -const authClassName = 'AuthQ' - -export class AuthUtil { - static #instance: AuthUtil - protected static readonly logIfChanged = onceChanged((s: string) => getLogger().info(s)) - private reauthenticatePromptShown: boolean = false - private _isCustomizationFeatureEnabled: boolean = false +/** + * Handles authentication within Amazon Q. + * Amazon Q only supports a single connection at a time. + */ +export class AuthUtil implements IAuthProvider { + public readonly profileName = VSCODE_EXTENSION_ID.amazonq + public readonly regionProfileManager: RegionProfileManager - // user should only see that screen once. - // TODO: move to memento - public hasAlreadySeenMigrationAuthScreen: boolean = false + // IAM login currently not supported + private session: SsoLogin - public get isCustomizationFeatureEnabled(): boolean { - return this._isCustomizationFeatureEnabled + static create(lspAuth: LanguageClientAuth) { + return (this.#instance ??= new this(lspAuth)) } - // This boolean controls whether the Select Customization node will be visible. A change to this value - // means that the old UX was wrong and must refresh the devTool tree. - public set isCustomizationFeatureEnabled(value: boolean) { - if (this._isCustomizationFeatureEnabled === value) { - return + static #instance: AuthUtil + public static get instance() { + if (!this.#instance) { + throw new ToolkitError('AuthUtil not ready. Was it initialized with a running LSP?') } - this._isCustomizationFeatureEnabled = value - void Commands.tryExecute('aws.amazonq.refreshStatusBar') + return this.#instance } - public readonly secondaryAuth = getSecondaryAuth( - this.auth, - 'codewhisperer', - 'Amazon Q', - isValidCodeWhispererCoreConnection - ) - public readonly restore = () => this.secondaryAuth.restoreConnection() - - public constructor( - public readonly auth = Auth.instance, - public readonly regionProfileManager = new RegionProfileManager(() => this.conn) - ) {} - - public initCodeWhispererHooks = once(() => { - this.auth.onDidChangeConnectionState(async (e) => { - getLogger().info(`codewhisperer: connection changed to ${e.state}: ${e.id}`) - if (e.state !== 'authenticating') { - await this.refreshCodeWhisperer() - } - - await this.setVscodeContextProps() - }) - - this.secondaryAuth.onDidChangeActiveConnection(async () => { - getLogger().info(`codewhisperer: active connection changed`) - if (this.isValidEnterpriseSsoInUse()) { - void vscode.commands.executeCommand('aws.amazonq.notifyNewCustomizations') - await this.regionProfileManager.restoreProfileSelection() - } - vsCodeState.isFreeTierLimitReached = false - await Promise.all([ - // onDidChangeActiveConnection may trigger before these modules are activated. - Commands.tryExecute('aws.amazonq.refreshStatusBar'), - Commands.tryExecute('aws.amazonq.updateReferenceLog'), - ]) - - await this.setVscodeContextProps() - - // To check valid connection - if (this.isValidEnterpriseSsoInUse() || (this.isBuilderIdInUse() && !this.isConnectionExpired())) { - await showAmazonQWalkthroughOnce() - } - - if (!this.isConnected()) { - await this.regionProfileManager.invalidateProfile(this.regionProfileManager.activeRegionProfile?.arn) - await this.regionProfileManager.clearCache() - } - }) + private constructor(private readonly lspAuth: LanguageClientAuth) { + this.session = new SsoLogin(this.profileName, this.lspAuth) + this.onDidChangeConnectionState((e: AuthStateEvent) => this.stateChangeHandler(e)) + this.regionProfileManager = new RegionProfileManager(this) this.regionProfileManager.onDidChangeRegionProfile(async () => { await this.setVscodeContextProps() }) - }) - - public async setVscodeContextProps() { - // if users are "pending profile selection", they're not fully connected and require profile selection for Q usage - // requireProfileSelection() always returns false for builderID users - await setContext('aws.codewhisperer.connected', this.isConnected() && !this.requireProfileSelection()) - const doShowAmazonQLoginView = - !this.isConnected() || this.isConnectionExpired() || this.requireProfileSelection() - await setContext('aws.amazonq.showLoginView', doShowAmazonQLoginView) - await setContext('aws.codewhisperer.connectionExpired', this.isConnectionExpired()) - await setContext('aws.amazonq.connectedSsoIdc', isIdcSsoConnection(this.conn)) - } - - public reformatStartUrl(startUrl: string | undefined) { - return !startUrl ? undefined : startUrl.replace(/[\/#]+$/g, '') - } - - // current active cwspr connection - public get conn() { - return this.secondaryAuth.activeConnection + lspAuth.registerCacheWatcher(async (event: cacheChangedEvent) => await this.cacheChangedHandler(event)) } - // TODO: move this to the shared auth.ts - public get startUrl(): string | undefined { - // Reformat the url to remove any trailing '/' and `#` - // e.g. https://view.awsapps.com/start/# will become https://view.awsapps.com/start - return isSsoConnection(this.conn) ? this.reformatStartUrl(this.conn?.startUrl) : undefined + // Do NOT use this in production code, only used for testing + static destroy(): void { + this.#instance = undefined as any } - public get isUsingSavedConnection() { - return this.conn !== undefined && this.secondaryAuth.hasSavedConnection + isSsoSession() { + return this.session.loginType === LoginTypes.SSO } - public isConnected(): boolean { - return this.conn !== undefined - } - - public isEnterpriseSsoInUse(): boolean { - const conn = this.conn - // we have an sso that isn't builder id, must be IdC by process of elimination - const isUsingEnterpriseSso = conn?.type === 'sso' && !isBuilderIdConnection(conn) - return conn !== undefined && isUsingEnterpriseSso + /** + * HACK: Ideally we'd put {@link notifySelectDeveloperProfile} in to {@link restore}. + * But because {@link refreshState} is only called if !isConnected, we cannot do it since + * {@link notifySelectDeveloperProfile} needs {@link refreshState} to run so it can set + * the Bearer Token in the LSP first. + */ + didStartSignedIn = false + + async restore() { + await this.session.restore() + this.didStartSignedIn = this.isConnected() + + // HACK: We noticed that if calling `refreshState()` here when the user was already signed in, something broke. + // So as a solution we only call it if they were not already signed in. + // + // But in the case where a user was already signed in, we allow `session.restore()` to trigger `refreshState()` through + // event emitters. + // This is unoptimal since `refreshState()` should be able to be called multiple times and still work. + // + // Because of this edge case, when `restore()` is called we cannot assume all Auth is setup when this function returns, + // since we may still be waiting on the event emitter to trigger the expected functions. + // + // TODO: Figure out why removing the if statement below causes things to break. Maybe we just need to + // promisify the call and any subsequent callers will not make a redundant call. + if (!this.didStartSignedIn) { + await this.refreshState() + } } - // If there is an active SSO connection - public isValidEnterpriseSsoInUse(): boolean { - return this.isEnterpriseSsoInUse() && !this.isConnectionExpired() - } + async login(startUrl: string, region: string) { + const response = await this.session.login({ startUrl, region, scopes: amazonQScopes }) + await showAmazonQWalkthroughOnce() - public isBuilderIdInUse(): boolean { - return this.conn !== undefined && isBuilderIdConnection(this.conn) + return response } - @withTelemetryContext({ name: 'connectToAwsBuilderId', class: authClassName }) - public async connectToAwsBuilderId(): Promise { - let conn = (await this.auth.listConnections()).find(isBuilderIdConnection) - - if (!conn) { - conn = await this.auth.createConnection(createBuilderIdProfile(amazonQScopes)) - } else if (!isValidAmazonQConnection(conn)) { - conn = await this.secondaryAuth.addScopes(conn, amazonQScopes) - } - - if (this.auth.getConnectionState(conn) === 'invalid') { - conn = await this.auth.reauthenticate(conn) + reauthenticate() { + if (!this.isSsoSession()) { + throw new ToolkitError('Cannot reauthenticate non-SSO session.') } - return (await this.secondaryAuth.useNewConnection(conn)) as SsoConnection + return this.session.reauthenticate() } - @withTelemetryContext({ name: 'connectToEnterpriseSso', class: authClassName }) - public async connectToEnterpriseSso(startUrl: string, region: string): Promise { - let conn = (await this.auth.listConnections()).find( - (conn): conn is SsoConnection => - isSsoConnection(conn) && conn.startUrl.toLowerCase() === startUrl.toLowerCase() - ) - - if (!conn) { - conn = await this.auth.createConnection(createSsoProfile(startUrl, region, amazonQScopes)) - } else if (!isValidAmazonQConnection(conn)) { - conn = await this.secondaryAuth.addScopes(conn, amazonQScopes) - } - - if (this.auth.getConnectionState(conn) === 'invalid') { - conn = await this.auth.reauthenticate(conn) + logout() { + if (!this.isSsoSession()) { + // Only SSO requires logout + return } - - return (await this.secondaryAuth.useNewConnection(conn)) as SsoConnection + this.lspAuth.deleteBearerToken() + return this.session.logout() } - public static get instance() { - if (this.#instance !== undefined) { - return this.#instance + async getToken() { + if (this.isSsoSession()) { + return (await this.session.getToken()).token + } else { + throw new ToolkitError('Cannot get token for non-SSO session.') } - - const self = (this.#instance = new this()) - return self } - @withTelemetryContext({ name: 'getBearerToken', class: authClassName }) - public async getBearerToken(): Promise { - await this.restore() - - if (this.conn === undefined) { - throw new ToolkitError('No connection found', { code: 'NoConnection' }) - } - - if (!isSsoConnection(this.conn)) { - throw new ToolkitError('Connection is not an SSO connection', { code: 'BadConnectionType' }) - } - - try { - const bearerToken = await this.conn.getToken() - return bearerToken.accessToken - } catch (err) { - if (err instanceof ProfileNotFoundError) { - // Expected that connection would be deleted by conn.getToken() - void focusAmazonQPanel.execute(placeholder, 'profileNotFoundSignout') - } - throw err - } + get connection() { + return this.session.data } - @withTelemetryContext({ name: 'getCredentials', class: authClassName }) - public async getCredentials() { - await this.restore() - - if (this.conn === undefined) { - throw new ToolkitError('No connection found', { code: 'NoConnection' }) - } - - if (!isIamConnection(this.conn)) { - throw new ToolkitError('Connection is not an IAM connection', { code: 'BadConnectionType' }) - } - - return this.conn.getCredentials() + getAuthState() { + return this.session.getConnectionState() } - public isConnectionValid(log: boolean = true): boolean { - const connectionValid = this.conn !== undefined && !this.secondaryAuth.isConnectionExpired - - if (log) { - this.logConnection() - } - - return connectionValid + isConnected() { + return this.getAuthState() === 'connected' } - public isConnectionExpired(log: boolean = true): boolean { - const connectionExpired = - this.secondaryAuth.isConnectionExpired && - this.conn !== undefined && - isValidCodeWhispererCoreConnection(this.conn) - - if (log) { - this.logConnection() - } - - return connectionExpired + isConnectionExpired() { + return this.getAuthState() === 'expired' } - requireProfileSelection(): boolean { - if (isBuilderIdConnection(this.conn)) { - return false - } - return isIdcSsoConnection(this.conn) && this.regionProfileManager.activeRegionProfile === undefined + isBuilderIdConnection() { + return this.connection?.startUrl === builderIdStartUrl } - private logConnection() { - const logStr = indent( - `codewhisperer: connection states - connection isValid=${this.isConnectionValid(false)}, - connection isValidCodewhispererCoreConnection=${isValidCodeWhispererCoreConnection(this.conn)}, - connection isExpired=${this.isConnectionExpired(false)}, - secondaryAuth isExpired=${this.secondaryAuth.isConnectionExpired}, - connection isUndefined=${this.conn === undefined}`, - 4, - true - ) - - AuthUtil.logIfChanged(logStr) + isIdcConnection() { + return Boolean(this.connection?.startUrl && this.connection?.startUrl !== builderIdStartUrl) } - @withTelemetryContext({ name: 'reauthenticate', class: authClassName }) - public async reauthenticate() { - try { - if (this.conn?.type !== 'sso') { - return - } - - if (!hasExactScopes(this.conn, amazonQScopes)) { - const conn = await setScopes(this.conn, amazonQScopes, this.auth) - await this.secondaryAuth.useNewConnection(conn) - } + isInternalAmazonUser(): boolean { + return this.isConnected() && this.connection?.startUrl === internalStartUrl + } - await this.auth.reauthenticate(this.conn) - } catch (err) { - throw ToolkitError.chain(err, 'Unable to authenticate connection') - } finally { - await this.setVscodeContextProps() - } + onDidChangeConnectionState(handler: (e: AuthStateEvent) => any) { + return this.session.onDidChangeConnectionState(handler) } - public async refreshCodeWhisperer() { - vsCodeState.isFreeTierLimitReached = false - await Commands.tryExecute('aws.amazonq.refreshStatusBar') + public async setVscodeContextProps(state = this.getAuthState()) { + await setContext('aws.codewhisperer.connected', state === 'connected') + const showAmazonQLoginView = + !this.isConnected() || this.isConnectionExpired() || this.regionProfileManager.requireProfileSelection() + await setContext('aws.amazonq.showLoginView', showAmazonQLoginView) + await setContext('aws.amazonq.connectedSsoIdc', this.isIdcConnection()) + await setContext('aws.codewhisperer.connectionExpired', state === 'expired') } - @withTelemetryContext({ name: 'showReauthenticatePrompt', class: authClassName }) + private reauthenticatePromptShown: boolean = false public async showReauthenticatePrompt(isAutoTrigger?: boolean) { if (isAutoTrigger && this.reauthenticatePromptShown) { return @@ -379,6 +225,26 @@ export class AuthUtil { } } + private _isCustomizationFeatureEnabled: boolean = false + public get isCustomizationFeatureEnabled(): boolean { + return this._isCustomizationFeatureEnabled + } + + // This boolean controls whether the Select Customization node will be visible. A change to this value + // means that the old UX was wrong and must refresh the devTool tree. + public set isCustomizationFeatureEnabled(value: boolean) { + if (this._isCustomizationFeatureEnabled === value) { + return + } + this._isCustomizationFeatureEnabled = value + void Commands.tryExecute('aws.amazonq.refreshStatusBar') + } + + public async notifyReauthenticate(isAutoTrigger?: boolean) { + void this.showReauthenticatePrompt(isAutoTrigger) + await this.setVscodeContextProps() + } + public async notifySessionConfiguration() { const suppressId = 'amazonQSessionConfigurationMessage' const settings = AmazonQPromptSettings.instance @@ -411,175 +277,188 @@ export class AuthUtil { }) } - @withTelemetryContext({ name: 'notifyReauthenticate', class: authClassName }) - public async notifyReauthenticate(isAutoTrigger?: boolean) { - void this.showReauthenticatePrompt(isAutoTrigger) - await this.setVscodeContextProps() - } - - public isValidCodeTransformationAuthUser(): boolean { - return (this.isEnterpriseSsoInUse() || this.isBuilderIdInUse()) && this.isConnectionValid() + private async cacheChangedHandler(event: cacheChangedEvent) { + if (event === 'delete') { + await this.logout() + } else if (event === 'create') { + await this.restore() + } } - /** - * Asynchronously returns a snapshot of the overall auth state of CodeWhisperer + Chat features. - * It guarantees the latest state is correct at the risk of modifying connection state. - * If this guarantee is not required, use sync method getChatAuthStateSync() - * - * By default, network errors are ignored when determining auth state since they may be silently - * recoverable later. - * - * THROTTLE: This function is called in rapid succession by Amazon Q features and can lead to - * a barrage of disk access and/or token refreshes. We throttle to deal with this. - * - * Note we do an explicit cast of the return type due to Lodash types incorrectly indicating - * a FeatureAuthState or undefined can be returned. But since we set `leading: true` - * it will always return FeatureAuthState - */ - public getChatAuthState = throttle(() => this._getChatAuthState(), 2000, { - leading: true, - }) as () => Promise - /** - * IMPORTANT: Only use this if you do NOT want to swallow network errors, otherwise use {@link getChatAuthState()} - * @param ignoreNetErr swallows network errors - */ - @withTelemetryContext({ name: 'getChatAuthState', class: authClassName }) - public async _getChatAuthState(ignoreNetErr: boolean = true): Promise { - // The state of the connection may not have been properly validated - // and the current state we see may be stale, so refresh for latest state. - if (ignoreNetErr) { - await tryRun( - () => this.auth.refreshConnectionState(this.conn), - (err) => !isNetworkError(err), - 'getChatAuthState: Cannot refresh connection state due to network error: %s' - ) + private async stateChangeHandler(e: AuthStateEvent) { + if (e.state === 'refreshed') { + const params = this.isSsoSession() ? (await this.session.getToken()).updateCredentialsParams : undefined + await this.lspAuth.updateBearerToken(params!) + return } else { - await this.auth.refreshConnectionState(this.conn) + getLogger().info(`codewhisperer: connection changed to ${e.state}`) + await this.refreshState(e.state) } - - return this.getChatAuthStateSync(this.conn) } - /** - * Synchronously returns a snapshot of the overall auth state of CodeWhisperer + Chat features without - * validating or modifying the connection state. It is possible that the connection - * is invalid/valid, but the current state displays something else. To guarantee the true state, - * use async method getChatAuthState() - */ - public getChatAuthStateSync(conn = this.conn): FeatureAuthState { - if (conn === undefined) { - return buildFeatureAuthState(AuthStates.disconnected) + private async refreshState(state = this.getAuthState()) { + if (state === 'expired' || state === 'notConnected') { + this.lspAuth.deleteBearerToken() + if (this.isIdcConnection()) { + await this.regionProfileManager.invalidateProfile(this.regionProfileManager.activeRegionProfile?.arn) + await this.regionProfileManager.clearCache() + } + } + if (state === 'connected') { + const bearerTokenParams = (await this.session.getToken()).updateCredentialsParams + await this.lspAuth.updateBearerToken(bearerTokenParams) + + if (this.isIdcConnection()) { + await this.regionProfileManager.restoreProfileSelection() + } } - if (!isSsoConnection(conn) && !isSageMaker()) { - throw new ToolkitError(`Connection "${conn.id}" is not a valid type: ${conn.type}`) + // regardless of state, send message at startup if user needs to select a Developer Profile + void this.tryNotifySelectDeveloperProfile() + + vsCodeState.isFreeTierLimitReached = false + await this.setVscodeContextProps(state) + await Promise.all([ + Commands.tryExecute('aws.amazonq.refreshStatusBar'), + Commands.tryExecute('aws.amazonq.updateReferenceLog'), + ]) + + if (state === 'connected' && this.isIdcConnection()) { + void vscode.commands.executeCommand('aws.amazonq.notifyNewCustomizations') } + } - // default to expired to indicate reauth is needed if unmodified - const state: FeatureAuthState = buildFeatureAuthState(AuthStates.expired) + private tryNotifySelectDeveloperProfile = once(async () => { + if (this.regionProfileManager.requireProfileSelection() && this.didStartSignedIn) { + await notifySelectDeveloperProfile() + } + }) - if (this.isConnectionExpired()) { - return state + async getTelemetryMetadata(): Promise { + if (!this.isConnected()) { + return { + id: 'undefined', + } } - if (isBuilderIdConnection(conn) || isIdcSsoConnection(conn) || isSageMaker()) { - // TODO: refactor - if (isValidCodeWhispererCoreConnection(conn)) { - if (this.requireProfileSelection()) { - state[Features.codewhispererCore] = AuthStates.pendingProfileSelection - } else { - state[Features.codewhispererCore] = AuthStates.connected - } + if (this.isSsoSession()) { + const ssoSessionDetails = (await this.session.getProfile()).ssoSession?.settings + return { + authScopes: ssoSessionDetails?.sso_registration_scopes?.join(','), + credentialSourceId: AuthUtil.instance.isBuilderIdConnection() ? 'awsId' : 'iamIdentityCenter', + credentialStartUrl: AuthUtil.instance.connection?.startUrl, + awsRegion: AuthUtil.instance.connection?.region, } - if (isValidAmazonQConnection(conn)) { - if (this.requireProfileSelection()) { - for (const v of Object.values(Features)) { - state[v as Feature] = AuthStates.pendingProfileSelection - } - } else { - for (const v of Object.values(Features)) { - state[v as Feature] = AuthStates.connected - } - } + } else if (!AuthUtil.instance.isSsoSession) { + return { + credentialSourceId: 'sharedCredentials', } } - return state + throw new Error('getTelemetryMetadataForConn() called with unknown connection type') } - /** - * Edge Case: Due to a change in behaviour/functionality, there are potential extra - * auth connections that the Amazon Q extension has cached. We need to remove these - * as they are irrelevant to the Q extension and can cause issues. - */ - public async clearExtraConnections(): Promise { - const currentQConn = this.conn - // Q currently only maintains 1 connection at a time, so we assume everything else is extra. - // IMPORTANT: In the case Q starts to manage multiple connections, this implementation will need to be updated. - const allOtherConnections = (await this.auth.listConnections()).filter((c) => c.id !== currentQConn?.id) - for (const conn of allOtherConnections) { - getLogger().warn(`forgetting extra amazon q connection: %O`, conn) - await telemetry.auth_modifyConnection.run( - async () => { - telemetry.record({ - connectionState: Auth.instance.getConnectionState(conn) ?? 'undefined', - source: asStringifiedStack(telemetry.getFunctionStack()), - ...(await getTelemetryMetadataForConn(conn)), - }) + async getAuthFormIds(): Promise { + if (!this.isConnected()) { + return [] + } - if (isInDevEnv()) { - telemetry.record({ action: 'forget' }) - // in a Dev Env the connection may be used by code catalyst, so we forget instead of fully deleting - await this.auth.forgetConnection(conn) - } else { - telemetry.record({ action: 'delete' }) - await this.auth.deleteConnection(conn) - } - }, - { functionId: { name: 'clearExtraConnections', class: authClassName } } - ) + const authIds: AuthFormId[] = [] + let connType: 'builderId' | 'identityCenter' + + // TODO: update when there is IAM support + if (!this.isSsoSession()) { + return ['credentials'] + } else if (this.isBuilderIdConnection()) { + connType = 'builderId' + } else if (this.isIdcConnection()) { + connType = 'identityCenter' + const ssoSessionDetails = (await this.session.getProfile()).ssoSession?.settings + if (hasScopes(ssoSessionDetails?.sso_registration_scopes ?? [], scopesSsoAccountAccess)) { + authIds.push('identityCenterExplorer') + } + } else { + return ['unknown'] } - } -} + authIds.push(`${connType}CodeWhisperer`) -export type FeatureAuthState = { [feature in Feature]: AuthState } -export type Feature = (typeof Features)[keyof typeof Features] -export type AuthState = (typeof AuthStates)[keyof typeof AuthStates] + return authIds + } -export const AuthStates = { - /** The current connection is working and supports this feature. */ - connected: 'connected', - /** No connection exists, so this feature cannot be used*/ - disconnected: 'disconnected', - /** - * The current connection exists, but needs to be reauthenticated for this feature to work - * - * Look to use {@link AuthUtil.reauthenticate()} - */ - expired: 'expired', /** - * A connection exists, but does not support this feature. + * Migrates existing SSO connections to the LSP identity server by updating the cache files * - * Eg: We are currently using Builder ID, but must use Identity Center. + * @param clientName - The client name to use for the new registration cache file + * @returns A Promise that resolves when the migration is complete + * @throws Error if file operations fail during migration */ - unsupported: 'unsupported', - /** - * The current connection exists and isn't expired, - * but fetching/refreshing the token resulted in a network error. - */ - connectedWithNetworkError: 'connectedWithNetworkError', - pendingProfileSelection: 'pendingProfileSelection', -} as const -const Features = { - codewhispererCore: 'codewhispererCore', - codewhispererChat: 'codewhispererChat', - amazonQ: 'amazonQ', -} as const - -function buildFeatureAuthState(state: AuthState): FeatureAuthState { - return { - codewhispererCore: state, - codewhispererChat: state, - amazonQ: state, + async migrateSsoConnectionToLsp(clientName: string) { + const memento = getEnvironmentSpecificMemento() + const key = 'auth.profiles' + const profiles: { readonly [id: string]: StoredProfile } | undefined = memento.get(key) + + let toImport: SsoProfile | undefined + let profileId: string | undefined + + if (!profiles) { + return + } else { + getLogger().info(`codewhisperer: checking for old SSO connections`) + for (const [id, p] of Object.entries(profiles)) { + if (p.type === 'sso' && hasExactScopes(p.scopes ?? [], amazonQScopes)) { + toImport = p + profileId = id + if (p.metadata.connectionState === 'valid') { + break + } + } + } + + if (toImport && profileId) { + getLogger().info(`codewhisperer: migrating SSO connection to LSP identity server...`) + + const registrationKey = { + startUrl: toImport.startUrl, + region: toImport.ssoRegion, + scopes: amazonQScopes, + } + + await this.session.updateProfile(registrationKey) + + const cacheDir = getCacheDir() + + const hash = (str: string) => { + const hasher = crypto.createHash('sha1') + return hasher.update(str).digest('hex') + } + const filePath = (str: string) => { + return path.join(cacheDir, hash(str) + '.json') + } + + const fromRegistrationFile = getRegistrationCacheFile(cacheDir, registrationKey) + const toRegistrationFile = filePath( + JSON.stringify({ + region: toImport.ssoRegion, + startUrl: toImport.startUrl, + tool: clientName, + }) + ) + + const fromTokenFile = getTokenCacheFile(cacheDir, profileId) + const toTokenFile = filePath(this.profileName) + + try { + await fs.rename(fromRegistrationFile, toRegistrationFile) + await fs.rename(fromTokenFile, toTokenFile) + getLogger().debug('Successfully renamed registration and token files') + } catch (err) { + getLogger().error(`Failed to rename files during migration: ${err}`) + throw err + } + + await memento.update(key, undefined) + getLogger().info(`codewhisperer: successfully migrated SSO connection to LSP identity server`) + } + } } } diff --git a/packages/core/src/codewhisperer/util/customizationUtil.ts b/packages/core/src/codewhisperer/util/customizationUtil.ts index 600317c53e0..ed3812166e4 100644 --- a/packages/core/src/codewhisperer/util/customizationUtil.ts +++ b/packages/core/src/codewhisperer/util/customizationUtil.ts @@ -157,20 +157,11 @@ export const baseCustomization = { * @returns customization selected by users, `baseCustomization` if none is selected */ export const getSelectedCustomization = (): Customization => { - if ( - !AuthUtil.instance.isCustomizationFeatureEnabled || - !AuthUtil.instance.isValidEnterpriseSsoInUse() || - !AuthUtil.instance.conn - ) { + if (!AuthUtil.instance.isCustomizationFeatureEnabled || !AuthUtil.instance.isIdcConnection()) { return baseCustomization } - const selectedCustomizationArr = globals.globalState.tryGet<{ [label: string]: Customization }>( - 'CODEWHISPERER_SELECTED_CUSTOMIZATION', - Object, - {} - ) - const selectedCustomization = selectedCustomizationArr[AuthUtil.instance.conn.label] + const selectedCustomization = globals.globalState.getAmazonQCustomization(AuthUtil.instance.profileName) if (selectedCustomization && selectedCustomization.name !== '') { return selectedCustomization @@ -187,7 +178,7 @@ export const getSelectedCustomization = (): Customization => { * 2. the override customization arn is different from the previous override customization if any. The purpose is to only do override once on users' behalf. */ export const setSelectedCustomization = async (customization: Customization, isOverride: boolean = false) => { - if (!AuthUtil.instance.isValidEnterpriseSsoInUse() || !AuthUtil.instance.conn) { + if (!AuthUtil.instance.isIdcConnection()) { return } if (isOverride) { @@ -196,15 +187,10 @@ export const setSelectedCustomization = async (customization: Customization, isO return } } - const selectedCustomizationObj = globals.globalState.tryGet<{ [label: string]: Customization }>( - 'CODEWHISPERER_SELECTED_CUSTOMIZATION', - Object, - {} - ) - selectedCustomizationObj[AuthUtil.instance.conn.label] = customization - getLogger().debug(`Selected customization ${customization.name} for ${AuthUtil.instance.conn.label}`) - await globals.globalState.update('CODEWHISPERER_SELECTED_CUSTOMIZATION', selectedCustomizationObj) + await globals.globalState.update('CODEWHISPERER_SELECTED_CUSTOMIZATION', customization) + getLogger().debug(`Selected customization ${customization.name} for ${AuthUtil.instance.profileName}`) + if (isOverride) { await globals.globalState.update('aws.amazonq.customization.overrideV2', customization.arn) } @@ -216,28 +202,18 @@ export const setSelectedCustomization = async (customization: Customization, isO } export const getPersistedCustomizations = (): Customization[] => { - if (!AuthUtil.instance.isValidEnterpriseSsoInUse() || !AuthUtil.instance.conn) { + if (!AuthUtil.instance.isIdcConnection()) { return [] } - const persistedCustomizationsObj = globals.globalState.tryGet<{ [label: string]: Customization[] }>( - 'CODEWHISPERER_PERSISTED_CUSTOMIZATIONS', - Object, - {} - ) - return persistedCustomizationsObj[AuthUtil.instance.conn.label] || [] + return globals.globalState.getAmazonQCachedCustomization(AuthUtil.instance.profileName) } export const setPersistedCustomizations = async (customizations: Customization[]) => { - if (!AuthUtil.instance.isValidEnterpriseSsoInUse() || !AuthUtil.instance.conn) { + if (!AuthUtil.instance.isIdcConnection()) { return } - const persistedCustomizationsObj = globals.globalState.tryGet<{ [label: string]: Customization[] }>( - 'CODEWHISPERER_PERSISTED_CUSTOMIZATIONS', - Object, - {} - ) - persistedCustomizationsObj[AuthUtil.instance.conn.label] = customizations - await globals.globalState.update('CODEWHISPERER_PERSISTED_CUSTOMIZATIONS', persistedCustomizationsObj) + + await globals.globalState.update('CODEWHISPERER_PERSISTED_CUSTOMIZATIONS', customizations) } export const getNewCustomizationsAvailable = () => { diff --git a/packages/core/src/codewhisperer/util/getStartUrl.ts b/packages/core/src/codewhisperer/util/getStartUrl.ts index 0d9ca7617ff..f1db38f5f1f 100644 --- a/packages/core/src/codewhisperer/util/getStartUrl.ts +++ b/packages/core/src/codewhisperer/util/getStartUrl.ts @@ -28,9 +28,8 @@ export const getStartUrl = async () => { } export async function connectToEnterpriseSso(startUrl: string, region: Region['id']) { - let conn try { - conn = await AuthUtil.instance.connectToEnterpriseSso(startUrl, region) + await AuthUtil.instance.login(startUrl, region) } catch (e) { throw ToolkitError.chain(e, CodeWhispererConstants.failedToConnectIamIdentityCenter, { code: 'FailedToConnect', @@ -38,6 +37,4 @@ export async function connectToEnterpriseSso(startUrl: string, region: Region['i } vsCodeState.isFreeTierLimitReached = false await Commands.tryExecute('aws.amazonq.enableCodeSuggestions') - - return conn } diff --git a/packages/core/src/codewhisperer/util/showSsoPrompt.ts b/packages/core/src/codewhisperer/util/showSsoPrompt.ts index 13af4cf771f..b3d78654745 100644 --- a/packages/core/src/codewhisperer/util/showSsoPrompt.ts +++ b/packages/core/src/codewhisperer/util/showSsoPrompt.ts @@ -17,6 +17,7 @@ import { telemetry } from '../../shared/telemetry/telemetry' import { createBuilderIdItem, createSsoItem, createIamItem } from '../../auth/utils' import { Commands } from '../../shared/vscode/commands2' import { vsCodeState } from '../models/model' +import { builderIdRegion, builderIdStartUrl } from '../../auth/sso/constants' export const showCodeWhispererConnectionPrompt = async () => { const items = [createBuilderIdItem(), createSsoItem(), createCodeWhispererIamItem()] @@ -45,16 +46,13 @@ export const showCodeWhispererConnectionPrompt = async () => { export async function awsIdSignIn() { getLogger().info('selected AWS ID sign in') - let conn try { - conn = await AuthUtil.instance.connectToAwsBuilderId() + await AuthUtil.instance.login(builderIdStartUrl, builderIdRegion) } catch (e) { throw ToolkitError.chain(e, failedToConnectAwsBuilderId, { code: 'FailedToConnect' }) } vsCodeState.isFreeTierLimitReached = false await Commands.tryExecute('aws.amazonq.enableCodeSuggestions') - - return conn } export const createCodeWhispererIamItem = () => { diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index 060a5ecb282..bb4e62cfaba 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -101,7 +101,7 @@ export class TelemetryHelper { artifactsUploadDuration: artifactsUploadDuration, buildPayloadBytes: buildPayloadBytes, buildZipFileBytes: buildZipFileBytes, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, acceptedCharactersCount: acceptedCharactersCount, acceptedCount: acceptedCount, acceptedLinesCount: acceptedLinesCount, @@ -146,7 +146,7 @@ export class TelemetryHelper { codewhispererSupplementalContextLength: supplementalContextMetadata?.contentsLength, codewhispererSupplementalContextTimeout: supplementalContextMetadata?.isProcessTimeout, codewhispererTriggerType: session.triggerType, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, duration: duration || 0, reason: reason ? reason.substring(0, 200) : undefined, result, @@ -196,7 +196,7 @@ export class TelemetryHelper { codewhispererTimeToFirstRecommendation: session.timeToFirstRecommendation, codewhispererTriggerType: session.triggerType, codewhispererTypeaheadLength: this.typeAheadLength, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, traceId: this.traceId, }) @@ -283,7 +283,7 @@ export class TelemetryHelper { codewhispererSupplementalContextLength: supplementalContextMetadata?.contentsLength, codewhispererSupplementalContextTimeout: supplementalContextMetadata?.isProcessTimeout, codewhispererTriggerType: session.triggerType, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, traceId: this.traceId, } events.push(event) @@ -658,7 +658,7 @@ export class TelemetryHelper { codewhispererRequestId: this._firstResponseRequestId, codewhispererSessionId: session.sessionId, codewhispererTriggerType: session.triggerType, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) } public sendCodeScanEvent(languageId: string, jobId: string) { diff --git a/packages/core/src/codewhisperer/views/activeStateController.ts b/packages/core/src/codewhisperer/views/activeStateController.ts index b3c991a9d38..5f50c23a2b5 100644 --- a/packages/core/src/codewhisperer/views/activeStateController.ts +++ b/packages/core/src/codewhisperer/views/activeStateController.ts @@ -48,12 +48,7 @@ export class ActiveStateController implements vscode.Disposable { subscribeOnce(this.container.lineTracker.onReady)(async (_) => { await this.onReady() }), - this.container.auth.auth.onDidChangeConnectionState(async (e) => { - if (e.state !== 'authenticating') { - await this._refresh(vscode.window.activeTextEditor) - } - }), - this.container.auth.secondaryAuth.onDidChangeActiveConnection(async () => { + this.container.auth.onDidChangeConnectionState(async (e) => { await this._refresh(vscode.window.activeTextEditor) }) ) @@ -139,7 +134,7 @@ export class ActiveStateController implements vscode.Disposable { return } - if (!this.container.auth.isConnectionValid()) { + if (!this.container.auth.isConnected()) { this.clear(this._editor) return } diff --git a/packages/core/src/codewhisperer/views/lineAnnotationController.ts b/packages/core/src/codewhisperer/views/lineAnnotationController.ts index 8b1d38ed7ae..3faee7dde1e 100644 --- a/packages/core/src/codewhisperer/views/lineAnnotationController.ts +++ b/packages/core/src/codewhisperer/views/lineAnnotationController.ts @@ -293,12 +293,7 @@ export class LineAnnotationController implements vscode.Disposable { this.container.lineTracker.onDidChangeActiveLines(async (e) => { await this.onActiveLinesChanged(e) }), - this.container.auth.auth.onDidChangeConnectionState(async (e) => { - if (e.state !== 'authenticating') { - await this.refresh(vscode.window.activeTextEditor, 'editor') - } - }), - this.container.auth.secondaryAuth.onDidChangeActiveConnection(async () => { + this.container.auth.onDidChangeConnectionState(async () => { await this.refresh(vscode.window.activeTextEditor, 'editor') }), Commands.register('aws.amazonq.dismissTutorial', async () => { @@ -424,7 +419,7 @@ export class LineAnnotationController implements vscode.Disposable { return } - if (!this.container.auth.isConnectionValid()) { + if (!this.container.auth.isConnected()) { this.clear() return } diff --git a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts index d511bd9a5f6..0426c853d9b 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts +++ b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts @@ -135,7 +135,7 @@ export class SecurityIssueWebview extends VueWebview { ruleId: this.issue!.ruleId, component: 'webview', result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, codeFixAction: fixAction, } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index ba2072eb6dc..8eb704535ca 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -65,7 +65,6 @@ import { getSelectedCustomization } from '../../../codewhisperer/util/customizat import { getHttpStatusCode, AwsClientResponseError } from '../../../shared/errors' import { uiEventRecorder } from '../../../amazonq/util/eventRecorder' import { telemetry } from '../../../shared/telemetry/telemetry' -import { isSsoConnection } from '../../../auth/connection' import { inspect } from '../../../shared/utilities/collectionUtils' import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' @@ -967,9 +966,9 @@ export class ChatController { const tabID = triggerEvent.tabID - const credentialsState = await AuthUtil.instance.getChatAuthState() + const credentialsState = AuthUtil.instance.getAuthState() - if (credentialsState.codewhispererChat !== 'connected' && credentialsState.codewhispererCore !== 'connected') { + if (credentialsState !== 'connected') { await this.messenger.sendAuthNeededExceptionMessage(credentialsState, tabID, triggerID) return } @@ -1109,11 +1108,9 @@ export class ChatController { const tabID = triggerEvent.tabID - const credentialsState = await AuthUtil.instance.getChatAuthState() + const credentialsState = AuthUtil.instance.getAuthState() - if ( - !(credentialsState.codewhispererChat === 'connected' && credentialsState.codewhispererCore === 'connected') - ) { + if (!(credentialsState === 'connected')) { await this.messenger.sendAuthNeededExceptionMessage(credentialsState, tabID, triggerID) return } @@ -1193,7 +1190,7 @@ export class ChatController { try { this.messenger.sendInitalStream(tabID, triggerID, triggerPayload.documentReferences) this.telemetryHelper.setConversationStreamStartTime(tabID) - if (isSsoConnection(AuthUtil.instance.conn)) { + if (AuthUtil.instance.isConnected() && AuthUtil.instance.isSsoSession()) { const { $metadata, generateAssistantResponseResponse } = await session.chatSso(request) response = { $metadata: $metadata, diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 8c914686ad4..76fe510b564 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -36,7 +36,6 @@ import { ChatPromptCommandType, DocumentReference, TriggerPayload } from '../mod import { getHttpStatusCode, getRequestId, ToolkitError } from '../../../../shared/errors' import { keys } from '../../../../shared/utilities/tsUtils' import { getLogger } from '../../../../shared/logger/logger' -import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' import { CodeScanIssue } from '../../../../codewhisperer/models/model' import { marked } from 'marked' import { JSDOM } from 'jsdom' @@ -47,6 +46,7 @@ import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants' import { ChatItem, ChatItemButton, ChatItemFormItem, DetailedList, MynahUIDataModel } from '@aws/mynah-ui' import { Database } from '../../../../shared/db/chatDb/chatDb' import { TabType } from '../../../../amazonq/webview/ui/storages/tabsStorage' +import { AuthState } from '../../../../auth/auth2' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -63,7 +63,7 @@ export class Messenger { private readonly telemetryHelper: CWCTelemetryHelper ) {} - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string, triggerID: string) { + public async sendAuthNeededExceptionMessage(credentialState: AuthState, tabID: string, triggerID: string) { const { message, authType } = extractAuthFollowUp(credentialState) this.dispatcher.sendAuthNeededExceptionMessage( new AuthNeededException( diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 2d9e01db9a0..292d00f46fc 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -71,7 +71,7 @@ export function recordTelemetryChatRunCommand(type: CwsprChatCommandType, comman result: 'Succeeded', cwsprChatCommandType: type, cwsprChatCommandName: command, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) } @@ -255,7 +255,7 @@ export class CWCTelemetryHelper { event = { result, cwsprChatConversationId: conversationId ?? '', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatMessageId: message.messageId, cwsprChatUserIntent: this.getUserIntentForTelemetry(message.userIntent), cwsprChatInteractionType: 'insertAtCursor', @@ -273,7 +273,7 @@ export class CWCTelemetryHelper { event = { result, cwsprChatConversationId: conversationId ?? '', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatMessageId: message.messageId, cwsprChatUserIntent: this.getUserIntentForTelemetry(message.userIntent), cwsprChatInteractionType: 'copySnippet', @@ -292,7 +292,7 @@ export class CWCTelemetryHelper { cwsprChatConversationId: conversationId ?? '', cwsprChatMessageId: message.messageId, cwsprChatInteractionType: 'acceptDiff', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatAcceptedCharactersLength: message.code.length, cwsprChatHasReference: message.referenceTrackerInformation && message.referenceTrackerInformation.length > 0, @@ -307,7 +307,7 @@ export class CWCTelemetryHelper { cwsprChatConversationId: conversationId ?? '', cwsprChatMessageId: message.messageId, cwsprChatInteractionType: 'viewDiff', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatAcceptedCharactersLength: message.code.length, cwsprChatHasReference: message.referenceTrackerInformation && message.referenceTrackerInformation.length > 0, @@ -320,7 +320,7 @@ export class CWCTelemetryHelper { event = { result, cwsprChatConversationId: conversationId ?? '', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatMessageId: message.messageId, cwsprChatInteractionType: 'clickFollowUp', } @@ -331,7 +331,7 @@ export class CWCTelemetryHelper { result, cwsprChatMessageId: message.messageId, cwsprChatConversationId: conversationId ?? '', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatInteractionType: message.vote, } break @@ -341,7 +341,7 @@ export class CWCTelemetryHelper { result, cwsprChatMessageId: message.messageId, cwsprChatConversationId: conversationId ?? '', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatInteractionType: 'clickLink', cwsprChatInteractionTarget: message.link, } @@ -352,7 +352,7 @@ export class CWCTelemetryHelper { result, cwsprChatMessageId: message.messageId, cwsprChatConversationId: conversationId ?? '', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatInteractionType: 'clickBodyLink', cwsprChatInteractionTarget: message.link, } @@ -363,7 +363,7 @@ export class CWCTelemetryHelper { result, cwsprChatMessageId: 'footer', cwsprChatConversationId: conversationId ?? '', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatInteractionType: 'clickBodyLink', cwsprChatInteractionTarget: message.link, } @@ -479,7 +479,7 @@ export class CWCTelemetryHelper { cwsprChatUserIntent: telemetryUserIntent, cwsprChatHasCodeSnippet: triggerPayload.codeSelection && !triggerPayload.codeSelection.isEmpty, cwsprChatProgrammingLanguage: triggerPayload.fileLanguage, - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, cwsprChatHasProjectContext: triggerPayload.relevantTextDocuments ? triggerPayload.relevantTextDocuments.length > 0 && triggerPayload.useRelevantDocuments === true : false, @@ -567,7 +567,7 @@ export class CWCTelemetryHelper { cwsprChatRequestLength: triggerPayload.message.length, cwsprChatResponseLength: message.messageLength, cwsprChatConversationType: 'Chat', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, codewhispererCustomizationArn: triggerPayload.customization.arn, cwsprChatHasProjectContext: hasProjectLevelContext, cwsprChatHasContextList: triggerPayload.documentReferences.length > 0, @@ -641,7 +641,7 @@ export class CWCTelemetryHelper { cwsprChatResponseCode: responseCode, cwsprChatRequestLength: triggerPayload.message?.length ?? 0, cwsprChatConversationType: 'Chat', - credentialStartUrl: AuthUtil.instance.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, }) } diff --git a/packages/core/src/codewhispererChat/editor/codelens.ts b/packages/core/src/codewhispererChat/editor/codelens.ts index 4df72d776d6..4e671315fc4 100644 --- a/packages/core/src/codewhispererChat/editor/codelens.ts +++ b/packages/core/src/codewhispererChat/editor/codelens.ts @@ -8,7 +8,7 @@ import { ToolkitError } from '../../shared/errors' import { Commands, placeholder } from '../../shared/vscode/commands2' import { platform } from 'os' import { focusAmazonQPanel } from '../commands/registerCommands' -import { AuthStates, AuthUtil } from '../../codewhisperer/util/authUtil' +import { AuthUtil } from '../../codewhisperer/util/authUtil' import { inlinehintKey } from '../../codewhisperer/models/constants' import { EndState } from '../../codewhisperer/views/lineAnnotationController' @@ -89,7 +89,7 @@ export class TryChatCodeLensProvider implements vscode.CodeLensProvider { return resolve([]) } - if (AuthUtil.instance.getChatAuthStateSync().amazonQ !== AuthStates.connected) { + if (AuthUtil.instance.getAuthState() !== 'connected') { return resolve([]) } diff --git a/packages/core/src/login/webview/commonAuthViewProvider.ts b/packages/core/src/login/webview/commonAuthViewProvider.ts index f805d7cf759..639267c5fc0 100644 --- a/packages/core/src/login/webview/commonAuthViewProvider.ts +++ b/packages/core/src/login/webview/commonAuthViewProvider.ts @@ -44,9 +44,8 @@ import { CodeCatalystAuthenticationProvider } from '../../codecatalyst/auth' import { telemetry } from '../../shared/telemetry/telemetry' import { AuthSources } from './util' import { AuthFlowStates } from './vue/types' -import { getTelemetryMetadataForConn } from '../../auth/connection' -import { AuthUtil } from '../../codewhisperer/util/authUtil' import { ExtensionUse } from '../../auth/utils' +import { AuthUtil } from '../../codewhisperer/util/authUtil' export class CommonAuthViewProvider implements WebviewViewProvider { public readonly viewType: string @@ -109,7 +108,7 @@ export class CommonAuthViewProvider implements WebviewViewProvider { if (authState === AuthFlowStates.REAUTHNEEDED || authState === AuthFlowStates.REAUTHENTICATING) { this.webView!.server.storeMetricMetadata({ isReAuth: true, - ...(await getTelemetryMetadataForConn(AuthUtil.instance.conn)), + ...(await AuthUtil.instance.getTelemetryMetadata()), }) } else { this.webView!.server.storeMetricMetadata({ isReAuth: false }) diff --git a/packages/core/src/login/webview/index.ts b/packages/core/src/login/webview/index.ts index 80abcc4fd79..d7760c89d4c 100644 --- a/packages/core/src/login/webview/index.ts +++ b/packages/core/src/login/webview/index.ts @@ -5,3 +5,4 @@ export { CommonAuthViewProvider } from './commonAuthViewProvider' export { CommonAuthWebview } from './vue/backend' +export * as backendAmazonQ from './vue/amazonq/backend_amazonq' diff --git a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts index a83e99d04b7..0a9dd576d6f 100644 --- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts +++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts @@ -3,13 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { - AwsConnection, - Connection, - SsoConnection, - getTelemetryMetadataForConn, - isSsoConnection, -} from '../../../../auth/connection' +import { AwsConnection, SsoConnection } from '../../../../auth/connection' import { AuthUtil } from '../../../../codewhisperer/util/authUtil' import { CommonAuthWebview } from '../backend' import { awsIdSignIn } from '../../../../codewhisperer/util/showSsoPrompt' @@ -70,9 +64,8 @@ export class AmazonQLoginWebview extends CommonAuthWebview { authEnabledFeatures: 'codewhisperer', isReAuth: false, }) - - const conn = await awsIdSignIn() - this.storeMetricMetadata(await getTelemetryMetadataForConn(conn)) + await awsIdSignIn() + this.storeMetricMetadata(await AuthUtil.instance.getTelemetryMetadata()) void vscode.window.showInformationMessage('AmazonQ: Successfully connected to AWS Builder ID') }) @@ -92,8 +85,8 @@ export class AmazonQLoginWebview extends CommonAuthWebview { isReAuth: false, }) - const conn = await connectToEnterpriseSso(startUrl, region) - this.storeMetricMetadata(await getTelemetryMetadataForConn(conn)) + await connectToEnterpriseSso(startUrl, region) + this.storeMetricMetadata(await AuthUtil.instance.getTelemetryMetadata()) void vscode.window.showInformationMessage('AmazonQ: Successfully connected to AWS IAM Identity Center') }) @@ -109,9 +102,7 @@ export class AmazonQLoginWebview extends CommonAuthWebview { getLogger().error('amazon Q reauthenticate called on a non-existant connection') throw new ToolkitError('Cannot reauthenticate non-existant connection.') } - - const conn = AuthUtil.instance.conn - if (!isSsoConnection(conn)) { + if (!AuthUtil.instance.isSsoSession()) { getLogger().error('amazon Q reauthenticate called, but the connection is not SSO') throw new ToolkitError('Cannot reauthenticate non-SSO connection.') } @@ -122,14 +113,12 @@ export class AmazonQLoginWebview extends CommonAuthWebview { */ this.reauthError = await this.ssoSetup('reauthenticateAmazonQ', async () => { this.storeMetricMetadata({ - authEnabledFeatures: this.getAuthEnabledFeatures(conn), + authEnabledFeatures: 'codewhisperer', isReAuth: true, - ...(await getTelemetryMetadataForConn(conn)), + ...(await AuthUtil.instance.getTelemetryMetadata()), }) await AuthUtil.instance.reauthenticate() - this.storeMetricMetadata({ - ...(await getTelemetryMetadataForConn(conn)), - }) + this.storeMetricMetadata(await AuthUtil.instance.getTelemetryMetadata()) }) } finally { this.isReauthenticating = false @@ -147,10 +136,6 @@ export class AmazonQLoginWebview extends CommonAuthWebview { return this.reauthError } - async getActiveConnection(): Promise { - return AuthUtil.instance.conn - } - /** * `true` if the actual reauth flow is in progress. * @@ -162,11 +147,13 @@ export class AmazonQLoginWebview extends CommonAuthWebview { isReauthenticating: boolean = false private authState: AuthFlowState = 'LOGIN' override async refreshAuthState(): Promise { - const featureAuthStates = await AuthUtil.instance.getChatAuthState() - if (featureAuthStates.amazonQ === 'expired') { + if (AuthUtil.instance.getAuthState() === 'expired') { this.authState = this.isReauthenticating ? 'REAUTHENTICATING' : 'REAUTHNEEDED' return - } else if (featureAuthStates.amazonQ === 'pendingProfileSelection') { + } else if ( + AuthUtil.instance.isConnected() && + AuthUtil.instance.regionProfileManager.requireProfileSelection() + ) { this.authState = 'PENDING_PROFILE_SELECTION' // possible that user starts with "profile selection" state therefore the timeout for auth flow should be disposed otherwise will emit failure this.loadMetadata?.loadTimeout?.dispose() @@ -186,19 +173,18 @@ export class AmazonQLoginWebview extends CommonAuthWebview { @withTelemetryContext({ name: 'signout', class: className }) override async signout(): Promise { - const conn = AuthUtil.instance.secondaryAuth.activeConnection - if (!isSsoConnection(conn)) { - throw new ToolkitError(`Cannot signout non-SSO connection, type is: ${conn?.type}`) + if (!AuthUtil.instance.isSsoSession()) { + throw new ToolkitError(`Cannot signout non-SSO connection`) } this.storeMetricMetadata({ - authEnabledFeatures: this.getAuthEnabledFeatures(conn), + authEnabledFeatures: 'codewhisperer', isReAuth: true, - ...(await getTelemetryMetadataForConn(conn)), + ...(await AuthUtil.instance.getTelemetryMetadata()), result: 'Cancelled', }) - await AuthUtil.instance.secondaryAuth.deleteConnection() + await AuthUtil.instance.logout() this.reauthError = undefined this.emitAuthMetric() @@ -229,13 +215,12 @@ export class AmazonQLoginWebview extends CommonAuthWebview { try { return await AuthUtil.instance.regionProfileManager.getProfiles() } catch (e) { - const conn = AuthUtil.instance.conn as SsoConnection | undefined telemetry.amazonq_didSelectProfile.emit({ source: 'auth', amazonQProfileRegion: AuthUtil.instance.regionProfileManager.activeRegionProfile?.region ?? 'not-set', - ssoRegion: conn?.ssoRegion, + ssoRegion: AuthUtil.instance.connection?.region, result: 'Failed', - credentialStartUrl: conn?.startUrl, + credentialStartUrl: AuthUtil.instance.connection?.startUrl, reason: (e as Error).message, }) @@ -250,7 +235,7 @@ export class AmazonQLoginWebview extends CommonAuthWebview { private setupConnectionEventEmitter(): void { // allows the frontend to listen to Amazon Q auth events from the backend const codeWhispererConnectionChanged = createThrottle(() => this.onActiveConnectionModified.fire()) - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(codeWhispererConnectionChanged) + AuthUtil.instance.onDidChangeConnectionState(codeWhispererConnectionChanged) AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(codeWhispererConnectionChanged) /** diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts index c8f1f38d4d7..edb1980a8c0 100644 --- a/packages/core/src/login/webview/vue/backend.ts +++ b/packages/core/src/login/webview/vue/backend.ts @@ -14,7 +14,6 @@ import { handleWebviewError } from '../../../webviews/server' import { InvalidGrantException } from '@aws-sdk/client-sso-oidc' import { AwsConnection, - Connection, hasScopes, scopesCodeCatalyst, scopesCodeWhispererChat, @@ -196,8 +195,6 @@ export abstract class CommonAuthWebview extends VueWebview { abstract reauthenticateConnection(): Promise abstract getReauthError(): Promise - abstract getActiveConnection(): Promise - /** Refreshes the current state of the auth flow, determining what you see in the UI */ abstract refreshAuthState(): Promise /** Use {@link refreshAuthState} first to ensure this returns the latest state */ diff --git a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts index 4e4db35b9ad..caec2c764bc 100644 --- a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts +++ b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts @@ -9,7 +9,6 @@ import { getLogger } from '../../../../shared/logger/logger' import { CommonAuthWebview } from '../backend' import { AwsConnection, - Connection, SsoConnection, TelemetryMetadata, createSsoProfile, @@ -161,9 +160,6 @@ export class ToolkitLoginWebview extends CommonAuthWebview { override reauthenticateConnection(): Promise { throw new Error('Method not implemented.') } - override getActiveConnection(): Promise { - throw new Error('Method not implemented.') - } override async refreshAuthState(): Promise {} override async getAuthState(): Promise { diff --git a/packages/core/src/shared/clients/codewhispererChatClient.ts b/packages/core/src/shared/clients/codewhispererChatClient.ts index 93b7b3bceb4..1210adb7e30 100644 --- a/packages/core/src/shared/clients/codewhispererChatClient.ts +++ b/packages/core/src/shared/clients/codewhispererChatClient.ts @@ -10,7 +10,7 @@ import { getUserAgent } from '../telemetry/util' // Create a client for featureDev streaming based off of aws sdk v3 export async function createCodeWhispererChatStreamingClient(): Promise { - const bearerToken = await AuthUtil.instance.getBearerToken() + const bearerToken = await AuthUtil.instance.getToken() const cwsprConfig = getCodewhispererConfig() const streamingClient = new CodeWhispererStreaming({ region: cwsprConfig.region, diff --git a/packages/core/src/shared/clients/qDeveloperChatClient.ts b/packages/core/src/shared/clients/qDeveloperChatClient.ts index ee98a78e356..d9344b5b406 100644 --- a/packages/core/src/shared/clients/qDeveloperChatClient.ts +++ b/packages/core/src/shared/clients/qDeveloperChatClient.ts @@ -6,12 +6,13 @@ import { QDeveloperStreaming } from '@amzn/amazon-q-developer-streaming-client' import { getCodewhispererConfig } from '../../codewhisperer/client/codewhisperer' import { getUserAgent } from '../telemetry/util' import { ConfiguredRetryStrategy } from '@smithy/util-retry' -import { AuthUtil } from '../../codewhisperer/util/authUtil' // Create a client for featureDev streaming based off of aws sdk v3 export async function createQDeveloperStreamingClient(): Promise { + throw new Error('Do not call this function until IAM is supported by LSP identity server') + const cwsprConfig = getCodewhispererConfig() - const credentials = await AuthUtil.instance.getCredentials() + const credentials = undefined const streamingClient = new QDeveloperStreaming({ region: cwsprConfig.region, endpoint: cwsprConfig.endpoint, diff --git a/packages/core/src/shared/featureConfig.ts b/packages/core/src/shared/featureConfig.ts index d7acb9657be..7cc6a9cbfc7 100644 --- a/packages/core/src/shared/featureConfig.ts +++ b/packages/core/src/shared/featureConfig.ts @@ -13,7 +13,6 @@ import * as nls from 'vscode-nls' import { codeWhispererClient as client } from '../codewhisperer/client/codewhisperer' import { AuthUtil } from '../codewhisperer/util/authUtil' import { getLogger } from './logger/logger' -import { isBuilderIdConnection, isIdcSsoConnection } from '../auth/connection' import { CodeWhispererSettings } from '../codewhisperer/util/codewhispererSettings' import globals from './extensionGlobals' import { getClientId, getOperatingSystem } from './telemetry/util' @@ -149,9 +148,9 @@ export class FeatureConfigProvider { const previousOverride = globals.globalState.tryGet('aws.amazonq.customization.overrideV2', String) if (customizationArnOverride !== undefined && customizationArnOverride !== previousOverride) { // Double check if server-side wrongly returns a customizationArn to BID users - if (isBuilderIdConnection(AuthUtil.instance.conn)) { + if (AuthUtil.instance.isBuilderIdConnection()) { this.featureConfigs.delete(Features.customizationArnOverride) - } else if (isIdcSsoConnection(AuthUtil.instance.conn)) { + } else if (AuthUtil.instance.isIdcConnection()) { const availableCustomizations = await getAvailableCustomizationsList() // If customizationArn from A/B is not available in listAvailableCustomizations response, don't use this value diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts index 13db46b430a..65d761412b8 100644 --- a/packages/core/src/shared/globalState.ts +++ b/packages/core/src/shared/globalState.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode' import { getLogger } from './logger/logger' import * as redshift from '../awsService/redshift/models/models' import { TypeConstructor, cast } from './utilities/typeConstructors' +import { Customization } from '../codewhisperer/client/codewhispereruserclient' type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId' export type ToolIdStateKey = `${ToolId}.savedConnectionId` @@ -229,6 +230,53 @@ export class GlobalState implements vscode.Memento { return all?.[warehouseArn] } + /** + * Get the Amazon Q customization. If legacy (map of customizations) store the + * customization with label of profile name + * + * @param profileName name of profile, only used in case legacy customization is found + * @returns Amazon Q customization, or undefined if not found. + * If legacy, return the Amazon Q customization for the auth profile name + */ + getAmazonQCustomization(profileName: string): Customization | undefined { + const result = this.tryGet('CODEWHISPERER_SELECTED_CUSTOMIZATION', Object, undefined) + + // Legacy migration for old customization map of type { [label: string]: Customization[] } + if (typeof result === 'object' && Object.values(result).every(Array.isArray)) { + const selectedCustomization = result[profileName] + this.tryUpdate('CODEWHISPERER_SELECTED_CUSTOMIZATION', selectedCustomization) + return selectedCustomization + } else { + return result + } + } + + /** + * Get the Amazon Q cached customizations. If legacy (map of customizations) store the + * customizations with label of profile name + * + * @param profileName name of profile, only used in case legacy customization is found + * @returns array of Amazon Q cached customizations, or empty array if not found. + * If legacy, return the Amazon Q persisted customizations for the auth profile name + */ + getAmazonQCachedCustomization(profileName: string): Customization[] { + const result = this.tryGet('CODEWHISPERER_PERSISTED_CUSTOMIZATIONS', Array, []) + + // Legacy migration for old customization map of type { [label: string]: Customization[] } + if (result.length === 0) { + const customizations = this.tryGet<{ [label: string]: Customization[] }>( + 'CODEWHISPERER_PERSISTED_CUSTOMIZATIONS', + Object, + {} + ) + const cachedCustomizationsArray = customizations[profileName] || [] + this.tryUpdate('CODEWHISPERER_PERSISTED_CUSTOMIZATIONS', cachedCustomizationsArray) + return cachedCustomizationsArray + } else { + return result + } + } + /** * Sets SSO session creation timestamp for the given session `id`. * @@ -270,3 +318,68 @@ export class GlobalState implements vscode.Memento { return all?.[id] } } + +export interface GlobalStatePollerProps { + getState: () => any + changeHandler: () => void + pollIntervalInMs: number +} + +/** + * Utility class that polls a state value at regular intervals and triggers a callback when the state changes. + * + * This class can be used to monitor changes in global state and react to those changes. + */ +export class GlobalStatePoller { + protected oldValue: any + protected pollIntervalInMs: number + protected getState: () => any + protected changeHandler: () => void + protected intervalId?: NodeJS.Timeout + + constructor(props: GlobalStatePollerProps) { + this.getState = props.getState + this.changeHandler = props.changeHandler + this.pollIntervalInMs = props.pollIntervalInMs + this.oldValue = this.getState() + } + + /** + * Factory method that creates and starts a GlobalStatePoller instance. + * + * @param getState - Function that returns the current state value to monitor, e.g. globals.globalState.tryGet + * @param changeHandler - Callback function that is invoked when the state changes + * @returns A new GlobalStatePoller instance that has already started polling + */ + static create(props: GlobalStatePollerProps) { + const instance = new GlobalStatePoller(props) + instance.poll() + return instance + } + + /** + * Starts polling the state value. When a change is detected, the changeHandler callback is invoked. + */ + private poll() { + if (this.intervalId) { + this.kill() + } + this.intervalId = setInterval(() => { + const newValue = this.getState() + if (this.oldValue !== newValue) { + this.oldValue = newValue + this.changeHandler() + } + }, this.pollIntervalInMs) + } + + /** + * Stops the polling interval. + */ + kill() { + if (this.intervalId) { + clearInterval(this.intervalId) + this.intervalId = undefined + } + } +} diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index f4c78e2093c..b393926e4cf 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -74,3 +74,4 @@ export * as BaseLspInstaller from './lsp/baseLspInstaller' export * as collectionUtil from './utilities/collectionUtils' export * from './datetime' export * from './performance/marks' +export * as mementoUtils from './utilities/mementos' diff --git a/packages/core/src/test/amazonq/customizationUtil.test.ts b/packages/core/src/test/amazonq/customizationUtil.test.ts index a3a49e907d9..19e59b91c03 100644 --- a/packages/core/src/test/amazonq/customizationUtil.test.ts +++ b/packages/core/src/test/amazonq/customizationUtil.test.ts @@ -21,22 +21,17 @@ import { import { FeatureContext, globals } from '../../shared' import { resetCodeWhispererGlobalVariables } from '../codewhisperer/testUtil' import { createSsoProfile, createTestAuth } from '../credentials/testUtil' -import { SsoConnection } from '../../auth' +import { createTestAuthUtil } from '../testAuthUtil' const enterpriseSsoStartUrl = 'https://enterprise.awsapps.com/start' describe('customizationProvider', function () { - let auth: ReturnType - let ssoConn: SsoConnection let regionProfileManager: RegionProfileManager beforeEach(async () => { - auth = createTestAuth(globals.globalState) - ssoConn = await auth.createInvalidSsoConnection( - createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: amazonQScopes }) - ) - - regionProfileManager = new RegionProfileManager(() => ssoConn) + createTestAuth(globals.globalState) + await createTestAuthUtil() + regionProfileManager = new RegionProfileManager(AuthUtil.instance) }) afterEach(() => { @@ -66,7 +61,6 @@ describe('customizationProvider', function () { describe('CodeWhisperer-customizationUtils', function () { let auth: ReturnType - let ssoConn: SsoConnection let featureCustomization: FeatureContext before(async function () { @@ -75,8 +69,10 @@ describe('CodeWhisperer-customizationUtils', function () { }) beforeEach(async function () { + await createTestAuthUtil() + auth = createTestAuth(globals.globalState) - ssoConn = await auth.createInvalidSsoConnection( + await auth.createInvalidSsoConnection( createSsoProfile({ startUrl: enterpriseSsoStartUrl, scopes: amazonQScopes }) ) featureCustomization = { @@ -91,7 +87,6 @@ describe('CodeWhisperer-customizationUtils', function () { sinon.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) sinon.stub(AuthUtil.instance, 'isConnected').returns(true) sinon.stub(AuthUtil.instance, 'isCustomizationFeatureEnabled').value(true) - sinon.stub(AuthUtil.instance, 'conn').value(ssoConn) await resetCodeWhispererGlobalVariables() }) @@ -101,14 +96,15 @@ describe('CodeWhisperer-customizationUtils', function () { }) it('Returns baseCustomization when not SSO', async function () { - sinon.stub(AuthUtil.instance, 'isValidEnterpriseSsoInUse').returns(false) + sinon.stub(AuthUtil.instance, 'isIdcConnection').returns(false) + const customization = getSelectedCustomization() assert.strictEqual(customization.name, baseCustomization.name) }) it('Returns selectedCustomization when customization manually selected', async function () { - sinon.stub(AuthUtil.instance, 'isValidEnterpriseSsoInUse').returns(true) + sinon.stub(AuthUtil.instance, 'isIdcConnection').returns(true) const selectedCustomization: Customization = { arn: 'selectedCustomizationArn', @@ -124,6 +120,8 @@ describe('CodeWhisperer-customizationUtils', function () { }) it(`setSelectedCustomization should set to the customization provided if override option is false or not specified`, async function () { + sinon.stub(AuthUtil.instance, 'isIdcConnection').returns(true) + await setSelectedCustomization({ arn: 'FOO' }, false) assert.strictEqual(getSelectedCustomization().arn, 'FOO') @@ -138,6 +136,8 @@ describe('CodeWhisperer-customizationUtils', function () { }) it(`setSelectedCustomization should only set to the customization provided once for override per customization arn if override is true`, async function () { + sinon.stub(AuthUtil.instance, 'isIdcConnection').returns(true) + await setSelectedCustomization({ arn: 'OVERRIDE' }, true) assert.strictEqual(getSelectedCustomization().arn, 'OVERRIDE') diff --git a/packages/core/src/test/amazonqDoc/controller.test.ts b/packages/core/src/test/amazonqDoc/controller.test.ts index d69edc47fd7..e3c62ffb2b8 100644 --- a/packages/core/src/test/amazonqDoc/controller.test.ts +++ b/packages/core/src/test/amazonqDoc/controller.test.ts @@ -18,7 +18,6 @@ import { } from './utils' import { CurrentWsFolders, MetricDataOperationName, MetricDataResult, NewFileInfo } from '../../amazonqDoc/types' import { DocCodeGenState, docScheme, Session } from '../../amazonqDoc' -import { AuthUtil } from '../../codewhisperer' import { ApiClientError, ApiServiceError, @@ -49,6 +48,7 @@ import { WorkspaceEmptyError, } from '../../amazonqDoc/errors' import { LlmError } from '../../amazonq/errors' + describe('Controller - Doc Generation', () => { const firstTabID = '123' const firstConversationID = '123' @@ -176,11 +176,6 @@ describe('Controller - Doc Generation', () => { configurable: true, }) - sandbox.stub(AuthUtil.instance, 'getChatAuthState').resolves({ - codewhispererCore: 'connected', - codewhispererChat: 'connected', - amazonQ: 'connected', - }) sandbox.stub(FileSystem.prototype, 'exists').resolves(false) if (isMultiTabs) { const secondSession = await createCodeGenState(sandbox, secondTabID, secondConversationID, secondUploadID) diff --git a/packages/core/src/test/amazonqDoc/utils.ts b/packages/core/src/test/amazonqDoc/utils.ts index 51c7305902c..d6d74e7ac3c 100644 --- a/packages/core/src/test/amazonqDoc/utils.ts +++ b/packages/core/src/test/amazonqDoc/utils.ts @@ -24,6 +24,8 @@ import { MetricData, } from '../../amazonqFeatureDev/client/featuredevproxyclient' import { FollowUpTypes } from '../../amazonq/commons/types' +import { AuthUtil } from '../../codewhisperer/util/authUtil' +import { LanguageClientAuth } from '../../auth/auth2' export function createMessenger(sandbox: sinon.SinonSandbox): DocMessenger { return new DocMessenger( @@ -102,7 +104,17 @@ export async function sessionWriteFile(session: Session, uri: vscode.Uri, encode }) } +export function createMockAuthUtil(sandbox: sinon.SinonSandbox) { + const mockLspAuth: Partial = { + registerSsoTokenChangedHandler: sinon.stub().resolves(), + } + AuthUtil.create(mockLspAuth as LanguageClientAuth) + sandbox.stub(AuthUtil.instance.regionProfileManager, 'onDidChangeRegionProfile').resolves() + sandbox.stub(AuthUtil.instance, 'getAuthState').returns('connected') +} + export async function createController(sandbox: sinon.SinonSandbox): Promise { + createMockAuthUtil(sandbox) const messenger = createMessenger(sandbox) // Create a new workspace root diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts index 7848d0561b0..8e8639332fd 100644 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts @@ -141,11 +141,7 @@ describe('Controller', () => { scheme: featureDevScheme, }) - sinon.stub(AuthUtil.instance, 'getChatAuthState').resolves({ - codewhispererCore: 'connected', - codewhispererChat: 'connected', - amazonQ: 'connected', - }) + sinon.stub(AuthUtil.instance, 'getAuthState').returns('connected') }) afterEach(() => { diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 01c7c43c947..914f629a380 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -409,8 +409,8 @@ describe('CodeWhisperer-basicCommands', function () { it('includes the "source" in the command execution metric', async function () { tryRegister(focusAmazonQPanel) - sinon.stub(AuthUtil.instance.secondaryAuth, 'deleteConnection') - targetCommand = testCommand(signoutCodeWhisperer, AuthUtil.instance) + sinon.stub(AuthUtil.instance, 'logout') + targetCommand = testCommand(signoutCodeWhisperer) await targetCommand.execute(placeholder, cwQuickPickSource) assertTelemetry('vscode_executeCommand', [ { source: cwQuickPickSource, command: focusAmazonQPanel.id }, @@ -475,8 +475,9 @@ describe('CodeWhisperer-basicCommands', function () { it('also shows customizations when connected to valid sso', async function () { sinon.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) sinon.stub(AuthUtil.instance, 'isConnected').returns(true) - sinon.stub(AuthUtil.instance, 'isValidEnterpriseSsoInUse').returns(true) + sinon.stub(AuthUtil.instance, 'isIdcConnection').returns(true) sinon.stub(AuthUtil.instance, 'isCustomizationFeatureEnabled').value(true) + sinon.stub(AuthUtil.instance.regionProfileManager, 'requireProfileSelection').returns(false) await CodeScansState.instance.setScansEnabled(false) getTestWindow().onDidShowQuickPick(async (e) => { @@ -499,7 +500,7 @@ describe('CodeWhisperer-basicCommands', function () { it('should not show auto-scans if using builder id', async function () { sinon.stub(AuthUtil.instance, 'isConnected').returns(true) - sinon.stub(AuthUtil.instance, 'isBuilderIdInUse').returns(true) + sinon.stub(AuthUtil.instance, 'isBuilderIdConnection').returns(true) getTestWindow().onDidShowQuickPick(async (e) => { e.assertItems([ diff --git a/packages/core/src/test/codewhisperer/startSecurityScan.test.ts b/packages/core/src/test/codewhisperer/startSecurityScan.test.ts index 551949aa3ab..38b00a2bdd3 100644 --- a/packages/core/src/test/codewhisperer/startSecurityScan.test.ts +++ b/packages/core/src/test/codewhisperer/startSecurityScan.test.ts @@ -30,6 +30,7 @@ import * as errors from '../../shared/errors' import * as timeoutUtils from '../../shared/utilities/timeoutUtils' import { SecurityIssueTreeViewProvider } from '../../codewhisperer' import { createClient, mockGetCodeScanResponse } from './testUtil' +import { createTestAuthUtil } from '../testAuthUtil' let extensionContext: FakeExtensionContext let mockSecurityPanelViewProvider: SecurityPanelViewProvider @@ -40,7 +41,9 @@ let focusStub: sinon.SinonStub describe('startSecurityScan', function () { const workspaceFolder = getTestWorkspaceFolder() + beforeEach(async function () { + await createTestAuthUtil() extensionContext = await FakeExtensionContext.create() mockSecurityPanelViewProvider = new SecurityPanelViewProvider(extensionContext) appRoot = join(workspaceFolder, 'python3.7-plain-sam-app') @@ -50,9 +53,11 @@ describe('startSecurityScan', function () { sinon.stub(timeoutUtils, 'sleep') focusStub = sinon.stub(SecurityIssueTreeViewProvider, 'focus') }) + afterEach(function () { sinon.restore() }) + after(async function () { await closeAllEditors() }) diff --git a/packages/core/src/test/codewhispererChat/editor/codelens.test.ts b/packages/core/src/test/codewhispererChat/editor/codelens.test.ts index 52243027ebf..3e9bd9284a5 100644 --- a/packages/core/src/test/codewhispererChat/editor/codelens.test.ts +++ b/packages/core/src/test/codewhispererChat/editor/codelens.test.ts @@ -15,7 +15,7 @@ import { InstalledClock } from '@sinonjs/fake-timers' import globals from '../../../shared/extensionGlobals' import { focusAmazonQPanel } from '../../../codewhispererChat/commands/registerCommands' import sinon from 'sinon' -import { AuthState, AuthStates, AuthUtil, FeatureAuthState } from '../../../codewhisperer/util/authUtil' +import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { inlinehintKey } from '../../../codewhisperer/models/constants' import { AutotriggerState, @@ -24,6 +24,8 @@ import { PressTabState, TryMoreExState, } from '../../../codewhisperer/views/lineAnnotationController' +import { AuthState } from '../../../auth/auth2' +import { createTestAuthUtil } from '../../testAuthUtil' describe('TryChatCodeLensProvider', () => { let instance: TryChatCodeLensProvider @@ -43,6 +45,7 @@ describe('TryChatCodeLensProvider', () => { }) beforeEach(async function () { + await createTestAuthUtil() isAmazonQVisibleEventEmitter = new vscode.EventEmitter() isAmazonQVisibleEvent = isAmazonQVisibleEventEmitter.event instance = new TryChatCodeLensProvider(isAmazonQVisibleEvent, () => codeLensPosition) @@ -58,7 +61,7 @@ describe('TryChatCodeLensProvider', () => { }) function stubConnection(state: AuthState) { - return sinon.stub(AuthUtil.instance, 'getChatAuthStateSync').returns({ amazonQ: state } as FeatureAuthState) + return sinon.stub(AuthUtil.instance, 'getAuthState').returns(state) } it('keeps returning a code lense until it hits the max times it should show', async function () { @@ -115,7 +118,9 @@ describe('TryChatCodeLensProvider', () => { stub.restore() } - const testStates = Object.values(AuthStates).filter((s) => s !== AuthStates.connected) + const testStates = Object.values(['connected', 'notConnected', 'expired'] as AuthState[]).filter( + (s) => s !== 'connected' + ) for (const state of testStates) { await testConnection(state) } diff --git a/packages/core/src/test/credentials/auth2.test.ts b/packages/core/src/test/credentials/auth2.test.ts new file mode 100644 index 00000000000..3f3df667d21 --- /dev/null +++ b/packages/core/src/test/credentials/auth2.test.ts @@ -0,0 +1,530 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { LanguageClientAuth, SsoLogin } from '../../auth/auth2' +import { LanguageClient } from 'vscode-languageclient' +import { + GetSsoTokenResult, + SsoTokenSourceKind, + AuthorizationFlowKind, + ListProfilesResult, + UpdateCredentialsParams, + SsoTokenChangedParams, + bearerCredentialsUpdateRequestType, + bearerCredentialsDeleteNotificationType, + ssoTokenChangedRequestType, + SsoTokenChangedKind, + invalidateSsoTokenRequestType, + ProfileKind, + AwsErrorCodes, +} from '@aws/language-server-runtimes/protocol' +import * as ssoProvider from '../../auth/sso/ssoAccessTokenProvider' + +const profileName = 'test-profile' +const sessionName = 'test-session' +const region = 'us-east-1' +const startUrl = 'test-url' +const tokenId = 'test-token' + +describe('LanguageClientAuth', () => { + let client: sinon.SinonStubbedInstance + let auth: LanguageClientAuth + const encryptionKey = Buffer.from('test-key') + let useDeviceFlowStub: sinon.SinonStub + + beforeEach(() => { + client = sinon.createStubInstance(LanguageClient) + auth = new LanguageClientAuth(client as unknown as LanguageClient, 'testClient', encryptionKey) + useDeviceFlowStub = sinon.stub(ssoProvider, 'useDeviceFlow') + }) + + afterEach(() => { + sinon.restore() + }) + + describe('getSsoToken', () => { + async function testGetSsoToken(useDeviceFlow: boolean) { + const tokenSource = { + kind: SsoTokenSourceKind.IamIdentityCenter, + profileName, + } + useDeviceFlowStub.returns(useDeviceFlow ? true : false) + + await auth.getSsoToken(tokenSource, true) + + sinon.assert.calledOnce(client.sendRequest) + sinon.assert.calledWith( + client.sendRequest, + sinon.match.any, + sinon.match({ + clientName: 'testClient', + source: tokenSource, + options: { + loginOnInvalidToken: true, + authorizationFlow: useDeviceFlow + ? AuthorizationFlowKind.DeviceCode + : AuthorizationFlowKind.Pkce, + }, + }) + ) + } + + it('sends correct request parameters for pkce flow', async () => { + await testGetSsoToken(false) + }) + + it('sends correct request parameters for device code flow', async () => { + await testGetSsoToken(true) + }) + }) + + describe('updateProfile', () => { + it('sends correct profile update parameters', async () => { + await auth.updateProfile(profileName, startUrl, region, ['scope1']) + + sinon.assert.calledOnce(client.sendRequest) + const requestParams = client.sendRequest.firstCall.args[1] + sinon.assert.match(requestParams.profile, { + name: profileName, + }) + sinon.assert.match(requestParams.ssoSession.settings, { + sso_region: region, + }) + }) + }) + + describe('getProfile', () => { + const profile = { name: profileName, settings: { sso_session: sessionName } } + const ssoSession = { name: sessionName, settings: { sso_region: region, sso_start_url: startUrl } } + + it('returns the correct profile and sso session', async () => { + const mockListProfilesResult: ListProfilesResult = { + profiles: [ + { + ...profile, + kinds: [], + }, + ], + ssoSessions: [ssoSession], + } + client.sendRequest.resolves(mockListProfilesResult) + + const result = await auth.getProfile(profileName) + + sinon.assert.calledOnce(client.sendRequest) + sinon.assert.match(result, { + profile, + ssoSession, + }) + }) + + it('returns undefined for non-existent profile', async () => { + const mockListProfilesResult: ListProfilesResult = { + profiles: [], + ssoSessions: [], + } + client.sendRequest.resolves(mockListProfilesResult) + + const result = await auth.getProfile('non-existent-profile') + + sinon.assert.calledOnce(client.sendRequest) + sinon.assert.match(result, { profile: undefined, ssoSession: undefined }) + }) + }) + + describe('updateBearerToken', () => { + it('sends request', async () => { + const updateParams: UpdateCredentialsParams = { + data: 'token-data', + encrypted: true, + } + + await auth.updateBearerToken(updateParams) + + sinon.assert.calledOnce(client.sendRequest) + sinon.assert.calledWith(client.sendRequest, bearerCredentialsUpdateRequestType.method, updateParams) + }) + }) + + describe('deleteBearerToken', () => { + it('sends notification', async () => { + auth.deleteBearerToken() + + sinon.assert.calledOnce(client.sendNotification) + sinon.assert.calledWith(client.sendNotification, bearerCredentialsDeleteNotificationType.method) + }) + }) + + describe('invalidateSsoToken', () => { + it('sends request', async () => { + client.sendRequest.resolves({ success: true }) + const result = await auth.invalidateSsoToken(tokenId) + + sinon.assert.calledOnce(client.sendRequest) + sinon.assert.calledWith(client.sendRequest, invalidateSsoTokenRequestType.method, { ssoTokenId: tokenId }) + sinon.assert.match(result, { success: true }) + }) + }) + + describe('registerSsoTokenChangedHandler', () => { + it('registers the handler correctly', () => { + const handler = sinon.spy() + + auth.registerSsoTokenChangedHandler(handler) + + sinon.assert.calledOnce(client.onNotification) + sinon.assert.calledWith(client.onNotification, ssoTokenChangedRequestType.method, sinon.match.func) + + // Simulate a token changed notification + const tokenChangedParams: SsoTokenChangedParams = { + kind: SsoTokenChangedKind.Refreshed, + ssoTokenId: tokenId, + } + const registeredHandler = client.onNotification.firstCall.args[1] + registeredHandler(tokenChangedParams) + + sinon.assert.calledOnce(handler) + sinon.assert.calledWith(handler, tokenChangedParams) + }) + }) +}) + +describe('SsoLogin', () => { + let lspAuth: sinon.SinonStubbedInstance + let ssoLogin: SsoLogin + let eventEmitter: vscode.EventEmitter + let fireEventSpy: sinon.SinonSpy + + const loginOpts = { + startUrl, + region, + scopes: ['scope1'], + } + + const mockGetSsoTokenResponse: GetSsoTokenResult = { + ssoToken: { + id: tokenId, + accessToken: 'encrypted-token', + }, + updateCredentialsParams: { + data: '', + }, + } + + beforeEach(() => { + lspAuth = sinon.createStubInstance(LanguageClientAuth) + eventEmitter = new vscode.EventEmitter() + fireEventSpy = sinon.spy(eventEmitter, 'fire') + ssoLogin = new SsoLogin(profileName, lspAuth as any) + ;(ssoLogin as any).eventEmitter = eventEmitter + ;(ssoLogin as any).connectionState = 'notConnected' + }) + + afterEach(() => { + sinon.restore() + eventEmitter.dispose() + }) + + describe('login', () => { + it('updates profile and returns SSO token', async () => { + lspAuth.updateProfile.resolves() + lspAuth.getSsoToken.resolves(mockGetSsoTokenResponse) + + const response = await ssoLogin.login(loginOpts) + + sinon.assert.calledOnce(lspAuth.updateProfile) + sinon.assert.calledWith( + lspAuth.updateProfile, + profileName, + loginOpts.startUrl, + loginOpts.region, + loginOpts.scopes + ) + sinon.assert.calledOnce(lspAuth.getSsoToken) + sinon.assert.match(ssoLogin.getConnectionState(), 'connected') + sinon.assert.match(ssoLogin.data, { + startUrl: loginOpts.startUrl, + region: loginOpts.region, + }) + sinon.assert.match(response.ssoToken.id, tokenId) + sinon.assert.match(response.updateCredentialsParams, mockGetSsoTokenResponse.updateCredentialsParams) + }) + }) + + describe('reauthenticate', () => { + it('throws when not connected', async () => { + ;(ssoLogin as any).connectionState = 'notConnected' + try { + await ssoLogin.reauthenticate() + sinon.assert.fail('Should have thrown an error') + } catch (err) { + sinon.assert.match((err as Error).message, 'Cannot reauthenticate when not connected.') + } + }) + + it('returns new SSO token when connected', async () => { + ;(ssoLogin as any).connectionState = 'connected' + lspAuth.getSsoToken.resolves(mockGetSsoTokenResponse) + + const response = await ssoLogin.reauthenticate() + + sinon.assert.calledOnce(lspAuth.getSsoToken) + sinon.assert.match(ssoLogin.getConnectionState(), 'connected') + sinon.assert.match(response.ssoToken.id, tokenId) + sinon.assert.match(response.updateCredentialsParams, mockGetSsoTokenResponse.updateCredentialsParams) + }) + }) + + describe('logout', () => { + it('invalidates token and updates state', async () => { + await ssoLogin.logout() + + sinon.assert.match(ssoLogin.getConnectionState(), 'notConnected') + sinon.assert.match(ssoLogin.data, undefined) + }) + + it('emits state change event', async () => { + ;(ssoLogin as any).connectionState = 'connected' + ;(ssoLogin as any).ssoTokenId = tokenId + ;(ssoLogin as any)._data = { + startUrl: loginOpts.startUrl, + region: loginOpts.region, + } + ;(ssoLogin as any).eventEmitter = eventEmitter + + lspAuth.invalidateSsoToken.resolves({ success: true }) + + await ssoLogin.logout() + + sinon.assert.calledOnce(fireEventSpy) + sinon.assert.calledWith(fireEventSpy, { + id: profileName, + state: 'notConnected', + }) + }) + }) + + describe('restore', () => { + const mockProfile = { + profile: { + kinds: [ProfileKind.SsoTokenProfile], + name: profileName, + }, + ssoSession: { + name: sessionName, + settings: { + sso_region: region, + sso_start_url: startUrl, + }, + }, + } + + it('restores connection state from existing profile', async () => { + lspAuth.getProfile.resolves(mockProfile) + lspAuth.getSsoToken.resolves(mockGetSsoTokenResponse) + + await ssoLogin.restore() + + sinon.assert.calledOnce(lspAuth.getProfile) + sinon.assert.calledWith(lspAuth.getProfile, mockProfile.profile.name) + sinon.assert.calledOnce(lspAuth.getSsoToken) + sinon.assert.calledWith( + lspAuth.getSsoToken, + sinon.match({ + kind: SsoTokenSourceKind.IamIdentityCenter, + profileName: mockProfile.profile.name, + }), + false // login parameter + ) + + sinon.assert.match(ssoLogin.data, { + region: region, + startUrl: startUrl, + }) + sinon.assert.match(ssoLogin.getConnectionState(), 'connected') + sinon.assert.match((ssoLogin as any).ssoTokenId, tokenId) + }) + + it('does not connect for non-existent profile', async () => { + lspAuth.getProfile.resolves({ profile: undefined, ssoSession: undefined }) + + await ssoLogin.restore() + + sinon.assert.calledOnce(lspAuth.getProfile) + sinon.assert.calledOnce(lspAuth.getSsoToken) + sinon.assert.match(ssoLogin.data, undefined) + sinon.assert.match(ssoLogin.getConnectionState(), 'notConnected') + }) + + it('emits state change event on successful restore', async () => { + ;(ssoLogin as any).eventEmitter = eventEmitter + + lspAuth.getProfile.resolves(mockProfile) + lspAuth.getSsoToken.resolves(mockGetSsoTokenResponse) + + await ssoLogin.restore() + + sinon.assert.calledOnce(fireEventSpy) + sinon.assert.calledWith(fireEventSpy, { + id: profileName, + state: 'connected', + }) + }) + }) + + describe('cancelLogin', () => { + it('cancels and dispose token source', async () => { + await ssoLogin.login(loginOpts).catch(() => {}) + + ssoLogin.cancelLogin() + + const tokenSource = (ssoLogin as any).cancellationToken + sinon.assert.match(tokenSource, undefined) + }) + }) + + describe('_getSsoToken', () => { + beforeEach(() => { + ;(ssoLogin as any).connectionState = 'connected' + }) + + const testErrorHandling = async (errorCode: string, expectedState: string, shouldEmitEvent: boolean = true) => { + const error = new Error('Token error') + ;(error as any).data = { awsErrorCode: errorCode } + lspAuth.getSsoToken.rejects(error) + + try { + await (ssoLogin as any)._getSsoToken(false) + sinon.assert.fail('Should have thrown an error') + } catch (err) { + sinon.assert.match(err, error) + } + + sinon.assert.match(ssoLogin.getConnectionState(), expectedState) + + if (shouldEmitEvent) { + sinon.assert.calledWith(fireEventSpy, { + id: profileName, + state: expectedState, + }) + } + + sinon.assert.match((ssoLogin as any).cancellationToken, undefined) + } + + const notConnectedErrors = [ + AwsErrorCodes.E_CANCELLED, + AwsErrorCodes.E_SSO_SESSION_NOT_FOUND, + AwsErrorCodes.E_PROFILE_NOT_FOUND, + AwsErrorCodes.E_INVALID_SSO_TOKEN, + ] + + for (const errorCode of notConnectedErrors) { + it(`handles ${errorCode} error`, async () => { + await testErrorHandling(errorCode, 'notConnected') + }) + } + + it('handles token refresh error', async () => { + await testErrorHandling(AwsErrorCodes.E_CANNOT_REFRESH_SSO_TOKEN, 'expired') + }) + + it('handles unknown errors', async () => { + await testErrorHandling('UNKNOWN_ERROR', ssoLogin.getConnectionState(), false) + }) + + it('returns correct response and cleans up cancellation token', async () => { + lspAuth.getSsoToken.resolves(mockGetSsoTokenResponse) + + const response = await (ssoLogin as any)._getSsoToken(true) + + sinon.assert.calledWith( + lspAuth.getSsoToken, + sinon.match({ + kind: SsoTokenSourceKind.IamIdentityCenter, + profileName, + }), + true + ) + + sinon.assert.match(response, mockGetSsoTokenResponse) + sinon.assert.match((ssoLogin as any).cancellationToken, undefined) + }) + + it('updates state when token is retrieved successfully', async () => { + ;(ssoLogin as any).connectionState = 'notConnected' + lspAuth.getSsoToken.resolves(mockGetSsoTokenResponse) + + await (ssoLogin as any)._getSsoToken(true) + + sinon.assert.match(ssoLogin.getConnectionState(), 'connected') + sinon.assert.match((ssoLogin as any).ssoTokenId, tokenId) + sinon.assert.calledWith(fireEventSpy, { + id: profileName, + state: 'connected', + }) + }) + }) + + describe('onDidChangeConnectionState', () => { + it('should register handler for connection state changes', () => { + const handler = sinon.spy() + ssoLogin.onDidChangeConnectionState(handler) + + // Simulate state change + ;(ssoLogin as any).updateConnectionState('connected') + + sinon.assert.calledWith(handler, { + id: profileName, + state: 'connected', + }) + }) + }) + + describe('ssoTokenChangedHandler', () => { + beforeEach(() => { + ;(ssoLogin as any).ssoTokenId = tokenId + ;(ssoLogin as any).connectionState = 'connected' + }) + + it('updates state when token expires', () => { + ;(ssoLogin as any).ssoTokenChangedHandler({ + kind: 'Expired', + ssoTokenId: tokenId, + }) + + sinon.assert.match(ssoLogin.getConnectionState(), 'expired') + sinon.assert.calledOnce(fireEventSpy) + sinon.assert.calledWith(fireEventSpy, { + id: profileName, + state: 'expired', + }) + }) + + it('emits refresh event when token is refreshed', () => { + ;(ssoLogin as any).ssoTokenChangedHandler({ + kind: 'Refreshed', + ssoTokenId: tokenId, + }) + + sinon.assert.calledOnce(fireEventSpy) + sinon.assert.calledWith(fireEventSpy, { + id: profileName, + state: 'refreshed', + }) + }) + + it('does not emit event for different token ID', () => { + ;(ssoLogin as any).ssoTokenChangedHandler({ + kind: 'Refreshed', + ssoTokenId: 'different-token-id', + }) + + sinon.assert.notCalled(fireEventSpy) + }) + }) +}) diff --git a/packages/core/src/test/credentials/sso/ssoAccessTokenProvider.test.ts b/packages/core/src/test/credentials/sso/ssoAccessTokenProvider.test.ts index 2cb98193224..bd7f264f557 100644 --- a/packages/core/src/test/credentials/sso/ssoAccessTokenProvider.test.ts +++ b/packages/core/src/test/credentials/sso/ssoAccessTokenProvider.test.ts @@ -83,7 +83,7 @@ describe('SsoAccessTokenProvider', function () { tempDir = await makeTemporaryTokenCacheFolder() cache = getCache(tempDir) reAuthState = new TestReAuthState() - sut = SsoAccessTokenProvider.create({ region, startUrl }, cache, oidcClient, reAuthState, () => true) + sut = SsoAccessTokenProvider.create({ region, startUrl }, cache, oidcClient, reAuthState) }) afterEach(async function () { @@ -271,13 +271,7 @@ describe('SsoAccessTokenProvider', function () { await sut.createToken() // Mimic when we sign out then in again with the same region+startUrl. The ID is the only thing different. - sut = SsoAccessTokenProvider.create( - { region, startUrl, identifier: 'bbb' }, - cache, - oidcClient, - reAuthState, - () => true - ) + sut = SsoAccessTokenProvider.create({ region, startUrl, identifier: 'bbb' }, cache, oidcClient, reAuthState) await sut.createToken() assertTelemetry('aws_loginWithBrowser', [ diff --git a/packages/core/src/test/credentials/utils.test.ts b/packages/core/src/test/credentials/utils.test.ts index ff4dc1046bc..2526118d5e1 100644 --- a/packages/core/src/test/credentials/utils.test.ts +++ b/packages/core/src/test/credentials/utils.test.ts @@ -67,7 +67,6 @@ type SsoTestCase = { kind: SsoKind; connections: Connection[]; expected: boolean type BuilderIdTestCase = { kind: BuilderIdKind; connections: Connection[]; expected: boolean } describe('connection exists funcs', function () { - const cwIdcConnection: SsoConnection = { ...ssoConnection, scopes: amazonQScopes, label: 'codeWhispererSso' } const cwBuilderIdConnection: SsoConnection = { ...builderIdConnection, scopes: amazonQScopes, @@ -81,7 +80,6 @@ describe('connection exists funcs', function () { const ssoConnections: Connection[] = [ ssoConnection, builderIdConnection, - cwIdcConnection, cwBuilderIdConnection, ccBuilderIdConnection, ] @@ -96,15 +94,13 @@ describe('connection exists funcs', function () { ].map((c) => { return { ...c, kind: 'any' } }) - const cwIdcCases: SsoTestCase[] = [ - { connections: [cwIdcConnection], expected: true }, - { connections: allConnections, expected: true }, + const ccIdcCases: SsoTestCase[] = [ { connections: [], expected: false }, - { connections: allConnections.filter((c) => c !== cwIdcConnection), expected: false }, + { connections: allConnections, expected: false }, ].map((c) => { - return { ...c, kind: 'codewhisperer' } + return { ...c, kind: 'codecatalyst' } }) - const allCases = [...anyCases, ...cwIdcCases] + const allCases = [...anyCases, ...ccIdcCases] for (const args of allCases) { it(`ssoExists() returns '${args.expected}' when kind '${args.kind}' given [${args.connections @@ -116,15 +112,6 @@ describe('connection exists funcs', function () { }) describe('builderIdExists()', function () { - const cwBuilderIdCases: BuilderIdTestCase[] = [ - { connections: [cwBuilderIdConnection], expected: true }, - { connections: allConnections, expected: true }, - { connections: [], expected: false }, - { connections: allConnections.filter((c) => c !== cwBuilderIdConnection), expected: false }, - ].map((c) => { - return { ...c, kind: 'codewhisperer' } - }) - const ccBuilderIdCases: BuilderIdTestCase[] = [ { connections: [ccBuilderIdConnection], expected: true }, { connections: allConnections, expected: true }, @@ -134,9 +121,7 @@ describe('connection exists funcs', function () { return { ...c, kind: 'codecatalyst' } }) - const allCases = [...cwBuilderIdCases, ...ccBuilderIdCases] - - for (const args of allCases) { + for (const args of ccBuilderIdCases) { it(`builderIdExists() returns '${args.expected}' when kind '${args.kind}' given [${args.connections .map((c) => c.label) .join(', ')}]`, async function () { diff --git a/packages/core/src/test/index.ts b/packages/core/src/test/index.ts index 9a01973e26d..0a4203f2fe5 100644 --- a/packages/core/src/test/index.ts +++ b/packages/core/src/test/index.ts @@ -25,3 +25,4 @@ export * from './testUtil' export * from './amazonq/utils' export * from './fake/mockFeatureConfigData' export * from './shared/ui/testUtils' +export * from './testAuthUtil' diff --git a/packages/core/src/test/login/webview/vue/backend_amazonq.test.ts b/packages/core/src/test/login/webview/vue/backend_amazonq.test.ts deleted file mode 100644 index c22b1a77fe1..00000000000 --- a/packages/core/src/test/login/webview/vue/backend_amazonq.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { SinonSandbox, createSandbox } from 'sinon' -import { assertTelemetry } from '../../../testUtil' -import assert from 'assert' -import { - createBuilderIdProfile, - createSsoProfile, - createTestAuth, - mockRegistration, -} from '../../../credentials/testUtil' -import { Auth } from '../../../../auth' -import { AmazonQLoginWebview } from '../../../../login/webview/vue/amazonq/backend_amazonq' -import { isBuilderIdConnection, isIdcSsoConnection } from '../../../../auth/connection' -import { amazonQScopes, AuthUtil } from '../../../../codewhisperer/util/authUtil' -import { getOpenExternalStub } from '../../../globalSetup.test' -import globals from '../../../../shared/extensionGlobals' - -// TODO: remove auth page and tests -describe('Amazon Q Login', function () { - const region = 'fakeRegion' - const startUrl = 'fakeUrl' - - let sandbox: SinonSandbox - let auth: ReturnType - let authUtil: AuthUtil - let backend: AmazonQLoginWebview - - beforeEach(function () { - sandbox = createSandbox() - auth = createTestAuth(globals.globalState) - authUtil = new AuthUtil(auth) - sandbox.stub(Auth, 'instance').value(auth) - sandbox.stub(AuthUtil, 'instance').value(authUtil) - getOpenExternalStub().resolves(true) - - backend = new AmazonQLoginWebview() - }) - - afterEach(function () { - sandbox.restore() - }) - - it('signs into builder ID and emits telemetry', async function () { - await backend.startBuilderIdSetup() - - assert.ok(isBuilderIdConnection(auth.activeConnection)) - assert.deepStrictEqual(auth.activeConnection.scopes, amazonQScopes) - assert.deepStrictEqual(auth.activeConnection.state, 'valid') - - assertTelemetry('auth_addConnection', { - result: 'Succeeded', - credentialSourceId: 'awsId', - authEnabledFeatures: 'codewhisperer', - isReAuth: false, - ssoRegistrationExpiresAt: mockRegistration.expiresAt.toISOString(), - ssoRegistrationClientId: mockRegistration.clientId, - }) - }) - - it('signs into IdC and emits telemetry', async function () { - await backend.startEnterpriseSetup(startUrl, region) - - assert.ok(isIdcSsoConnection(auth.activeConnection)) - assert.deepStrictEqual(auth.activeConnection.scopes, amazonQScopes) - assert.deepStrictEqual(auth.activeConnection.state, 'valid') - assert.deepStrictEqual(auth.activeConnection.startUrl, startUrl) - assert.deepStrictEqual(auth.activeConnection.ssoRegion, region) - - assertTelemetry('auth_addConnection', { - result: 'Succeeded', - credentialSourceId: 'iamIdentityCenter', - authEnabledFeatures: 'codewhisperer', - credentialStartUrl: startUrl, - awsRegion: region, - isReAuth: false, - ssoRegistrationExpiresAt: mockRegistration.expiresAt.toISOString(), - ssoRegistrationClientId: mockRegistration.clientId, - }) - }) - - it('reauths builder ID and emits telemetry', async function () { - const conn = await auth.createInvalidSsoConnection(createBuilderIdProfile({ scopes: amazonQScopes })) - await auth.useConnection(conn) - - // method under test - await backend.reauthenticateConnection() - - assert.deepStrictEqual(auth.activeConnection?.state, 'valid') - - assertTelemetry('auth_addConnection', { - result: 'Succeeded', - credentialSourceId: 'awsId', - authEnabledFeatures: 'codewhisperer', - isReAuth: true, - ssoRegistrationExpiresAt: mockRegistration.expiresAt.toISOString(), - ssoRegistrationClientId: mockRegistration.clientId, - }) - }) - - it('reauths IdC and emits telemetry', async function () { - const conn = await auth.createInvalidSsoConnection( - createSsoProfile({ scopes: amazonQScopes, startUrl, ssoRegion: region }) - ) - await auth.useConnection(conn) - - // method under test - await backend.reauthenticateConnection() - - assert.deepStrictEqual(auth.activeConnection?.state, 'valid') - - assertTelemetry('auth_addConnection', { - result: 'Succeeded', - credentialSourceId: 'iamIdentityCenter', - authEnabledFeatures: 'codewhisperer', - credentialStartUrl: startUrl, - awsRegion: region, - isReAuth: true, - ssoRegistrationExpiresAt: mockRegistration.expiresAt.toISOString(), - ssoRegistrationClientId: mockRegistration.clientId, - }) - }) - - it('signs out of reauth and emits telemetry', async function () { - const conn = await auth.createInvalidSsoConnection( - createSsoProfile({ scopes: amazonQScopes, startUrl, ssoRegion: region }) - ) - await auth.useConnection(conn) - - // method under test - await backend.signout() - - assert.equal(auth.activeConnection, undefined) - - assertTelemetry('auth_addConnection', { - result: 'Cancelled', - credentialSourceId: 'iamIdentityCenter', - authEnabledFeatures: 'codewhisperer', - credentialStartUrl: startUrl, - awsRegion: region, - isReAuth: true, - ssoRegistrationExpiresAt: mockRegistration.expiresAt.toISOString(), - ssoRegistrationClientId: mockRegistration.clientId, - }) - }) -}) diff --git a/packages/core/src/test/shared/featureConfig.test.ts b/packages/core/src/test/shared/featureConfig.test.ts index 9c9f20cb5fb..e94358361b0 100644 --- a/packages/core/src/test/shared/featureConfig.test.ts +++ b/packages/core/src/test/shared/featureConfig.test.ts @@ -10,9 +10,12 @@ import { Features, FeatureConfigProvider, featureDefinitions, FeatureName } from import { ListFeatureEvaluationsResponse } from '../../codewhisperer' import { createSpyClient } from '../codewhisperer/testUtil' import { mockFeatureConfigsData } from '../fake/mockFeatureConfigData' +import { createTestAuthUtil } from '../testAuthUtil' describe('FeatureConfigProvider', () => { beforeEach(async () => { + await createTestAuthUtil() + const clientSpy = await createSpyClient() sinon.stub(clientSpy, 'listFeatureEvaluations').returns({ promise: () => diff --git a/packages/core/src/test/testAuthUtil.ts b/packages/core/src/test/testAuthUtil.ts new file mode 100644 index 00000000000..595f8bf45ef --- /dev/null +++ b/packages/core/src/test/testAuthUtil.ts @@ -0,0 +1,47 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as jose from 'jose' +import * as crypto from 'crypto' +import { LanguageClientAuth } from '../auth/auth2' +import { AuthUtil } from '../codewhisperer/util/authUtil' + +export async function createTestAuthUtil() { + const encryptionKey = crypto.randomBytes(32) + + const jwe = await new jose.CompactEncrypt(new TextEncoder().encode(JSON.stringify({ your: 'mock data' }))) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + const fakeToken = { + ssoToken: { + id: 'fake-id', + accessToken: jwe, + }, + updateCredentialsParams: { + data: 'fake-data', + }, + } + + const mockLspAuth: Partial = { + registerSsoTokenChangedHandler: sinon.stub().resolves(), + updateProfile: sinon.stub().resolves(), + getSsoToken: sinon.stub().resolves(fakeToken), + getProfile: sinon.stub().resolves({ + sso_registration_scopes: ['codewhisperer'], + }), + deleteBearerToken: sinon.stub().resolves(), + updateBearerToken: sinon.stub().resolves(), + invalidateSsoToken: sinon.stub().resolves(), + registerCacheWatcher: sinon.stub().resolves(), + encryptionKey, + } + + // Since AuthUtil is a singleton, we want to remove an existing instance before setting up a new one + AuthUtil.destroy() + + AuthUtil.create(mockLspAuth as LanguageClientAuth) +} diff --git a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts index 0038795ad89..29a535707b6 100644 --- a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts +++ b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts @@ -6,11 +6,12 @@ import assert from 'assert' import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' import { ConfigurationEntry } from '../../codewhisperer/models/model' -import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' +import { skipTestIfNoValidConn } from '../util/connection' import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' import { createMockTextEditor, resetCodeWhispererGlobalVariables } from '../../test/codewhisperer/testUtil' import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' import { session } from '../../codewhisperer/util/codeWhispererSession' +import { AuthUtil } from '../../codewhisperer/util/authUtil' /* New model deployment may impact references returned. @@ -49,7 +50,7 @@ describe('CodeWhisperer service invocation', async function () { } before(async function () { - validConnection = await setValidConnection() + validConnection = AuthUtil.instance.isConnected() }) beforeEach(function () { diff --git a/packages/core/src/testE2E/codewhisperer/securityScan.test.ts b/packages/core/src/testE2E/codewhisperer/securityScan.test.ts index 730b9628290..0dc5c5f105d 100644 --- a/packages/core/src/testE2E/codewhisperer/securityScan.test.ts +++ b/packages/core/src/testE2E/codewhisperer/securityScan.test.ts @@ -9,7 +9,7 @@ import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' import * as CodeWhispererConstants from '../../codewhisperer/models/constants' import * as path from 'path' import * as testutil from '../../test/testUtil' -import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' +import { skipTestIfNoValidConn } from '../util/connection' import { resetCodeWhispererGlobalVariables } from '../../test/codewhisperer/testUtil' import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' import { closeAllEditors } from '../../test/testUtil' @@ -23,6 +23,7 @@ import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import fs from '../../shared/fs/fs' import { ZipUtil } from '../../codewhisperer/util/zipUtil' import { randomUUID } from '../../shared/crypto' +import { AuthUtil } from '../../codewhisperer' const filePromptWithSecurityIssues = `from flask import app @@ -53,7 +54,7 @@ describe('CodeWhisperer security scan', async function () { const workspaceFolder = getTestWorkspaceFolder() before(async function () { - validConnection = await setValidConnection() + validConnection = AuthUtil.instance.isConnected() }) beforeEach(function () { diff --git a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts index d4265d13982..33d910f59cb 100644 --- a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts +++ b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts @@ -6,7 +6,7 @@ import assert from 'assert' import * as vscode from 'vscode' import * as path from 'path' -import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' +import { skipTestIfNoValidConn } from '../util/connection' import { ConfigurationEntry } from '../../codewhisperer/models/model' import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' @@ -20,6 +20,7 @@ import { sleep } from '../../shared/utilities/timeoutUtils' import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' import { session } from '../../codewhisperer/util/codeWhispererSession' +import { AuthUtil } from '../../codewhisperer/util/authUtil' describe('CodeWhisperer service invocation', async function () { let validConnection: boolean @@ -32,7 +33,7 @@ describe('CodeWhisperer service invocation', async function () { } before(async function () { - validConnection = await setValidConnection() + validConnection = AuthUtil.instance.isConnected() }) beforeEach(function () { diff --git a/packages/core/src/testE2E/util/connection.ts b/packages/core/src/testE2E/util/connection.ts index bf158426a2a..da2f733a458 100644 --- a/packages/core/src/testE2E/util/connection.ts +++ b/packages/core/src/testE2E/util/connection.ts @@ -3,9 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { isValidAmazonQConnection } from '../../codewhisperer/util/authUtil' -import { Auth } from '../../auth/auth' - /* In order to run codewhisperer or gumby integration tests user must: @@ -16,24 +13,6 @@ Test cases will skip if the above criteria are not met. If user has an expired connection they must reauthenticate prior to running tests. */ -async function getValidConnection() { - return (await Auth.instance.listConnections()).find(isValidAmazonQConnection) -} - -export async function setValidConnection() { - const conn = await getValidConnection() - let validConnection: boolean - - if (conn !== undefined && Auth.instance.getConnectionState(conn) === 'valid') { - validConnection = true - await Auth.instance.useConnection(conn) - } else { - validConnection = false - console.log(`No valid auth connection, will skip Amazon Q integration test cases`) - } - return validConnection -} - export function skipTestIfNoValidConn(validConnection: boolean, ctx: Mocha.Context) { if (!validConnection && ctx.currentTest) { ctx.currentTest.title += ` (skipped - no valid connection)` diff --git a/packages/core/src/testInteg/amazonQTransform/transformByQ.test.ts b/packages/core/src/testInteg/amazonQTransform/transformByQ.test.ts index b191997a236..e2b423f0591 100644 --- a/packages/core/src/testInteg/amazonQTransform/transformByQ.test.ts +++ b/packages/core/src/testInteg/amazonQTransform/transformByQ.test.ts @@ -13,18 +13,16 @@ import request from '../../shared/request' import { transformByQState, ZipManifest } from '../../codewhisperer/models/model' import globals from '../../shared/extensionGlobals' import { fs } from '../../shared' -import { setValidConnection } from '../../testE2E/util/connection' +import { AuthUtil } from '../../codewhisperer/util/authUtil' describe('transformByQ', async function () { let tempDir = '' let tempFileName = '' let tempFilePath = '' let zippedCodePath = '' - let validConnection: boolean before(async function () { - validConnection = await setValidConnection() - if (!validConnection) { + if (!AuthUtil.instance.isConnected()) { this.skip() } tempDir = path.join(os.tmpdir(), 'gumby-test') From 0b841528b3f64643d6ff8a900295eb5cb356613f Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 16 May 2025 15:02:17 -0400 Subject: [PATCH 36/48] fix: active inline properly --- packages/amazonq/src/lsp/client.ts | 84 ++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 0e4e1555086..08d049c9aba 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -31,7 +31,12 @@ import { LSPErrorCodes, updateConfigurationRequestType, } from '@aws/language-server-runtimes/protocol' -import { AuthUtil, CodeWhispererSettings, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { + AuthUtil, + CodeWhispererSettings, + getSelectedCustomization, + TelemetryHelper, +} from 'aws-core-vscode/codewhisperer' import { Settings, createServerOptions, @@ -46,13 +51,20 @@ import { openUrl, getClientId, extensionVersion, + Commands, } from 'aws-core-vscode/shared' import { processUtils } from 'aws-core-vscode/shared' import { activate as activateChat } from './chat/activation' +import { activate as activeInlineChat } from '../inlineChat/activation' import { AmazonQResourcePaths } from './lspInstaller' import { auth2 } from 'aws-core-vscode/auth' import { ConfigSection, isValidConfigSection, pushConfigUpdate, toAmazonQLSPLogLevel } from './config' import { telemetry } from 'aws-core-vscode/telemetry' +import { SessionManager } from '../app/inline/sessionManager' +import { LineTracker } from '../app/inline/stateTracker/lineTracker' +import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' +import { InlineTutorialAnnotation } from '../app/inline/tutorials/inlineTutorialAnnotation' +import { InlineCompletionManager } from '../app/inline/completion' const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') @@ -181,7 +193,7 @@ export async function startLanguageServer( */ await initializeAuth(client) - await postStartLanguageServer(client, resourcePaths, toDispose) + await postStartLanguageServer(extensionContext, client, resourcePaths, toDispose) return client @@ -210,7 +222,60 @@ export async function startLanguageServer( } } +async function setupInline( + extensionContext: vscode.ExtensionContext, + client: LanguageClient, + toDispose: vscode.Disposable[] +) { + const sessionManager = new SessionManager() + const lineTracker = new LineTracker() + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) + const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) + + const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) + + inlineManager.registerInlineCompletion() + + activeInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) + + toDispose.push( + inlineManager, + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + }), + Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + const editor = vscode.window.activeTextEditor + if (editor) { + if (forceProceed) { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer', true) + } else { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer') + } + } + }), + Commands.register('aws.amazonq.dismissTutorial', async () => { + const editor = vscode.window.activeTextEditor + if (editor) { + inlineTutorialAnnotation.clear() + try { + telemetry.ui_click.emit({ elementId: `dismiss_${inlineTutorialAnnotation.currentState.id}` }) + } catch (_) {} + await inlineTutorialAnnotation.dismissTutorial() + getLogger().debug(`codewhisperer: user dismiss tutorial.`) + } + }), + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }) + ) +} + async function postStartLanguageServer( + extensionContext: vscode.ExtensionContext, client: LanguageClient, resourcePaths: AmazonQResourcePaths, toDispose: vscode.Disposable[] @@ -308,23 +373,12 @@ async function postStartLanguageServer( ) }) - // 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', true)) { await activateChat(client, encryptionKey, resourcePaths.ui) } + await setupInline(extensionContext, client, toDispose) + toDispose.push( AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(sendProfileToLsp), vscode.commands.registerCommand('aws.amazonq.getWorkspaceId', async () => { From 0d6d35e9406850c960e81d484c47e7c2bb59ca6d Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Mon, 19 May 2025 12:56:02 -0400 Subject: [PATCH 37/48] fix(amazonq): fix cache watcher on token file to avoid logout after SSO connection migration (#7345) ## Problem The current cache file watcher is triggered on every `*.json` file change in the cache folder. When migrating old auth SSO connections to Flare, there is a race condition. If the old connection is migrated (meaning that the old token file deletes) after the file watcher is instantiated, the file watcher `delete` event will trigger and logout the user ## Solution The Flare auth cache file watcher will only listen to events for the Flare token .json file. This way, when we delete the old token .json file, there is no logout event triggered --- - 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/util/clearCache.ts | 7 ++--- .../apps/inline/recommendationService.test.ts | 20 +++++++++---- packages/core/src/auth/auth2.ts | 5 ++-- packages/core/src/auth/sso/cache.ts | 20 +++++++++++-- .../core/src/codewhisperer/util/authUtil.ts | 29 ++++++++----------- 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/packages/amazonq/src/util/clearCache.ts b/packages/amazonq/src/util/clearCache.ts index b516c33d43c..19a2e730ae7 100644 --- a/packages/amazonq/src/util/clearCache.ts +++ b/packages/amazonq/src/util/clearCache.ts @@ -32,10 +32,9 @@ async function clearCache() { return } - // SSO cache persists on disk, this should indirectly delete it - const conn = AuthUtil.instance.conn - if (conn) { - await AuthUtil.instance.auth.deleteConnection(conn) + // SSO cache persists on disk, this should log out + if (AuthUtil.instance.isConnected()) { + await AuthUtil.instance.logout() } await globals.globalState.clear() diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index 57eca77b147..9bd195aa4a2 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -9,7 +9,7 @@ import { Position, CancellationToken, InlineCompletionItem } from 'vscode' import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' -import { createMockDocument } from 'aws-core-vscode/test' +import { createMockDocument, createTestAuthUtil } from 'aws-core-vscode/test' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' @@ -17,6 +17,11 @@ describe('RecommendationService', () => { let languageClient: LanguageClient let sendRequestStub: sinon.SinonStub let sandbox: sinon.SinonSandbox + let sessionManager: SessionManager + let lineTracker: LineTracker + let activeStateController: InlineGeneratingMessage + let service: RecommendationService + const mockDocument = createMockDocument() const mockPosition = { line: 0, character: 0 } as Position const mockContext = { triggerKind: 1, selectedCompletionInfo: undefined } @@ -29,12 +34,8 @@ describe('RecommendationService', () => { insertText: 'ItemTwo', } as InlineCompletionItem const mockPartialResultToken = 'some-random-token' - const sessionManager = new SessionManager() - const lineTracker = new LineTracker() - const activeStateController = new InlineGeneratingMessage(lineTracker) - const service = new RecommendationService(sessionManager, activeStateController) - beforeEach(() => { + beforeEach(async () => { sandbox = sinon.createSandbox() sendRequestStub = sandbox.stub() @@ -42,6 +43,13 @@ describe('RecommendationService', () => { languageClient = { sendRequest: sendRequestStub, } as unknown as LanguageClient + + await createTestAuthUtil() + + sessionManager = new SessionManager() + lineTracker = new LineTracker() + activeStateController = new InlineGeneratingMessage(lineTracker) + service = new RecommendationService(sessionManager, activeStateController) }) afterEach(() => { diff --git a/packages/core/src/auth/auth2.ts b/packages/core/src/auth/auth2.ts index bff664b7e7b..273a644ebbd 100644 --- a/packages/core/src/auth/auth2.ts +++ b/packages/core/src/auth/auth2.ts @@ -41,7 +41,8 @@ import { LanguageClient } from 'vscode-languageclient' import { getLogger } from '../shared/logger/logger' import { ToolkitError } from '../shared/errors' import { useDeviceFlow } from './sso/ssoAccessTokenProvider' -import { getCacheFileWatcher } from './sso/cache' +import { getCacheDir, getCacheFileWatcher, getFlareCacheFileName } from './sso/cache' +import { VSCODE_EXTENSION_ID } from '../shared/extensions' export const notificationTypes = { updateBearerToken: new RequestType( @@ -77,7 +78,7 @@ export type TokenSource = IamIdentityCenterSsoTokenSource | AwsBuilderIdSsoToken * Handles auth requests to the Identity Server in the Amazon Q LSP. */ export class LanguageClientAuth { - readonly #ssoCacheWatcher = getCacheFileWatcher() + readonly #ssoCacheWatcher = getCacheFileWatcher(getCacheDir(), getFlareCacheFileName(VSCODE_EXTENSION_ID.amazonq)) constructor( private readonly client: LanguageClient, diff --git a/packages/core/src/auth/sso/cache.ts b/packages/core/src/auth/sso/cache.ts index ce9c246ee7a..f9d62c50305 100644 --- a/packages/core/src/auth/sso/cache.ts +++ b/packages/core/src/auth/sso/cache.ts @@ -45,8 +45,8 @@ export function getCache(directory = getCacheDir()): SsoCache { } } -export function getCacheFileWatcher(directory = getCacheDir()) { - const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(directory, '*.json')) +export function getCacheFileWatcher(directory = getCacheDir(), file = '*.json') { + const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(directory, file)) globals.context.subscriptions.push(watcher) return watcher } @@ -158,3 +158,19 @@ export function getRegistrationCacheFile(ssoCacheDir: string, key: RegistrationK const suffix = `${key.region}${key.scopes && key.scopes.length > 0 ? `-${hash(key.startUrl, key.scopes)}` : ''}` return path.join(ssoCacheDir, `aws-toolkit-vscode-client-id-${suffix}.json`) } + +/** + * Returns the cache file name that Flare identity server uses for SSO token and registration + * + * @param key - The key to use for the new registration cache file. + * See https://github.com/aws/language-servers/blob/c10819ea2c25ce564c75fb43a6792f3c919b757a/server/aws-lsp-identity/src/sso/cache/fileSystemSsoCache.ts + * @returns File name of the Flare cache file + */ +export function getFlareCacheFileName(key: string) { + const hash = (str: string) => { + const hasher = crypto.createHash('sha1') + return hasher.update(str).digest('hex') + } + + return hash(key) + '.json' +} diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index ec9a2ff91e1..bb1a4d11366 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -8,7 +8,6 @@ import * as localizedText from '../../shared/localizedText' import * as nls from 'vscode-nls' import { fs } from '../../shared/fs/fs' import * as path from 'path' -import * as crypto from 'crypto' import { ToolkitError } from '../../shared/errors' import { AmazonQPromptSettings } from '../../shared/settings' import { @@ -37,7 +36,7 @@ import { VSCODE_EXTENSION_ID } from '../../shared/extensions' import { RegionProfileManager } from '../region/regionProfileManager' import { AuthFormId } from '../../login/webview/vue/types' import { getEnvironmentSpecificMemento } from '../../shared/utilities/mementos' -import { getCacheDir, getRegistrationCacheFile, getTokenCacheFile } from '../../auth/sso/cache' +import { getCacheDir, getFlareCacheFileName, getRegistrationCacheFile, getTokenCacheFile } from '../../auth/sso/cache' import { notifySelectDeveloperProfile } from '../region/utils' import { once } from '../../shared/utilities/functionUtils' @@ -278,6 +277,7 @@ export class AuthUtil implements IAuthProvider { } private async cacheChangedHandler(event: cacheChangedEvent) { + getLogger().debug(`Auth: Cache change event received: ${event}`) if (event === 'delete') { await this.logout() } else if (event === 'create') { @@ -427,25 +427,20 @@ export class AuthUtil implements IAuthProvider { const cacheDir = getCacheDir() - const hash = (str: string) => { - const hasher = crypto.createHash('sha1') - return hasher.update(str).digest('hex') - } - const filePath = (str: string) => { - return path.join(cacheDir, hash(str) + '.json') - } - const fromRegistrationFile = getRegistrationCacheFile(cacheDir, registrationKey) - const toRegistrationFile = filePath( - JSON.stringify({ - region: toImport.ssoRegion, - startUrl: toImport.startUrl, - tool: clientName, - }) + const toRegistrationFile = path.join( + cacheDir, + getFlareCacheFileName( + JSON.stringify({ + region: toImport.ssoRegion, + startUrl: toImport.startUrl, + tool: clientName, + }) + ) ) const fromTokenFile = getTokenCacheFile(cacheDir, profileId) - const toTokenFile = filePath(this.profileName) + const toTokenFile = path.join(cacheDir, getFlareCacheFileName(this.profileName)) try { await fs.rename(fromRegistrationFile, toRegistrationFile) From ca75b50d503f2c245fea3d0ec4bad8fd51b3b0da Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Mon, 19 May 2025 15:40:35 -0400 Subject: [PATCH 38/48] fix(amazonq): register commands before invocation (#7346) ## Problem Commands `updateReferenceLog` and `refreshStatusBar` are not yet registered when invoked in `AuthUtil` ## Solution Register the commands before `activateAmazonqLsp`, instead of in `activateCodeWhisperer` --- - 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/extension.ts | 23 +++++++++++++++---- packages/core/src/codewhisperer/activation.ts | 6 ----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 5cef3994b7a..b034f360cec 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -3,8 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { authUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth' -import { activate as activateCodeWhisperer, shutdown as shutdownCodeWhisperer } from 'aws-core-vscode/codewhisperer' +import { authUtils, CredentialsStore, LoginManager } from 'aws-core-vscode/auth' +import { + activate as activateCodeWhisperer, + refreshStatusBar, + shutdown as shutdownCodeWhisperer, + updateReferenceLog, +} from 'aws-core-vscode/codewhisperer' import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode' import { CommonAuthWebview } from 'aws-core-vscode/login' import { @@ -113,16 +118,16 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is await activateTelemetry(context, globals.awsContext, Settings.instance, 'Amazon Q For VS Code') - await initializeAuth(globals.loginManager) - const extContext = { extensionContext: context, } + activateAuthDependentCommands() + // Auth is dependent on LSP, needs to be activated before CW and Inline await activateAmazonqLsp(context) - // This contains every lsp agnostic things (auth, security scan, code scan) + // This contains every lsp agnostic things (security scan, code scan) await activateCodeWhisperer(extContext as ExtContext) if (!Experiments.instance.get('amazonqLSPInline', false)) { await activateInlineCompletion() @@ -185,6 +190,14 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is } }) ) + + // Activate commands that are required for activateAmazonqLsp + function activateAuthDependentCommands() { + // update reference log instance + updateReferenceLog.register() + // refresh codewhisperer status bar + refreshStatusBar.register() + } } export async function deactivateCommon() { diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 57ca91e602f..ab260dfc5e1 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -27,7 +27,6 @@ import { showLearnMore, showSsoSignIn, showFreeTierLimit, - updateReferenceLog, showIntroduction, reconnect, openSecurityIssuePanel, @@ -62,7 +61,6 @@ import { } from './service/diagnosticsProvider' import { SecurityPanelViewProvider, openEditorAtRange } from './views/securityPanelViewProvider' import { Commands, registerCommandErrorHandler, registerDeclaredCommands } from '../shared/vscode/commands2' -import { refreshStatusBar } from './service/statusBar' import { AuthUtil } from './util/authUtil' import { ImportAdderProvider } from './service/importAdderProvider' import { openUrl } from '../shared/utilities/vsCodeUtils' @@ -235,10 +233,6 @@ export async function activate(context: ExtContext): Promise { showLearnMore.register(), // show free tier limit showFreeTierLimit.register(), - // update reference log instance - updateReferenceLog.register(), - // refresh codewhisperer status bar - refreshStatusBar.register(), // generate code fix generateFix.register(client, context), // regenerate code fix From 813efa0ace70d608d8313f17582564153366340c Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Tue, 20 May 2025 12:21:56 -0400 Subject: [PATCH 39/48] fix(customizations): Fix bug where customizations were always registered as new (#7352) ## Problem All cached customizations are always marked as new, so a notification for new customizations always appears when switching profiles. ## Solution Fix the bug by flattening the cached customization array and add some unit tests --- - 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. --- .../codewhisperer/util/customizationUtil.ts | 9 +++- .../codewhisperer/customizationUtil.test.ts | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/test/codewhisperer/customizationUtil.test.ts diff --git a/packages/core/src/codewhisperer/util/customizationUtil.ts b/packages/core/src/codewhisperer/util/customizationUtil.ts index ed3812166e4..04bb85d7a43 100644 --- a/packages/core/src/codewhisperer/util/customizationUtil.ts +++ b/packages/core/src/codewhisperer/util/customizationUtil.ts @@ -89,7 +89,14 @@ export const onProfileChangedListener: (event: ProfileChangedEvent) => any = asy */ export const getNewCustomizations = (availableCustomizations: Customization[]) => { const persistedCustomizations = getPersistedCustomizations() - return availableCustomizations.filter((c) => !persistedCustomizations.map((p) => p.arn).includes(c.arn)) + const newCustomizations = availableCustomizations.filter( + (c) => + !persistedCustomizations + .flat() + .map((p) => p.arn) + .includes(c.arn) + ) + return newCustomizations } export async function notifyNewCustomizations() { diff --git a/packages/core/src/test/codewhisperer/customizationUtil.test.ts b/packages/core/src/test/codewhisperer/customizationUtil.test.ts new file mode 100644 index 00000000000..ab81a50bafb --- /dev/null +++ b/packages/core/src/test/codewhisperer/customizationUtil.test.ts @@ -0,0 +1,47 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as assert from 'assert' +import * as customizationModule from '../../../src/codewhisperer/util/customizationUtil' + +describe('getNewCustomizations', () => { + let getPersistedCustomizationsStub: sinon.SinonStub + + const availableCustomizations = [ + { arn: 'arn1', name: 'custom1' }, + { arn: 'arn2', name: 'custom2' }, + ] + + const persistedCustomizations = [[{ arn: 'arn1', name: 'custom1' }], [{ arn: 'arn2', name: 'custom2' }]] + + beforeEach(() => { + getPersistedCustomizationsStub = sinon.stub(customizationModule, 'getPersistedCustomizations') + }) + + afterEach(() => { + sinon.restore() + }) + + it('returns new customizations that are not in persisted customizations', () => { + const customizations = [...availableCustomizations, { arn: 'arn3', name: 'custom3' }] + + getPersistedCustomizationsStub.returns(persistedCustomizations) + + const result = customizationModule.getNewCustomizations(customizations) + + assert.deepEqual(result, [{ arn: 'arn3', name: 'custom3' }]) + sinon.assert.calledOnce(getPersistedCustomizationsStub) + }) + + it('returns empty array when all available customizations are persisted', () => { + getPersistedCustomizationsStub.returns(persistedCustomizations) + + const result = customizationModule.getNewCustomizations(availableCustomizations) + + assert.deepEqual(result.length, 0) + sinon.assert.calledOnce(getPersistedCustomizationsStub) + }) +}) From 7983fc1886724ce5cfd3f18f414bab0c0448de72 Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Thu, 22 May 2025 10:09:04 -0400 Subject: [PATCH 40/48] fix(amazonq): throw if no region profiles are available (#7357) ## Problem When ListRegionProfile call throttles for a subset of regions, we currently do not throw, but instead return the available profiles in the regions where the call succeeded. However, if that list is empty (no profiles in that region), we return an empty list. This breaks the UI, and causes a state that is not recoverable ## Solution Throw an error in the scenario where availableProfiles is empty. This triggers a retry state in the UI, making the state recoverable. --- - 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. --- .../codewhisperer/region/regionProfileManager.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index 4ba64570521..a3d30816056 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -182,9 +182,17 @@ export class RegionProfileManager { } } - // Only throw error if all regions fail - if (failedRegions.length === endpoints.size) { - throw new Error(`Failed to list profiles for all regions: ${failedRegions.join(', ')}`) + // Throw error if any regional API calls failed and no profiles are available + if (failedRegions.length > 0 && availableProfiles.length === 0) { + throw new ToolkitError(`Failed to list Q Developer profiles for regions: ${failedRegions.join(', ')}`, { + code: 'ListQDeveloperProfilesFailed', + }) + } + + // Throw an error if all listAvailableProfile calls succeeded, but user has no Q developer profiles + // This is not an expected state + if (failedRegions.length === 0 && availableProfiles.length === 0) { + throw new ToolkitError('This user has no Q Developer profiles', { code: 'QDeveloperProfileNotFound' }) } this._profiles = availableProfiles From e411922c1c91f68e271891c30e2f2acb080b7fe2 Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Fri, 23 May 2025 09:00:21 -0400 Subject: [PATCH 41/48] fix(amazonq): Handle developer profile migration more gracefully (#7362) ## Problem The cached/persisted Q Developer Profile selection is stored as a map of `{connectionID: RegionProfile}`, where `connectionID` is a `randomUUID`. When migrating to Flare auth, we move away from the concept of a connectionID, and we do not have access to the latest ID of a user. The result is that we cannot restore the user's last selected region profile, and always need users who update versions to make a profile selection. ## Solution To handle this more gracefully, we will: * Use regionProfile if matching auth profile name (existing logic) * If no match, check if there is only a single RegionProfile stored in lastUsed. If so, use that one * If no match, and multiple RegionProfiles are stored in lastUsed cache, make user select Unit tests added --- - 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. --- .../region/regionProfileManager.test.ts | 59 ++++++++++++++++--- .../region/regionProfileManager.ts | 31 +++++++--- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts index aa79e9052bd..a77e47e33ab 100644 --- a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts @@ -9,6 +9,7 @@ import { AuthUtil, RegionProfile, RegionProfileManager, defaultServiceConfig } f import { globals } from 'aws-core-vscode/shared' import { constants } from 'aws-core-vscode/auth' import { createTestAuthUtil } from 'aws-core-vscode/test' +import { randomUUID } from 'crypto' const enterpriseSsoStartUrl = 'https://enterprise.awsapps.com/start' const region = 'us-east-1' @@ -158,7 +159,7 @@ describe('RegionProfileManager', async function () { }) }) - describe('persistence', function () { + describe('persistSelectedRegionProfile', function () { it('persistSelectedRegionProfile', async function () { await setupConnection('idc') await regionProfileManager.switchRegionProfile(profileFoo, 'user') @@ -177,14 +178,13 @@ describe('RegionProfileManager', async function () { assert.strictEqual(state[AuthUtil.instance.profileName], profileFoo) }) + }) - it(`restoreRegionProfile`, async function () { - sinon.stub(regionProfileManager, 'listRegionProfile').resolves([profileFoo]) + describe('restoreRegionProfile', function () { + beforeEach(async function () { await setupConnection('idc') - if (!AuthUtil.instance.isConnected()) { - fail('connection should not be undefined') - } - + }) + it('restores region profile if profile name matches', async function () { const state = {} as any state[AuthUtil.instance.profileName] = profileFoo @@ -194,6 +194,51 @@ describe('RegionProfileManager', async function () { assert.strictEqual(regionProfileManager.activeRegionProfile, profileFoo) }) + + it('returns early when no profiles exist', async function () { + const state = {} as any + state[AuthUtil.instance.profileName] = undefined + + await globals.globalState.update('aws.amazonq.regionProfiles', state) + + await regionProfileManager.restoreRegionProfile() + assert.strictEqual(regionProfileManager.activeRegionProfile, undefined) + }) + + it('returns early when no profile name matches, and multiple profiles exist', async function () { + const state = {} as any + state[AuthUtil.instance.profileName] = undefined + state[randomUUID()] = profileFoo + + await globals.globalState.update('aws.amazonq.regionProfiles', state) + + await regionProfileManager.restoreRegionProfile() + assert.strictEqual(regionProfileManager.activeRegionProfile, undefined) + }) + + it('uses single profile when no profile name matches', async function () { + const state = {} as any + state[randomUUID()] = profileFoo + + await globals.globalState.update('aws.amazonq.regionProfiles', state) + + await regionProfileManager.restoreRegionProfile() + + assert.strictEqual(regionProfileManager.activeRegionProfile, profileFoo) + }) + + it('handles cross-validation failure', async function () { + const state = { + [AuthUtil.instance.profileName]: profileFoo, + } + sinon.stub(regionProfileManager, 'loadPersistedRegionProfiles').returns(state) + sinon.stub(regionProfileManager, 'getProfiles').resolves([]) // No matching profile + const invalidateStub = sinon.stub(regionProfileManager, 'invalidateProfile') + + await regionProfileManager.restoreRegionProfile() + + assert.ok(invalidateStub.calledWith(profileFoo.arn)) + }) }) describe('invalidate', function () { diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index a3d30816056..2645c573249 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -38,7 +38,7 @@ const endpoints = createConstantMap({ 'eu-central-1': 'https://q.eu-central-1.amazonaws.com/', }) -const getRegionProfile = () => +const getRegionProfiles = () => globals.globalState.tryGet<{ [label: string]: RegionProfile }>('aws.amazonq.regionProfiles', Object, {}) /** @@ -86,9 +86,9 @@ export class RegionProfileManager { // This is a poller that handles synchornization of selected region profiles between different IDE windows. // It checks for changes in global state of region profile, invoking the change handler to switch profiles public globalStatePoller = GlobalStatePoller.create({ - getState: getRegionProfile, + getState: getRegionProfiles, changeHandler: async () => { - const profile = this.loadPersistedRegionProfle() + const profile = this.loadPersistedRegionProfiles() void this._switchRegionProfile(profile[this.authProvider.profileName], 'reload') }, pollIntervalInMs: 2000, @@ -285,10 +285,23 @@ export class RegionProfileManager { // Note: should be called after [this.authProvider.isConnected()] returns non null async restoreRegionProfile() { - const previousSelected = this.loadPersistedRegionProfle()[this.authProvider.profileName] || undefined - if (!previousSelected) { + const profiles = this.loadPersistedRegionProfiles() + if (!profiles || Object.keys(profiles).length === 0) { return } + + let previousSelected = profiles[this.authProvider.profileName] + + // If no profile matches auth profileName and there are multiple profiles, return so user can select + if (!previousSelected && Object.keys(profiles).length > 1) { + return + } + + // If no profile matches auth profileName but there's only one profile, use that one + if (!previousSelected && Object.keys(profiles).length === 1) { + previousSelected = Object.values(profiles)[0] + } + // cross-validation this.getProfiles() .then(async (profiles) => { @@ -319,8 +332,8 @@ export class RegionProfileManager { await this.switchRegionProfile(previousSelected, 'reload') } - private loadPersistedRegionProfle(): { [label: string]: RegionProfile } { - return getRegionProfile() + public loadPersistedRegionProfiles(): { [label: string]: RegionProfile } { + return getRegionProfiles() } async persistSelectRegionProfile() { @@ -330,7 +343,7 @@ export class RegionProfileManager { } // persist connectionId to profileArn - const previousPersistedState = getRegionProfile() + const previousPersistedState = getRegionProfiles() previousPersistedState[this.authProvider.profileName] = this.activeRegionProfile await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState) @@ -379,7 +392,7 @@ export class RegionProfileManager { this._activeRegionProfile = undefined } - const profiles = this.loadPersistedRegionProfle() + const profiles = this.loadPersistedRegionProfiles() const updatedProfiles = Object.fromEntries( Object.entries(profiles).filter(([connId, profile]) => profile.arn !== arn) ) From 0ddbf4a928089db47a7a518c713883d0e6e8c33f Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Fri, 23 May 2025 10:03:52 -0400 Subject: [PATCH 42/48] fix(amazonq): handle existing Flare connection when migrating SSO connections (#7363) ## Problem Auth migration to LSP is not handled gracefully when a user downgrades and upgrades to auth on LSP multiple times, causing users to be logged out if they upgrade a second time ## Solution In the auth migration script, call the LSP identity server to check if a token is available. If the token is available, don't migrate the auth connection. If no token is available, migrate. Added unit tests for the case. --- - 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. --- .../unit/codewhisperer/util/authUtil.test.ts | 25 ++++ .../core/src/codewhisperer/util/authUtil.ts | 112 +++++++++++------- packages/core/src/shared/logger/logger.ts | 1 + 3 files changed, 93 insertions(+), 45 deletions(-) diff --git a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts index 97e245fecd3..1795639e1e2 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts @@ -225,6 +225,7 @@ describe('AuthUtil', async function () { }) describe('migrateSsoConnectionToLsp', function () { + let mockLspAuth: any let memento: any let cacheDir: string let fromRegistrationFile: string @@ -250,6 +251,9 @@ describe('AuthUtil', async function () { sinon.stub(mementoUtils, 'getEnvironmentSpecificMemento').returns(memento) sinon.stub(cache, 'getCacheDir').returns(cacheDir) + mockLspAuth = (auth as any).lspAuth + mockLspAuth.getSsoToken.resolves(undefined) + fromTokenFile = cache.getTokenCacheFile(cacheDir, 'profile1') const registrationKey = { startUrl: validProfile.startUrl, @@ -269,6 +273,27 @@ describe('AuthUtil', async function () { sinon.restore() }) + it('skips migration if LSP token exists', async function () { + memento.get.returns({ profile1: validProfile }) + mockLspAuth.getSsoToken.resolves({ token: 'valid-token' }) + + await auth.migrateSsoConnectionToLsp('test-client') + + assert.ok(memento.update.calledWith('auth.profiles', undefined)) + assert.ok(!auth.session.updateProfile?.called) + }) + + it('proceeds with migration if LSP token check throws', async function () { + memento.get.returns({ profile1: validProfile }) + mockLspAuth.getSsoToken.rejects(new Error('Token check failed')) + const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves() + + await auth.migrateSsoConnectionToLsp('test-client') + + assert.ok(updateProfileStub.calledOnce) + assert.ok(memento.update.calledWith('auth.profiles', undefined)) + }) + it('migrates valid SSO connection', async function () { memento.get.returns({ profile1: validProfile }) diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index bb1a4d11366..1419eaa4772 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -39,6 +39,7 @@ import { getEnvironmentSpecificMemento } from '../../shared/utilities/mementos' import { getCacheDir, getFlareCacheFileName, getRegistrationCacheFile, getTokenCacheFile } from '../../auth/sso/cache' import { notifySelectDeveloperProfile } from '../region/utils' import { once } from '../../shared/utilities/functionUtils' +import { CancellationTokenSource, SsoTokenSourceKind } from '@aws/language-server-runtimes/server-interface' const localize = nls.loadMessageBundle() @@ -64,6 +65,8 @@ export interface IAuthProvider { */ export class AuthUtil implements IAuthProvider { public readonly profileName = VSCODE_EXTENSION_ID.amazonq + protected logger = getLogger('amazonqAuth') + public readonly regionProfileManager: RegionProfileManager // IAM login currently not supported @@ -277,7 +280,7 @@ export class AuthUtil implements IAuthProvider { } private async cacheChangedHandler(event: cacheChangedEvent) { - getLogger().debug(`Auth: Cache change event received: ${event}`) + this.logger.debug(`Cache change event received: ${event}`) if (event === 'delete') { await this.logout() } else if (event === 'create') { @@ -291,7 +294,7 @@ export class AuthUtil implements IAuthProvider { await this.lspAuth.updateBearerToken(params!) return } else { - getLogger().info(`codewhisperer: connection changed to ${e.state}`) + this.logger.info(`codewhisperer: connection changed to ${e.state}`) await this.refreshState(e.state) } } @@ -402,58 +405,77 @@ export class AuthUtil implements IAuthProvider { if (!profiles) { return - } else { - getLogger().info(`codewhisperer: checking for old SSO connections`) - for (const [id, p] of Object.entries(profiles)) { - if (p.type === 'sso' && hasExactScopes(p.scopes ?? [], amazonQScopes)) { - toImport = p - profileId = id - if (p.metadata.connectionState === 'valid') { - break - } - } - } + } - if (toImport && profileId) { - getLogger().info(`codewhisperer: migrating SSO connection to LSP identity server...`) + try { + // Try go get token from LSP auth. If available, skip migration and delete old auth profile + const token = await this.lspAuth.getSsoToken( + { + kind: SsoTokenSourceKind.IamIdentityCenter, + profileName: this.profileName, + }, + false, + new CancellationTokenSource().token + ) + if (token) { + this.logger.info('existing LSP auth connection found. Skipping migration') + await memento.update(key, undefined) + return + } + } catch { + this.logger.info('unable to get token from LSP auth, proceeding migration') + } - const registrationKey = { - startUrl: toImport.startUrl, - region: toImport.ssoRegion, - scopes: amazonQScopes, + this.logger.info('checking for old SSO connections') + for (const [id, p] of Object.entries(profiles)) { + if (p.type === 'sso' && hasExactScopes(p.scopes ?? [], amazonQScopes)) { + toImport = p + profileId = id + if (p.metadata.connectionState === 'valid') { + break } + } + } - await this.session.updateProfile(registrationKey) + if (toImport && profileId) { + this.logger.info('migrating SSO connection to LSP identity server...') - const cacheDir = getCacheDir() + const registrationKey = { + startUrl: toImport.startUrl, + region: toImport.ssoRegion, + scopes: amazonQScopes, + } - const fromRegistrationFile = getRegistrationCacheFile(cacheDir, registrationKey) - const toRegistrationFile = path.join( - cacheDir, - getFlareCacheFileName( - JSON.stringify({ - region: toImport.ssoRegion, - startUrl: toImport.startUrl, - tool: clientName, - }) - ) - ) + await this.session.updateProfile(registrationKey) - const fromTokenFile = getTokenCacheFile(cacheDir, profileId) - const toTokenFile = path.join(cacheDir, getFlareCacheFileName(this.profileName)) + const cacheDir = getCacheDir() - try { - await fs.rename(fromRegistrationFile, toRegistrationFile) - await fs.rename(fromTokenFile, toTokenFile) - getLogger().debug('Successfully renamed registration and token files') - } catch (err) { - getLogger().error(`Failed to rename files during migration: ${err}`) - throw err - } - - await memento.update(key, undefined) - getLogger().info(`codewhisperer: successfully migrated SSO connection to LSP identity server`) + const fromRegistrationFile = getRegistrationCacheFile(cacheDir, registrationKey) + const toRegistrationFile = path.join( + cacheDir, + getFlareCacheFileName( + JSON.stringify({ + region: toImport.ssoRegion, + startUrl: toImport.startUrl, + tool: clientName, + }) + ) + ) + + const fromTokenFile = getTokenCacheFile(cacheDir, profileId) + const toTokenFile = path.join(cacheDir, getFlareCacheFileName(this.profileName)) + + try { + await fs.rename(fromRegistrationFile, toRegistrationFile) + await fs.rename(fromTokenFile, toTokenFile) + this.logger.debug('Successfully renamed registration and token files') + } catch (err) { + this.logger.error(`Failed to rename files during migration: ${err}`) + throw err } + + this.logger.info('successfully migrated SSO connection to LSP identity server') + await memento.update(key, undefined) } } } diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index b398ff93162..bb94fb0dc53 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -21,6 +21,7 @@ export type LogTopic = | 'nextEditPrediction' | 'resourceCache' | 'telemetry' + | 'amazonqAuth' class ErrorLog { constructor( From 453528464c2a168bc8f863f94c2108b49efebb81 Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Fri, 23 May 2025 15:07:39 -0400 Subject: [PATCH 43/48] fix(telemetry): add result parameter when emitting telemetry (#7378) ## Problem Users are seeing `telemetry: invalid Metric: "codewhisperer_clientComponentLatency" emitted without the `result` property, which is always required. Consider using `.run()` instead of `.emit()`, which will set these properties automatically. See https://github.com/aws/aws-toolkit-vscode/blob/master/docs/telemetry.md#guidelines` ## Solution Add result attribute when emitting the metric --- - 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/core/src/codewhisperer/util/telemetryHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index bb4e62cfaba..67566ebfc01 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -659,6 +659,7 @@ export class TelemetryHelper { codewhispererSessionId: session.sessionId, codewhispererTriggerType: session.triggerType, credentialStartUrl: AuthUtil.instance.connection?.startUrl, + result: 'Succeeded', }) } public sendCodeScanEvent(languageId: string, jobId: string) { From 5ca78d56db2b4f8e91caeab9b459dbde47b59c57 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <43144436+aws-toolkit-automation@users.noreply.github.com> Date: Thu, 29 May 2025 12:13:53 -0400 Subject: [PATCH 44/48] Merge master into feature/flare-mega (#7393) ## Automatic merge failed - Resolve conflicts and push to this PR branch. - **Do not squash-merge** this PR. Use the "Create a merge commit" option to do a regular merge. ## Command line hint To perform the merge from the command line, you could do something like the following (where "origin" is the name of the remote in your local git repo): ``` git stash git fetch --all git checkout origin/feature/flare-mega git merge origin/master git commit git push origin HEAD:refs/heads/autoMerge/feature/flare-mega ``` --------- Signed-off-by: nkomonen-amazon Co-authored-by: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Co-authored-by: aws-toolkit-automation <> Co-authored-by: opieter-aws --- CONTRIBUTING.md | 2 +- package-lock.json | 8 +++++--- packages/amazonq/.changes/1.70.0.json | 10 ++++++++++ .../Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json | 4 ---- packages/amazonq/CHANGELOG.md | 4 ++++ packages/amazonq/package.json | 2 +- packages/core/package.json | 2 +- 7 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 packages/amazonq/.changes/1.70.0.json delete mode 100644 packages/amazonq/.changes/next-release/Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04dbdd11a26..9992cd16dcf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -382,7 +382,7 @@ If you need to report an issue attach these to give the most detailed informatio - ![](./docs/images/logsView.png) 2. Click the gear icon on the bottom right and select `Debug` - ![](./docs/images/logsSetDebug.png) -3. Click the gear icon again and select `Set As Default`. This will ensure we stay in `Debug` until explicitly changed +3. Click the gear icon again and select `Set As Default`. This will ensure we stay in `Debug` until explicitly changed. - ![](./docs/images/logsSetDefault.png) 4. Open the Command Palette again and select `Reload Window`. 5. Now you should see additional `[debug]` prefixed logs in the output. diff --git a/package-lock.json b/package-lock.json index 51483c52c1e..9bde57faca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11078,7 +11078,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.30.3", + "version": "4.34.1", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.34.1.tgz", + "integrity": "sha512-CO65lwedf6Iw3a3ULOl+9EHafIekiPlP+n8QciN9a3POfsRamHl0kpBGaGBzBRgsQ/h5R0FvFG/gAuWoiK/YIA==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { @@ -25394,7 +25396,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.70.0-SNAPSHOT", + "version": "1.71.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -25437,7 +25439,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.30.3", + "@aws/mynah-ui": "^4.34.1", "@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/1.70.0.json b/packages/amazonq/.changes/1.70.0.json new file mode 100644 index 00000000000..841e8107430 --- /dev/null +++ b/packages/amazonq/.changes/1.70.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-05-28", + "version": "1.70.0", + "entries": [ + { + "type": "Removal", + "description": "Disable local workspace LSP" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json b/packages/amazonq/.changes/next-release/Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json deleted file mode 100644 index 4c95991dbb6..00000000000 --- a/packages/amazonq/.changes/next-release/Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Removal", - "description": "Disable local workspace LSP" -} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 30d7ee956c1..10c6904fe2a 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.70.0 2025-05-28 + +- **Removal** Disable local workspace LSP + ## 1.69.0 2025-05-22 - **Bug Fix** /transform: avoid prompting user for target JDK path unnecessarily diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 5820ecdff8c..889d5f70967 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.70.0-SNAPSHOT", + "version": "1.71.0-SNAPSHOT", "extensionKind": [ "workspace" ], diff --git a/packages/core/package.json b/packages/core/package.json index f35369cc5b9..67e20d5feb1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -526,7 +526,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.30.3", + "@aws/mynah-ui": "^4.34.1", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", From 8c5111abdcb34d3524d7c23199b83c880c38875e Mon Sep 17 00:00:00 2001 From: opieter-aws Date: Mon, 9 Jun 2025 14:59:53 -0400 Subject: [PATCH 45/48] Lintfix --- packages/amazonq/src/extension.ts | 1 - packages/amazonq/src/lsp/client.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 57444ffb3e9..e0b1c66cdf7 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -39,7 +39,6 @@ import { Experiments, isSageMaker, Commands, - isAmazonLinux2, ProxyUtil, } from 'aws-core-vscode/shared' import { ExtStartUpSources } from 'aws-core-vscode/telemetry' diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 43642420de3..2460af9260c 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -48,7 +48,6 @@ import { getOptOutPreference, isAmazonLinux2, oidcClientName, - openUrl, getClientId, extensionVersion, Commands, From 3966878ecd8292142268ac28614fe915027d3059 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Tue, 10 Jun 2025 08:33:03 -0700 Subject: [PATCH 46/48] fix(amazonq): merge master into feature/flare-mega (#7456) ## Problem ## Solution --- - 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. --------- Co-authored-by: Justin M. Keyes Co-authored-by: chungjac --- ...-57661731-6180-4157-a04b-d3a8b50aa1a8.json | 4 ++++ packages/amazonq/package.json | 22 ++++++++++++++----- packages/amazonq/src/lsp/chat/activation.ts | 14 +++++++++++- packages/amazonq/src/lsp/chat/commands.ts | 2 +- packages/amazonq/src/lsp/chat/messages.ts | 2 +- packages/amazonq/src/lsp/client.ts | 1 + packages/core/package.nls.json | 1 + packages/core/src/auth/index.ts | 2 ++ .../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, 70 insertions(+), 14 deletions(-) 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/package.json b/packages/amazonq/package.json index 09c26af6b06..c7557807a6d 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -387,16 +387,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", @@ -678,6 +683,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/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index f6277068734..2ef8810ca60 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' @@ -78,6 +78,18 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu Commands.register('aws.amazonq.updateCustomizations', () => { pushCustomizationToServer(languageClient) }), + 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 ac3f1836521..c55086fd3fc 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -316,7 +316,7 @@ export function registerMessageListeners( ) 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 diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 2460af9260c..4395ade9a2c 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -150,6 +150,7 @@ export async function startLanguageServer( awsClientCapabilities: { q: { developerProfiles: true, + mcp: true, }, window: { notifications: true, 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/index.ts b/packages/core/src/auth/index.ts index 87a6877844e..2dd361f9804 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -28,3 +28,5 @@ export * as cache from './sso/cache' export * as authUtils from './utils' export * as auth2 from './auth2' export * as SsoAccessTokenProvider from './sso/ssoAccessTokenProvider' +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 0e3090538fb..1d7d6278d79 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -177,6 +177,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 d8c05270073..f9c4b70e960 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(), ...(AuthUtil.instance.isIdcConnection() && 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 7fb7107af47..4d18401a242 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 @@ -490,6 +498,7 @@ describe('CodeWhisperer-basicCommands', function () { switchToAmazonQNode(), ...genericItems(), createSettingsNode(), + createManageSubscription(), createSignout() ) e.dispose() // skip needing to select an item to continue @@ -516,6 +525,7 @@ describe('CodeWhisperer-basicCommands', function () { ...genericItems(), createSeparator(), createSettingsNode(), + createManageSubscription(), createSignout(), ]) e.dispose() // skip needing to select an item to continue @@ -538,6 +548,7 @@ describe('CodeWhisperer-basicCommands', function () { switchToAmazonQNode(), ...genericItems(), createSettingsNode(), + createManageSubscription(), createSignout() ) e.dispose() From ac7628369bf885e7e2a2218ad3eaa7061b44f3b0 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Tue, 10 Jun 2025 11:11:32 -0700 Subject: [PATCH 47/48] merge 2 --- packages/amazonq/src/lsp/chat/activation.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index 2b512537c23..2ef8810ca60 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -90,18 +90,6 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu getLogger('amazonqLsp').error('failed request: aws/chat/manageSubscription: %O', e) }) }), - 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, { From 919cd73bf3dc4c6ac24e51d278c4748b57a6e45a Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:59:31 -0700 Subject: [PATCH 48/48] fix(amazonq): fix git merge caused compile failure (#7475) ## Problem ## Solution --- - 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/core/src/codewhisperer/ui/statusBarMenu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index eae0335c0eb..c1431deadab 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -107,7 +107,7 @@ export function getQuickPickItems(): DataQuickPickItem[] { createSettingsNode(), ...(AuthUtil.instance.isIdcConnection() && regionProfile ? [createSelectRegionProfileNode(regionProfile)] : []), ...(AuthUtil.instance.isConnected() && !hasVendedIamCredentials() && !hasVendedCredentialsFromMetadata() - ? [...(AuthUtil.instance.isBuilderIdInUse() ? [createManageSubscription()] : []), createSignout()] + ? [...(AuthUtil.instance.isBuilderIdConnection() ? [createManageSubscription()] : []), createSignout()] : []), ]