From 3b7082a90278f8fcdf0e9d055470dc2e6a52f03f Mon Sep 17 00:00:00 2001 From: Neil Kulkarni <60868290+neilk-aws@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:14:14 -0700 Subject: [PATCH 01/18] telemetry(amazonq): /dev error classification #6772 ## Problem Today, telemetry emitted from the feature development is classified as error, fault, or LLM-based failure across various parts of the code base. This makes it difficult to know all the error types that exist and how to classify them appropriately when emitting metrics. ## Solution The solution here allows different exceptions to raise from different parts of the code base and uses inheritance to model each error as a client error, service error, or LLM-based error. When emitting telemetry, we now only need to check whether the error received extends from one of these 3 to know how to classify the telemetry. --- .../unit/amazonqFeatureDev/util/files.test.ts | 4 +- packages/core/src/amazonq/errors.ts | 11 ++- packages/core/src/amazonq/index.ts | 1 - packages/core/src/amazonq/util/files.ts | 3 +- .../core/src/amazonqDoc/session/session.ts | 4 +- .../amazonqFeatureDev/client/featureDev.ts | 33 +++++-- .../controllers/chat/controller.ts | 31 +------ packages/core/src/amazonqFeatureDev/errors.ts | 91 +++++++++++++------ .../src/amazonqFeatureDev/session/session.ts | 10 +- .../amazonqFeatureDev/session/sessionState.ts | 33 ++++--- packages/core/src/shared/errors.ts | 24 +++++ .../controllers/chat/controller.test.ts | 20 +--- 12 files changed, 153 insertions(+), 112 deletions(-) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts index badc34f6dd7..574d0a25a19 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts @@ -11,11 +11,11 @@ import { maxRepoSizeBytes, } from 'aws-core-vscode/amazonqFeatureDev' import { assertTelemetry, getWorkspaceFolder, TestFolder } from 'aws-core-vscode/test' -import { fs, AmazonqCreateUpload, ZipStream } from 'aws-core-vscode/shared' +import { fs, AmazonqCreateUpload, ZipStream, ContentLengthError } from 'aws-core-vscode/shared' import { MetricName, Span } from 'aws-core-vscode/telemetry' import sinon from 'sinon' import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' -import { ContentLengthError, CurrentWsFolders } from 'aws-core-vscode/amazonq' +import { CurrentWsFolders } from 'aws-core-vscode/amazonq' import path from 'path' const testDevfilePrepareRepo = async (devfileEnabled: boolean) => { diff --git a/packages/core/src/amazonq/errors.ts b/packages/core/src/amazonq/errors.ts index 799206d7ab9..36e67d7a07e 100644 --- a/packages/core/src/amazonq/errors.ts +++ b/packages/core/src/amazonq/errors.ts @@ -9,10 +9,13 @@ * When thrown from common components, individual agents can catch and transform this error * to provide their own customized error messages. */ -import { ToolkitError } from '../shared/errors' +import { ErrorInformation, ToolkitError } from '../shared/errors' -export class ContentLengthError extends ToolkitError { - constructor(message: string) { - super(message, { code: 'ContentLengthError' }) +/** + * Errors extending this class are considered "LLM failures" in service metrics. + */ +export class LlmError extends ToolkitError { + constructor(message: string, info: ErrorInformation = {}) { + super(message, info) } } diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 59a53ddf658..a16ed4cc438 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -44,7 +44,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 { ContentLengthError } from './errors' import { FeatureContext } from '../shared/featureConfig' /** diff --git a/packages/core/src/amazonq/util/files.ts b/packages/core/src/amazonq/util/files.ts index b7c25c7f887..d64f195d1c8 100644 --- a/packages/core/src/amazonq/util/files.ts +++ b/packages/core/src/amazonq/util/files.ts @@ -16,7 +16,7 @@ import { PrepareRepoFailedError } from '../../amazonqFeatureDev/errors' import { getLogger } from '../../shared/logger/logger' import { maxFileSizeBytes } from '../../amazonqFeatureDev/limits' import { CurrentWsFolders, DeletedFileInfo, NewFileInfo, NewFileZipContents } from '../../amazonqDoc/types' -import { hasCode, ToolkitError } from '../../shared/errors' +import { ContentLengthError, hasCode, ToolkitError } from '../../shared/errors' import { AmazonqCreateUpload, Span, telemetry as amznTelemetry, telemetry } from '../../shared/telemetry/telemetry' import { maxRepoSizeBytes } from '../../amazonqFeatureDev/constants' import { isCodeFile } from '../../shared/filetypes' @@ -28,7 +28,6 @@ import { ZipStream } from '../../shared/utilities/zipStream' import { isPresent } from '../../shared/utilities/collectionUtils' import { AuthUtil } from '../../codewhisperer/util/authUtil' import { TelemetryHelper } from '../util/telemetryHelper' -import { ContentLengthError } from '../errors' export const SvgFileExtension = '.svg' diff --git a/packages/core/src/amazonqDoc/session/session.ts b/packages/core/src/amazonqDoc/session/session.ts index 301a9d0e0fa..ac4a3052694 100644 --- a/packages/core/src/amazonqDoc/session/session.ts +++ b/packages/core/src/amazonqDoc/session/session.ts @@ -30,8 +30,8 @@ import fs from '../../shared/fs/fs' import globals from '../../shared/extensionGlobals' import { extensionVersion } from '../../shared/vscode/env' import { getLogger } from '../../shared/logger/logger' +import { ContentLengthError as CommonContentLengthError } from '../../shared/errors' import { ContentLengthError } from '../errors' -import { ContentLengthError as CommonAmazonQContentLengthError } from '../../amazonq/errors' export class Session { private _state?: SessionState | Omit @@ -152,7 +152,7 @@ export class Session { return resp.interaction } catch (e) { - if (e instanceof CommonAmazonQContentLengthError) { + if (e instanceof CommonContentLengthError) { getLogger().debug(`Content length validation failed: ${e.message}`) throw new ContentLengthError() } diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts index f025ac1743f..74692148e70 100644 --- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts +++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts @@ -14,12 +14,14 @@ import { featureName, startTaskAssistLimitReachedMessage } from '../constants' import { CodeReference } from '../../amazonq/webview/ui/connector' import { ApiError, + ApiServiceError, CodeIterationLimitError, ContentLengthError, + FeatureDevServiceError, MonthlyConversationLimitError, UnknownApiError, } from '../errors' -import { ToolkitError, isAwsError } from '../../shared/errors' +import { isAwsError } from '../../shared/errors' import { getCodewhispererConfig } from '../../codewhisperer/client/codewhisperer' import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient' import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util' @@ -93,7 +95,7 @@ export class FeatureDevClient implements FeatureClient { ) { throw new MonthlyConversationLimitError(e.message) } - throw new ApiError(e.message, 'CreateConversation', e.code, e.statusCode ?? 400) + throw ApiError.of(e.message, 'CreateConversation', e.code, e.statusCode ?? 500) } throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'CreateConversation') @@ -136,7 +138,7 @@ export class FeatureDevClient implements FeatureClient { if (e.code === 'ValidationException' && e.message.includes('Invalid contentLength')) { throw new ContentLengthError() } - throw new ApiError(e.message, 'CreateUploadUrl', e.code, e.statusCode ?? 400) + throw ApiError.of(e.message, 'CreateUploadUrl', e.code, e.statusCode ?? 500) } throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'CreateUploadUrl') @@ -198,8 +200,10 @@ export class FeatureDevClient implements FeatureClient { ) { throw new CodeIterationLimitError() } + throw ApiError.of(e.message, 'StartTaskAssistCodeGeneration', e.code, e.statusCode ?? 500) } - throw new ToolkitError((e as Error).message, { code: 'StartCodeGenerationFailed' }) + + throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'StartTaskAssistCodeGeneration') } } @@ -220,7 +224,12 @@ export class FeatureDevClient implements FeatureClient { (e as any).requestId }` ) - throw new ToolkitError((e as Error).message, { code: 'GetCodeGenerationFailed' }) + + if (isAwsError(e)) { + throw ApiError.of(e.message, 'GetTaskAssistCodeGeneration', e.code, e.statusCode ?? 500) + } + + throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'GetTaskAssistCodeGeneration') } } @@ -235,7 +244,12 @@ export class FeatureDevClient implements FeatureClient { const archiveResponse = await streamingClient.exportResultArchive(params) const buffer: number[] = [] if (archiveResponse.body === undefined) { - throw new ToolkitError('Empty response from CodeWhisperer Streaming service.') + throw new ApiServiceError( + 'Empty response from CodeWhisperer Streaming service.', + 'ExportResultArchive', + 'EmptyResponse', + 500 + ) } for await (const chunk of archiveResponse.body) { if (chunk.internalServerException !== undefined) { @@ -274,7 +288,12 @@ export class FeatureDevClient implements FeatureClient { (e as any).requestId }` ) - throw new ToolkitError((e as Error).message, { code: 'ExportResultArchiveFailed' }) + + if (isAwsError(e)) { + throw ApiError.of(e.message, 'ExportResultArchive', e.code, e.statusCode ?? 500) + } + + throw new FeatureDevServiceError(e instanceof Error ? e.message : 'Unknown error', 'ExportResultArchive') } } diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index f2220221b20..6a05f451f8b 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -15,7 +15,7 @@ import { createUserFacingErrorMessage, denyListedErrors, FeatureDevServiceError, - isAPIClientError, + getMetricResult, MonthlyConversationLimitError, NoChangeRequiredException, PrepareRepoFailedError, @@ -550,34 +550,7 @@ export class FeatureDevController { this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption')) } catch (err: any) { getLogger().error(`${featureName}: Error during code generation: ${err}`) - - let result: string - switch (err.constructor.name) { - case FeatureDevServiceError.name: - if (err.code === 'EmptyPatchException') { - result = MetricDataResult.LlmFailure - } else if (err.code === 'GuardrailsException' || err.code === 'ThrottlingException') { - result = MetricDataResult.Error - } else { - result = MetricDataResult.Fault - } - break - case MonthlyConversationLimitError.name: - case CodeIterationLimitError.name: - case PromptRefusalException.name: - case NoChangeRequiredException.name: - result = MetricDataResult.Error - break - default: - if (isAPIClientError(err)) { - result = MetricDataResult.Error - } else { - result = MetricDataResult.Fault - } - break - } - - await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, result) + await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, getMetricResult(err)) throw err } finally { // Finish processing the event diff --git a/packages/core/src/amazonqFeatureDev/errors.ts b/packages/core/src/amazonqFeatureDev/errors.ts index f64217f94ad..2eb142f765b 100644 --- a/packages/core/src/amazonqFeatureDev/errors.ts +++ b/packages/core/src/amazonqFeatureDev/errors.ts @@ -3,17 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ToolkitError } from '../shared/errors' -import { - featureName, - clientErrorMessages, - startCodeGenClientErrorMessages, - startTaskAssistLimitReachedMessage, -} from './constants' +import { featureName, clientErrorMessages, startTaskAssistLimitReachedMessage } from './constants' import { uploadCodeError } from './userFacingText' import { i18n } from '../shared/i18n-helper' +import { LlmError } from '../amazonq/errors' +import { MetricDataResult } from '../amazonq/commons/types' +import { + ClientError, + ServiceError, + ContentLengthError as CommonContentLengthError, + ToolkitError, +} from '../shared/errors' -export class ConversationIdNotFoundError extends ToolkitError { +export class ConversationIdNotFoundError extends ServiceError { constructor() { super(i18n('AWS.amazonq.featureDev.error.conversationIdNotFoundError'), { code: 'ConversationIdNotFound', @@ -21,7 +23,7 @@ export class ConversationIdNotFoundError extends ToolkitError { } } -export class TabIdNotFoundError extends ToolkitError { +export class TabIdNotFoundError extends ServiceError { constructor() { super(i18n('AWS.amazonq.featureDev.error.tabIdNotFoundError'), { code: 'TabIdNotFound', @@ -29,7 +31,7 @@ export class TabIdNotFoundError extends ToolkitError { } } -export class WorkspaceFolderNotFoundError extends ToolkitError { +export class WorkspaceFolderNotFoundError extends ServiceError { constructor() { super(i18n('AWS.amazonq.featureDev.error.workspaceFolderNotFoundError'), { code: 'WorkspaceFolderNotFound', @@ -37,7 +39,7 @@ export class WorkspaceFolderNotFoundError extends ToolkitError { } } -export class UserMessageNotFoundError extends ToolkitError { +export class UserMessageNotFoundError extends ServiceError { constructor() { super(i18n('AWS.amazonq.featureDev.error.userMessageNotFoundError'), { code: 'MessageNotFound', @@ -45,7 +47,7 @@ export class UserMessageNotFoundError extends ToolkitError { } } -export class SelectedFolderNotInWorkspaceFolderError extends ToolkitError { +export class SelectedFolderNotInWorkspaceFolderError extends ClientError { constructor() { super(i18n('AWS.amazonq.featureDev.error.selectedFolderNotInWorkspaceFolderError'), { code: 'SelectedFolderNotInWorkspaceFolder', @@ -53,7 +55,7 @@ export class SelectedFolderNotInWorkspaceFolderError extends ToolkitError { } } -export class PromptRefusalException extends ToolkitError { +export class PromptRefusalException extends ClientError { constructor() { super(i18n('AWS.amazonq.featureDev.error.promptRefusalException'), { code: 'PromptRefusalException', @@ -61,7 +63,7 @@ export class PromptRefusalException extends ToolkitError { } } -export class NoChangeRequiredException extends ToolkitError { +export class NoChangeRequiredException extends ClientError { constructor() { super(i18n('AWS.amazonq.featureDev.error.noChangeRequiredException'), { code: 'NoChangeRequiredException', @@ -69,13 +71,13 @@ export class NoChangeRequiredException extends ToolkitError { } } -export class FeatureDevServiceError extends ToolkitError { +export class FeatureDevServiceError extends ServiceError { constructor(message: string, code: string) { super(message, { code }) } } -export class PrepareRepoFailedError extends ToolkitError { +export class PrepareRepoFailedError extends ServiceError { constructor() { super(i18n('AWS.amazonq.featureDev.error.prepareRepoFailedError'), { code: 'PrepareRepoFailed', @@ -83,60 +85,81 @@ export class PrepareRepoFailedError extends ToolkitError { } } -export class UploadCodeError extends ToolkitError { +export class UploadCodeError extends ServiceError { constructor(statusCode: string) { super(uploadCodeError, { code: `UploadCode-${statusCode}` }) } } -export class UploadURLExpired extends ToolkitError { +export class UploadURLExpired extends ClientError { constructor() { super(i18n('AWS.amazonq.featureDev.error.uploadURLExpired'), { code: 'UploadURLExpired' }) } } -export class IllegalStateTransition extends ToolkitError { +export class IllegalStateTransition extends ServiceError { constructor() { super(i18n('AWS.amazonq.featureDev.error.illegalStateTransition'), { code: 'IllegalStateTransition' }) } } -export class ContentLengthError extends ToolkitError { +export class IllegalStateError extends ServiceError { + constructor(message: string) { + super(message, { code: 'IllegalStateTransition' }) + } +} + +export class ContentLengthError extends CommonContentLengthError { constructor() { super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.name }) } } -export class ZipFileError extends ToolkitError { +export class ZipFileError extends ServiceError { constructor() { super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.name }) } } -export class CodeIterationLimitError extends ToolkitError { +export class CodeIterationLimitError extends ClientError { constructor() { super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.name }) } } -export class MonthlyConversationLimitError extends ToolkitError { +export class MonthlyConversationLimitError extends ClientError { constructor(message: string) { super(message, { code: MonthlyConversationLimitError.name }) } } -export class UnknownApiError extends ToolkitError { +export class UnknownApiError extends ServiceError { constructor(message: string, api: string) { super(message, { code: `${api}-Unknown` }) } } -export class ApiError extends ToolkitError { +export class ApiClientError extends ClientError { + constructor(message: string, api: string, errorName: string, errorCode: number) { + super(message, { code: `${api}-${errorName}-${errorCode}` }) + } +} + +export class ApiServiceError extends ServiceError { constructor(message: string, api: string, errorName: string, errorCode: number) { super(message, { code: `${api}-${errorName}-${errorCode}` }) } } +export class ApiError { + static of(message: string, api: string, errorName: string, errorCode: number) { + if (errorCode >= 400 && errorCode < 500) { + return new ApiClientError(message, api, errorName, errorCode) + } + return new ApiServiceError(message, api, errorName, errorCode) + } +} + export const denyListedErrors: string[] = ['Deserialization error', 'Inaccessible host'] export function createUserFacingErrorMessage(message: string) { @@ -146,11 +169,23 @@ export function createUserFacingErrorMessage(message: string) { return message } -export function isAPIClientError(error: { code?: string; message: string }): boolean { +function isAPIClientError(error: { code?: string; message: string }): boolean { return ( - (error.code === 'StartCodeGenerationFailed' && - startCodeGenClientErrorMessages.some((msg: string) => error.message.includes(msg))) || clientErrorMessages.some((msg: string) => error.message.includes(msg)) || error.message.includes(startTaskAssistLimitReachedMessage) ) } + +export function getMetricResult(error: ToolkitError): MetricDataResult { + if (error instanceof ClientError || isAPIClientError(error)) { + return MetricDataResult.Error + } + if (error instanceof ServiceError) { + return MetricDataResult.Fault + } + if (error instanceof LlmError) { + return MetricDataResult.LlmFailure + } + + return MetricDataResult.Fault +} diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts index 6d692c297a4..d2b174f0f6f 100644 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ b/packages/core/src/amazonqFeatureDev/session/session.ts @@ -14,7 +14,7 @@ import { type SessionStateConfig, UpdateFilesPathsParams, } from '../../amazonq/commons/types' -import { ContentLengthError, ConversationIdNotFoundError } from '../errors' +import { ContentLengthError, ConversationIdNotFoundError, IllegalStateError } from '../errors' import { featureDevChat, referenceLogText, featureDevScheme } from '../constants' import fs from '../../shared/fs/fs' import { FeatureDevClient } from '../client/featureDev' @@ -33,7 +33,7 @@ import { UpdateAnswerMessage } from '../../amazonq/commons/connector/connectorMe import { FollowUpTypes } from '../../amazonq/commons/types' import { SessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { ContentLengthError as CommonAmazonQContentLengthError } from '../../amazonq/errors' +import { ContentLengthError as CommonContentLengthError } from '../../shared/errors' export class Session { private _state?: SessionState | Omit private task: string = '' @@ -159,7 +159,7 @@ export class Session { return resp.interaction } catch (e) { - if (e instanceof CommonAmazonQContentLengthError) { + if (e instanceof CommonContentLengthError) { getLogger().debug(`Content length validation failed: ${e.message}`) throw new ContentLengthError() } @@ -353,7 +353,7 @@ export class Session { get state() { if (!this._state) { - throw new Error("State should be initialized before it's read") + throw new IllegalStateError("State should be initialized before it's read") } return this._state } @@ -364,7 +364,7 @@ export class Session { get uploadId() { if (!('uploadId' in this.state)) { - throw new Error("UploadId has to be initialized before it's read") + throw new IllegalStateError("UploadId has to be initialized before it's read") } return this.state.uploadId } diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts index 62f71dbaeec..5890539409f 100644 --- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts +++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts @@ -6,11 +6,11 @@ import { MynahIcons } from '@aws/mynah-ui' import * as path from 'path' import * as vscode from 'vscode' -import { ToolkitError } from '../../shared/errors' import { getLogger } from '../../shared/logger/logger' import { featureDevScheme } from '../constants' import { - FeatureDevServiceError, + ApiClientError, + ApiServiceError, IllegalStateTransition, NoChangeRequiredException, PromptRefusalException, @@ -37,6 +37,7 @@ import { BasePrepareCodeGenState, CreateNextStateParams, } from '../../amazonq/session/sessionState' +import { LlmError } from '../../amazonq/errors' export class ConversationNotStartedState implements Omit { public tokenSource: vscode.CancellationTokenSource @@ -178,9 +179,11 @@ export class FeatureDevCodeGenState extends BaseCodeGenState { protected handleError(messenger: BaseMessenger, codegenResult: any): Error { switch (true) { case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { - return new FeatureDevServiceError( + return new ApiClientError( i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GuardrailsException' + 'GetTaskAssistCodeGeneration', + 'GuardrailsException', + 400 ) } case codegenResult.codeGenerationStatusDetail?.includes('PromptRefusal'): { @@ -190,21 +193,25 @@ export class FeatureDevCodeGenState extends BaseCodeGenState { if (codegenResult.codeGenerationStatusDetail?.includes('NO_CHANGE_REQUIRED')) { return new NoChangeRequiredException() } - return new FeatureDevServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'EmptyPatchException' - ) + return new LlmError(i18n('AWS.amazonq.featureDev.error.codeGen.default'), { + code: 'EmptyPatchException', + }) } case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { - return new FeatureDevServiceError( + return new ApiClientError( i18n('AWS.amazonq.featureDev.error.throttling'), - 'ThrottlingException' + 'GetTaskAssistCodeGeneration', + 'ThrottlingException', + 429 ) } default: { - return new ToolkitError(i18n('AWS.amazonq.featureDev.error.codeGen.default'), { - code: 'CodeGenFailed', - }) + return new ApiServiceError( + i18n('AWS.amazonq.featureDev.error.codeGen.default'), + 'GetTaskAssistCodeGeneration', + 'UnknownException', + 500 + ) } } } diff --git a/packages/core/src/shared/errors.ts b/packages/core/src/shared/errors.ts index 7d2509173e3..26fc0490ee8 100644 --- a/packages/core/src/shared/errors.ts +++ b/packages/core/src/shared/errors.ts @@ -826,6 +826,30 @@ export class PermissionsError extends ToolkitError { } } +/** + * Errors extending this class are considered "errors" in service metrics. + */ +export class ClientError extends ToolkitError { + constructor(message: string, info: ErrorInformation = { code: '400' }) { + super(message, info) + } +} + +/** + * Errors extending this class are considered "faults" in service metrics. + */ +export class ServiceError extends ToolkitError { + constructor(message: string, info: ErrorInformation = { code: '500' }) { + super(message, info) + } +} + +export class ContentLengthError extends ClientError { + constructor(message: string, info: ErrorInformation = { code: 'ContentLengthError' }) { + super(message, info) + } +} + export function isNetworkError(err?: unknown): err is Error & { code: string } { if (!(err instanceof Error)) { return false 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 03d29907e49..7848d0561b0 100644 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts @@ -24,7 +24,7 @@ import { ContentLengthError, createUserFacingErrorMessage, FeatureDevServiceError, - isAPIClientError, + getMetricResult, MonthlyConversationLimitError, NoChangeRequiredException, PrepareRepoFailedError, @@ -471,24 +471,6 @@ describe('Controller', () => { let session: any let sendMetricDataTelemetrySpy: sinon.SinonStub - const errorResultMapping = new Map([ - ['EmptyPatchException', MetricDataResult.LlmFailure], - [PromptRefusalException.name, MetricDataResult.Error], - [NoChangeRequiredException.name, MetricDataResult.Error], - [MonthlyConversationLimitError.name, MetricDataResult.Error], - [CodeIterationLimitError.name, MetricDataResult.Error], - ]) - - function getMetricResult(error: ToolkitError): MetricDataResult { - if (error instanceof FeatureDevServiceError && error.code) { - return errorResultMapping.get(error.code) ?? MetricDataResult.Error - } - if (isAPIClientError(error)) { - return MetricDataResult.Error - } - return errorResultMapping.get(error.constructor.name) ?? MetricDataResult.Fault - } - async function verifyException(error: ToolkitError) { sinon.stub(session, 'send').throws(error) From 9f2fde8783a38e97ba21bc9e5feea9ec9c11416e Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:16:15 -0400 Subject: [PATCH 02/18] fix(amazonq): Improve responses for saved prompts, workspace rules #6805 ## Problem The system prompt for saved prompts and workspace rules is too vague, users reporting they are sometimes ignored entirely. ## Solution Add system prompt for saved prompt and workspace rules to improve quality of responses --- ...Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json | 4 ++++ .../controllers/chat/controller.ts | 9 +++++++-- .../controllers/chat/telemetryHelper.ts | 16 +++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json diff --git a/packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json b/packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json new file mode 100644 index 00000000000..e2e4205f6c6 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q chat: Improve responses for saved prompts and workspace rules" +} diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index e1a41d69a23..846f3c6e445 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -980,9 +980,15 @@ export class ChatController { if (Array.isArray(prompts) && prompts.length > 0) { triggerPayload.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 const entry = { name: prompt.name.substring(0, aditionalContentNameLimit), - description: prompt.description.substring(0, aditionalContentNameLimit), + description: description.substring(0, aditionalContentNameLimit), innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), } // make sure the relevantDocument + additionalContext @@ -994,7 +1000,6 @@ export class ChatController { break } - const contextType = this.telemetryHelper.getContextType(prompt) if (contextType === 'rule') { triggerPayload.truncatedAdditionalContextLengths.ruleContextLength += entry.innerContext.length } else if (contextType === 'prompt') { diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 8c63eb44972..eb99b8c5dca 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -42,7 +42,8 @@ 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 } from '../../constants' +import { getUserPromptsDirectory, promptFileExtension } from '../../constants' +import { isInDirectory } from '../../../shared/filesystemUtilities' export function logSendTelemetryEventFailure(error: any) { let requestId: string | undefined @@ -148,13 +149,14 @@ export class CWCTelemetryHelper { } public getContextType(prompt: AdditionalContextPrompt): string { - if (prompt.relativePath.startsWith(path.join('.amazonq', 'rules'))) { - return 'rule' - } else if (prompt.filePath.startsWith(getUserPromptsDirectory())) { - return 'prompt' - } else { - return 'file' + 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 { From 6e367e9b7535ad44ed5a96b373b8efe3f7d73537 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 18 Mar 2025 15:20:54 -0700 Subject: [PATCH 03/18] build(deps): remove `@aws/fully-qualified-names` dependency #6807 ## Problem FQN not used since 6484c98c3ecd4a2b665e7ee8fa3d96e8ba9ce935. And it adds ~1 MB to the vsix size. ## Solution - Drop FQN dependency. - Remove "temporary" hack added a long time ago for mynah: b2dc00ab7431 --- package-lock.json | 9 --------- packages/amazonq/scripts/build/copyFiles.ts | 11 ----------- packages/core/package.json | 3 +-- packages/toolkit/scripts/build/copyFiles.ts | 12 ------------ 4 files changed, 1 insertion(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55b0caba934..1d7ededed09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10505,14 +10505,6 @@ "yargs": "^17.0.1" } }, - "node_modules/@aws/fully-qualified-names": { - "version": "2.1.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "web-tree-sitter": "^0.20.8" - } - }, "node_modules/@aws/mynah-ui": { "version": "4.25.1", "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.25.1.tgz", @@ -24399,7 +24391,6 @@ }, "devDependencies": { "@aws-sdk/types": "^3.13.1", - "@aws/fully-qualified-names": "^2.1.4", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", diff --git a/packages/amazonq/scripts/build/copyFiles.ts b/packages/amazonq/scripts/build/copyFiles.ts index 725c66ad7c0..45b1d263f0b 100644 --- a/packages/amazonq/scripts/build/copyFiles.ts +++ b/packages/amazonq/scripts/build/copyFiles.ts @@ -56,17 +56,6 @@ const tasks: CopyTask[] = [ destination: 'vue/', }, - // Mynah - { - target: path.join( - '../../node_modules', - '@aws', - 'fully-qualified-names', - 'node', - 'aws_fully_qualified_names_bg.wasm' - ), - destination: path.join('src', 'aws_fully_qualified_names_bg.wasm'), - }, { target: path.join('../../node_modules', 'web-tree-sitter', 'tree-sitter.wasm'), destination: path.join('src', 'tree-sitter.wasm'), diff --git a/packages/core/package.json b/packages/core/package.json index a1fa291c3b1..3223ca7e0c8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -440,7 +440,6 @@ }, "devDependencies": { "@aws-sdk/types": "^3.13.1", - "@aws/fully-qualified-names": "^2.1.4", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", @@ -497,6 +496,7 @@ "@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client", "@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming", "@aws-sdk/client-api-gateway": "<3.696.0", + "@aws-sdk/client-cloudcontrol": "<3.696.0", "@aws-sdk/client-cloudformation": "<3.696.0", "@aws-sdk/client-cloudwatch-logs": "<3.696.0", "@aws-sdk/client-codecatalyst": "<3.696.0", @@ -510,7 +510,6 @@ "@aws-sdk/client-ssm": "<3.696.0", "@aws-sdk/client-sso": "<3.696.0", "@aws-sdk/client-sso-oidc": "<3.696.0", - "@aws-sdk/client-cloudcontrol": "<3.696.0", "@aws-sdk/credential-provider-env": "<3.696.0", "@aws-sdk/credential-provider-process": "<3.696.0", "@aws-sdk/credential-provider-sso": "<3.696.0", diff --git a/packages/toolkit/scripts/build/copyFiles.ts b/packages/toolkit/scripts/build/copyFiles.ts index 7d065040416..e081a2eb9b4 100644 --- a/packages/toolkit/scripts/build/copyFiles.ts +++ b/packages/toolkit/scripts/build/copyFiles.ts @@ -95,18 +95,6 @@ const tasks: CopyTask[] = [ target: path.join('../../node_modules/aws-core-vscode/dist', 'vue'), destination: 'vue/', }, - - // Mynah - { - target: path.join( - '../../node_modules', - '@aws', - 'fully-qualified-names', - 'node', - 'aws_fully_qualified_names_bg.wasm' - ), - destination: path.join('src', 'aws_fully_qualified_names_bg.wasm'), - }, { target: path.join('../../node_modules', 'web-tree-sitter', 'tree-sitter.wasm'), destination: path.join('src', 'tree-sitter.wasm'), From 070c36d9de2d54b894a44193a6b4d4e24e0826eb Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Tue, 18 Mar 2025 21:49:24 -0700 Subject: [PATCH 04/18] fix(inline-completion): potential inline completion failure due to input validation exception of supplemental context (#6758) ## Problem ERROR: `at 'supplementalContexts' failed to satisfy constraint: Member must have length less than or equal to 5` ERROR: `supplementalContexts.1.member.content' failed to satisfy constraint: Member must have length less than or equal to 10240` ## Solution Jebtrains PR https://github.com/aws/aws-toolkit-jetbrains/pull/5466 * Requirement * - Maximum 5 supplemental context. * - Each chunk can't exceed 10240 characters * - Sum of all chunks can't exceed 20480 characters --- - 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. --- ...-bde47093-ee34-4802-a556-d1e5dc526f4d.json | 4 + .../util/supplemetalContextUtil.test.ts | 181 +++++++++++++++++- .../src/codewhisperer/models/constants.ts | 2 + .../supplementalContextUtil.ts | 69 ++++++- 4 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json diff --git a/packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json b/packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json new file mode 100644 index 00000000000..037bcdbe2ae --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Fix inline completion failure due to context length exceeding the threshold" +} diff --git a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts index 051ac65bee1..a42b0aa6158 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts @@ -7,12 +7,15 @@ 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 { FeatureConfigProvider } from 'aws-core-vscode/codewhisperer' +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 @@ -83,4 +86,180 @@ describe('supplementalContextUtil', function () { }) }) }) + + 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/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 74b4e6d508e..5762f8609da 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -856,6 +856,8 @@ export const crossFileContextConfig = { topK: 3, numberOfLinesEachChunk: 50, maximumTotalLength: 20480, + maxLengthEachChunk: 10240, + maxContextCount: 5, } export const utgConfig = { diff --git a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts index 03f9d59b3f2..bd214ace44e 100644 --- a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts +++ b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts @@ -11,6 +11,8 @@ 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, @@ -36,7 +38,7 @@ export async function fetchSupplementalContext( return supplementalContextPromise .then((value) => { if (value) { - return { + const resBeforeTruncation = { isUtg: isUtg, isProcessTimeout: false, supplementalContextItems: value.supplementalContextItems.filter( @@ -46,6 +48,8 @@ export async function fetchSupplementalContext( latency: performance.now() - timesBeforeFetching, strategy: value.strategy, } + + return truncateSuppelementalContext(resBeforeTruncation) } else { return undefined } @@ -68,3 +72,66 @@ export async function fetchSupplementalContext( } }) } + +/** + * 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 + } +} From 8f7772464ec7021fbbe029c9cbe00ca67a8d9017 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:20:21 -0400 Subject: [PATCH 05/18] feat(amazonq): Add changelog item for enabling inline code suggestions via Amazon Q Language Server (#6800) ## Problem - Missing changelog ## Solution - Add it before releasing --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/amazonq/.changes/next-release/Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json diff --git a/packages/amazonq/.changes/next-release/Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json b/packages/amazonq/.changes/next-release/Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json new file mode 100644 index 00000000000..81a1e026db4 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "(Experimental) Amazon Q inline code suggestions via Amazon Q Language Server. (enable with `aws.experiments.amazonqLSP: true`)" +} From cf8395730f776218485b87c0188f78952e02cfa9 Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:22:06 -0400 Subject: [PATCH 06/18] fix(amazonq): invalid version in language server cache causes crash (#6808) ## Problem If you have a `.DS_STORE` file in the same directory of the language server cache versions your language server will crash ## Solution Only look for valid semver versions when cleaning up your cache --- - 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/shared/lsp/utils/cleanup.ts | 6 +++--- .../core/src/test/shared/lsp/utils/cleanup.test.ts | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/core/src/shared/lsp/utils/cleanup.ts b/packages/core/src/shared/lsp/utils/cleanup.ts index 031afbe0888..83b58e2bb8f 100644 --- a/packages/core/src/shared/lsp/utils/cleanup.ts +++ b/packages/core/src/shared/lsp/utils/cleanup.ts @@ -7,10 +7,10 @@ import path from 'path' import { LspVersion } from '../types' import { fs } from '../../../shared/fs/fs' import { partition } from '../../../shared/utilities/tsUtils' -import { sort } from 'semver' +import { parse, sort } from 'semver' -async function getDownloadedVersions(installLocation: string) { - return (await fs.readdir(installLocation)).map(([f, _], __) => f) +export async function getDownloadedVersions(installLocation: string) { + return (await fs.readdir(installLocation)).filter((x) => parse(x[0]) !== null).map(([f, _], __) => f) } function isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean { diff --git a/packages/core/src/test/shared/lsp/utils/cleanup.test.ts b/packages/core/src/test/shared/lsp/utils/cleanup.test.ts index 75d0654f04f..98f37fff28f 100644 --- a/packages/core/src/test/shared/lsp/utils/cleanup.test.ts +++ b/packages/core/src/test/shared/lsp/utils/cleanup.test.ts @@ -4,7 +4,7 @@ */ import { Uri } from 'vscode' -import { cleanLspDownloads, fs } from '../../../../shared' +import { cleanLspDownloads, fs, getDownloadedVersions } from '../../../../shared' import { createTestWorkspaceFolder } from '../../../testUtil' import path from 'path' import assert from 'assert' @@ -101,4 +101,16 @@ describe('cleanLSPDownloads', function () { assert.strictEqual(result.length, 0) assert.strictEqual(deleted.length, 1) }) + + it('ignores invalid versions', async function () { + await fakeInstallVersions(['1.0.0', '.DS_STORE'], installationDir.fsPath) + const deleted = await cleanLspDownloads( + [{ serverVersion: '1.0.0', isDelisted: true, targets: [] }], + installationDir.fsPath + ) + + const result = await getDownloadedVersions(installationDir.fsPath) + assert.strictEqual(result.length, 0) + assert.strictEqual(deleted.length, 1) + }) }) From 5ff53bae539a64f3f43e816b3529b291c360b756 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:17:01 -0400 Subject: [PATCH 07/18] refactor(workspaceutil): unify file collecting utilities (#6804) ## Problem `collectFiles` and `collectFilesForIndex` serve different purposes, but implement the same core functionality. This core functionality should be not duplicated in two places. ## Solution - Increase testing coverage for both methods. - Refactor `collectFiles` to be general enough to handle `collectFilesForIndex` use case. This includes the ability to avoid reading the files when we are indexing. - `collectFilesForIndex` now directly calls `collectIndex` then does its additional logic with the result. ## Verification - Tested `@workspace` in chat with some different prompts. Seems to behave the same as before. - Tested `/review` on a project. --- - 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/amazonq/util/files.ts | 2 +- .../core/src/codewhisperer/util/zipUtil.ts | 2 +- .../src/shared/utilities/workspaceUtils.ts | 183 +++++++++--------- .../shared/utilities/workspaceUtils.test.ts | 129 ++++++++++-- 4 files changed, 197 insertions(+), 119 deletions(-) diff --git a/packages/core/src/amazonq/util/files.ts b/packages/core/src/amazonq/util/files.ts index d64f195d1c8..f61d4632714 100644 --- a/packages/core/src/amazonq/util/files.ts +++ b/packages/core/src/amazonq/util/files.ts @@ -87,7 +87,7 @@ export async function prepareRepoData( } const files = await collectFiles(repoRootPaths, workspaceFolders, { - maxSizeBytes: maxRepoSizeBytes, + maxTotalSizeBytes: maxRepoSizeBytes, excludeByGitIgnore: true, excludePatterns: excludePatterns, filterFn: filterFn, diff --git a/packages/core/src/codewhisperer/util/zipUtil.ts b/packages/core/src/codewhisperer/util/zipUtil.ts index 47ed91909bf..00ee0ae053d 100644 --- a/packages/core/src/codewhisperer/util/zipUtil.ts +++ b/packages/core/src/codewhisperer/util/zipUtil.ts @@ -420,7 +420,7 @@ export class ZipUtil { ) : vscode.workspace.workspaceFolders) as CurrentWsFolders, { - maxSizeBytes: this.getProjectScanPayloadSizeLimitInBytes(), + maxTotalSizeBytes: this.getProjectScanPayloadSizeLimitInBytes(), excludePatterns: useCase === FeatureUseCase.TEST_GENERATION ? [...CodeWhispererConstants.testGenExcludePatterns, ...defaultExcludePatterns] diff --git a/packages/core/src/shared/utilities/workspaceUtils.ts b/packages/core/src/shared/utilities/workspaceUtils.ts index 224c6645445..12cce75b3ff 100644 --- a/packages/core/src/shared/utilities/workspaceUtils.ts +++ b/packages/core/src/shared/utilities/workspaceUtils.ts @@ -293,14 +293,13 @@ export const defaultExcludePatterns = [ ] export function getExcludePattern(useDefaults: boolean = true) { - const globAlwaysExcludedDirs = getGlobalExcludePatterns() - const allPatterns = [...globAlwaysExcludedDirs] + const patterns = [...getGlobalExcludePatterns()] if (useDefaults) { - allPatterns.push(...defaultExcludePatterns) + patterns.push(...defaultExcludePatterns) } - return excludePatternsAsString(allPatterns) + return excludePatternsAsString(patterns) } function getGlobalExcludePatterns() { @@ -335,10 +334,19 @@ export type CollectFilesResultItem = { relativeFilePath: string fileUri: vscode.Uri fileContent: string + fileSizeBytes: number zipFilePath: string } export type CollectFilesFilter = (relativePath: string) => boolean // returns true if file should be filtered out - +interface CollectFilesOptions { + maxTotalSizeBytes?: number // 200 MB default + maxFileSizeBytes?: number // 10 MB default + includeContent?: boolean // default true + failOnLimit?: boolean // default true + excludeByGitIgnore?: boolean // default true + excludePatterns?: string[] // default defaultExcludePatterns + filterFn?: CollectFilesFilter +} /** * search files in sourcePaths and collect them using filtering options * @param sourcePaths the paths where collection starts @@ -349,48 +357,40 @@ export type CollectFilesFilter = (relativePath: string) => boolean // returns tr export async function collectFiles( sourcePaths: string[], workspaceFolders: CurrentWsFolders, - options?: { - maxSizeBytes?: number // 200 MB default - excludeByGitIgnore?: boolean // default true - excludePatterns?: string[] // default defaultExcludePatterns - filterFn?: CollectFilesFilter - } -): Promise { - const storage: Awaited = [] - + options?: (CollectFilesOptions & { includeContent: true }) | Omit +): Promise +export async function collectFiles( + sourcePaths: string[], + workspaceFolders: CurrentWsFolders, + options?: CollectFilesOptions & { includeContent: false } +): Promise[]> +export async function collectFiles( + sourcePaths: string[], + workspaceFolders: CurrentWsFolders, + options?: CollectFilesOptions +) { const workspaceFoldersMapping = getWorkspaceFoldersByPrefixes(workspaceFolders) const workspaceToPrefix = new Map( workspaceFoldersMapping === undefined ? [[workspaceFolders[0], '']] : Object.entries(workspaceFoldersMapping).map((value) => [value[1], value[0]]) ) - const prefixWithFolderPrefix = (folder: vscode.WorkspaceFolder, path: string) => { - const prefix = workspaceToPrefix.get(folder) - /** - * collects all files that are marked as source - * @param sourcePaths the paths where collection starts - * @param workspaceFolders the current workspace folders opened - * @param respectGitIgnore whether to respect gitignore file - * @returns all matched files - */ - if (prefix === undefined) { - throw new ToolkitError(`Failed to find prefix for workspace folder ${folder.name}`) - } - return prefix === '' ? path : `${prefix}/${path}` - } - - let totalSizeBytes = 0 + const includeContent = options?.includeContent ?? true + const maxFileSizeBytes = options?.maxFileSizeBytes ?? 1024 * 1024 * 10 const excludeByGitIgnore = options?.excludeByGitIgnore ?? true + const failOnLimit = options?.failOnLimit ?? true const inputExcludePatterns = options?.excludePatterns ?? defaultExcludePatterns - const maxSizeBytes = options?.maxSizeBytes ?? maxRepoSizeBytes + const maxSizeBytes = options?.maxTotalSizeBytes ?? maxRepoSizeBytes const excludePatterns = [...getGlobalExcludePatterns()] if (inputExcludePatterns.length) { excludePatterns.push(...inputExcludePatterns) } - const excludePatternFilter = excludePatternsAsString(excludePatterns) + let totalSizeBytes = 0 + const storage = [] + const excludePatternFilter = excludePatternsAsString(excludePatterns) for (const rootPath of sourcePaths) { const allFiles = await vscode.workspace.findFiles( new vscode.RelativePattern(rootPath, '**'), @@ -410,31 +410,56 @@ export async function collectFiles( } const fileStat = await fs.stat(file) - if (totalSizeBytes + fileStat.size > maxSizeBytes) { + if (failOnLimit && totalSizeBytes + fileStat.size > maxSizeBytes) { throw new ToolkitError( 'The project you have selected for source code is too large to use as context. Please select a different folder to use', { code: 'ContentLengthError' } ) } - const fileContent = await readFile(file) - - if (fileContent === undefined) { + if (fileStat.size > maxFileSizeBytes) { continue } - // Now that we've read the file, increase our usage - totalSizeBytes += fileStat.size - storage.push({ + const result = { workspaceFolder: relativePath.workspaceFolder, relativeFilePath: relativePath.relativePath, fileUri: file, - fileContent: fileContent, + fileSizeBytes: fileStat.size, zipFilePath: prefixWithFolderPrefix(relativePath.workspaceFolder, relativePath.relativePath), - }) + } + if (includeContent) { + const content = await readFile(file) + if (content === undefined) { + continue + } + totalSizeBytes += fileStat.size + storage.push({ + ...result, + fileContent: content, + }) + } else { + totalSizeBytes += fileStat.size + storage.push(result) + } } } return storage + + function prefixWithFolderPrefix(folder: vscode.WorkspaceFolder, path: string) { + const prefix = workspaceToPrefix.get(folder) + /** + * collects all files that are marked as source + * @param sourcePaths the paths where collection starts + * @param workspaceFolders the current workspace folders opened + * @param respectGitIgnore whether to respect gitignore file + * @returns all matched files + */ + if (prefix === undefined) { + throw new ToolkitError(`Failed to find prefix for workspace folder ${folder.name}`) + } + return prefix === '' ? path : `${prefix}/${path}` + } } const readFile = async (file: vscode.Uri) => { @@ -576,7 +601,7 @@ export function getWorkspaceFoldersByPrefixes( * 2. Must not be auto generated code * 3. Must not be within gitignore * 4. Ranked by priority. - * 5. Select files within maxSize limit. + * 5. Select files within maxFileSize limit. * This function do not read the actual file content or compress them into a zip. * TODO: Move this to LSP * @param sourcePaths the paths where collection starts @@ -590,65 +615,20 @@ export async function collectFilesForIndex( respectGitIgnore: boolean = true, maxSize = 250 * 1024 * 1024 // 250 MB, // make this configurable, so we can test it -): Promise< - { - workspaceFolder: vscode.WorkspaceFolder - relativeFilePath: string - fileUri: vscode.Uri - fileSizeBytes: number - }[] -> { - const storage: Awaited> = [] - - const isLanguageSupported = (filename: string) => { - const k = - /\.(js|ts|java|py|rb|cpp|tsx|jsx|cc|c|cs|vb|pl|r|m|hs|mts|mjs|h|clj|dart|groovy|lua|rb|jl|ipynb|html|json|css|md|php|swift|rs|scala|yaml|tf|sql|sh|go|yml|kt|smithy|config|kts|gradle|cfg|xml|vue)$/i - return k.test(filename) || filename.endsWith('Config') - } - - const isBuildOrBin = (filePath: string) => { - const k = /[/\\](bin|build|node_modules|env|\.idea|\.venv|venv)[/\\]/i - return k.test(filePath) - } - - let totalSizeBytes = 0 - for (const rootPath of sourcePaths) { - const allFiles = await vscode.workspace.findFiles( - new vscode.RelativePattern(rootPath, '**'), - getExcludePattern() - ) - const files = respectGitIgnore ? await filterOutGitignoredFiles(rootPath, allFiles) : allFiles - - for (const file of files) { - if (!isLanguageSupported(file.fsPath)) { - continue - } - if (isBuildOrBin(file.fsPath)) { - continue - } - const relativePath = getWorkspaceRelativePath(file.fsPath, { workspaceFolders }) - if (!relativePath) { - continue - } - - const fileStat = await fs.stat(file) - // ignore single file over 10 MB - if (fileStat.size > 10 * 1024 * 1024) { - continue - } - storage.push({ - workspaceFolder: relativePath.workspaceFolder, - relativeFilePath: relativePath.relativePath, - fileUri: file, - fileSizeBytes: fileStat.size, - }) - } - } +) { + const storage = await collectFiles(sourcePaths, workspaceFolders, { + maxFileSizeBytes: 10 * 1024 * 1024, + includeContent: false, + failOnLimit: false, + excludeByGitIgnore: respectGitIgnore, + filterFn: (rp) => !isLanguageSupported(rp) || isBuildOrBin(rp), + }) // prioritize upper level files storage.sort((a, b) => a.fileUri.fsPath.length - b.fileUri.fsPath.length) const maxSizeBytes = Math.min(maxSize, os.freemem() / 2) + let totalSizeBytes = 0 let i = 0 for (i = 0; i < storage.length; i += 1) { totalSizeBytes += storage[i].fileSizeBytes @@ -658,6 +638,17 @@ export async function collectFilesForIndex( } // pick top 100k files below size limit return storage.slice(0, Math.min(100000, i)) + + function isLanguageSupported(filename: string) { + const k = + /\.(js|ts|java|py|rb|cpp|tsx|jsx|cc|c|cs|vb|pl|r|m|hs|mts|mjs|h|clj|dart|groovy|lua|rb|jl|ipynb|html|json|css|md|php|swift|rs|scala|yaml|tf|sql|sh|go|yml|kt|smithy|config|kts|gradle|cfg|xml|vue)$/i + return k.test(filename) || filename.endsWith('Config') + } + + function isBuildOrBin(filePath: string) { + const k = /[/\\](bin|build|node_modules|env|\.idea|\.venv|venv)[/\\]/i + return k.test(filePath) + } } /** diff --git a/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts b/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts index 18761491458..55c551b87d0 100644 --- a/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts +++ b/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts @@ -9,6 +9,7 @@ import * as vscode from 'vscode' import { collectFiles, collectFilesForIndex, + CollectFilesResultItem, findParentProjectFile, findStringInDirectory, getWorkspaceFoldersByPrefixes, @@ -19,7 +20,7 @@ import globals from '../../../shared/extensionGlobals' import { CodelensRootRegistry } from '../../../shared/fs/codelensRootRegistry' import { createTestWorkspace, createTestWorkspaceFolder, toFile } from '../../../test/testUtil' import sinon from 'sinon' -import { fs } from '../../../shared' +import { fs, ToolkitError } from '../../../shared' describe('workspaceUtils', () => { let sandbox: sinon.SinonSandbox @@ -256,11 +257,7 @@ describe('workspaceUtils', () => { await writeFile(['src', 'folder3', 'negate_test1'], fileContent) await writeFile(['src', 'folder3', 'negate_test6'], fileContent) - const result = (await collectFiles([workspaceFolder.uri.fsPath], [workspaceFolder])) - // for some reason, uri created inline differ in subfields, so skipping them from assertion - .map(({ fileUri, zipFilePath, ...r }) => ({ ...r })) - - result.sort((l, r) => l.relativeFilePath.localeCompare(r.relativeFilePath)) + const result = processIndexResults(await collectFiles([workspaceFolder.uri.fsPath], [workspaceFolder])) // non-posix filePath check here is important. assert.deepStrictEqual( @@ -269,41 +266,49 @@ describe('workspaceUtils', () => { workspaceFolder, relativeFilePath: '.gitignore', fileContent: gitignoreContent, + fileSizeBytes: 162, }, { workspaceFolder, relativeFilePath: 'file1', fileContent: 'test content', + fileSizeBytes: 12, }, { workspaceFolder, relativeFilePath: 'file3', fileContent: 'test content', + fileSizeBytes: 12, }, { workspaceFolder, relativeFilePath: 'range_file9', fileContent: 'test content', + fileSizeBytes: 12, }, { workspaceFolder, relativeFilePath: path.join('src', '.gitignore'), fileContent: gitignore2, + fileSizeBytes: 8, }, { workspaceFolder, relativeFilePath: path.join('src', 'folder2', 'a.js'), fileContent: fileContent, + fileSizeBytes: 12, }, { workspaceFolder, relativeFilePath: path.join('src', 'folder3', '.gitignore'), fileContent: gitignore3, + fileSizeBytes: 42, }, { workspaceFolder, relativeFilePath: path.join('src', 'folder3', 'negate_test1'), fileContent: fileContent, + fileSizeBytes: 12, }, ] satisfies typeof result, result @@ -336,6 +341,20 @@ describe('workspaceUtils', () => { assert.deepStrictEqual(1, result.length) assert.deepStrictEqual('non-license.md', result[0].relativeFilePath) }) + + it('throws when total size limit is exceeded (by default)', async function () { + const workspace = await createTestWorkspaceFolder() + sandbox.stub(vscode.workspace, 'workspaceFolders').value([workspace]) + + const fileContent = 'this is some text' + await toFile(fileContent, path.join(workspace.uri.fsPath, 'file1')) + await toFile(fileContent, path.join(workspace.uri.fsPath, 'file2')) + + await assert.rejects( + () => collectFiles([workspace.uri.fsPath], [workspace], { maxTotalSizeBytes: 15 }), + (e) => e instanceof ToolkitError && e.code === 'ContentLengthError' + ) + }) }) describe('getWorkspaceFoldersByPrefixes', function () { @@ -440,19 +459,20 @@ describe('workspaceUtils', () => { }) describe('collectFilesForIndex', function () { - it('returns all files in the workspace not excluded by gitignore and is a supported programming language', async function () { - // these variables are a manual selection of settings for the test in order to test the collectFiles function - const fileAmount = 3 - const fileNamePrefix = 'file' - const fileContent = 'test content' + let workspaceFolder: vscode.WorkspaceFolder - const workspaceFolder = await createTestWorkspace(fileAmount, { fileNamePrefix, fileContent }) - - const writeFile = (pathParts: string[], fileContent: string) => { - return toFile(fileContent, path.join(workspaceFolder.uri.fsPath, ...pathParts)) - } + const writeFile = (pathParts: string[], fileContent: string) => { + return toFile(fileContent, path.join(workspaceFolder.uri.fsPath, ...pathParts)) + } + beforeEach(async function () { + workspaceFolder = await createTestWorkspaceFolder() sandbox.stub(vscode.workspace, 'workspaceFolders').value([workspaceFolder]) + }) + + it('returns all files in the workspace not excluded by gitignore and is a supported programming language', async function () { + const fileContent = 'test content' + const gitignoreContent = `file2 # different formats of prefixes /build @@ -486,11 +506,9 @@ describe('workspaceUtils', () => { await writeFile(['src', 'folder3', 'negate_test1'], fileContent) await writeFile(['src', 'folder3', 'negate_test6'], fileContent) - const result = (await collectFilesForIndex([workspaceFolder.uri.fsPath], [workspaceFolder], true)) - // for some reason, uri created inline differ in subfields, so skipping them from assertion - .map(({ fileUri, ...r }) => ({ ...r })) - - result.sort((l, r) => l.relativeFilePath.localeCompare(r.relativeFilePath)) + const result = processIndexResults( + await collectFilesForIndex([workspaceFolder.uri.fsPath], [workspaceFolder], true) + ) // non-posix filePath check here is important. assert.deepStrictEqual( @@ -509,6 +527,68 @@ describe('workspaceUtils', () => { result ) }) + + it('does not include build related files', async function () { + const fileContent = 'this is a file' + + await writeFile(['bin', `ignored1`], fileContent) + await writeFile(['bin', `ignored2`], fileContent) + + await writeFile([`a.js`], fileContent) + await writeFile([`b.java`], fileContent) + + const result = processIndexResults( + await collectFilesForIndex([workspaceFolder.uri.fsPath], [workspaceFolder], true) + ) + + // non-posix filePath check here is important. + assert.deepStrictEqual( + [ + { + workspaceFolder, + relativeFilePath: 'a.js', + fileSizeBytes: 14, + }, + { + workspaceFolder, + relativeFilePath: 'b.java', + fileSizeBytes: 14, + }, + ] satisfies typeof result, + result + ) + }) + + it('returns top level files when max size is reached', async function () { + const fileContent = 'this is a file' + + await writeFile(['path', 'to', 'file', 'bot.js'], fileContent) + await writeFile(['path', 'to', 'file', `bot.java`], fileContent) + + await writeFile([`top.js`], fileContent) + await writeFile([`top.java`], fileContent) + + const result = processIndexResults( + await collectFilesForIndex([workspaceFolder.uri.fsPath], [workspaceFolder], true, 30) + ) + + // non-posix filePath check here is important. + assert.deepStrictEqual( + [ + { + workspaceFolder, + relativeFilePath: 'top.java', + fileSizeBytes: 14, + }, + { + workspaceFolder, + relativeFilePath: 'top.js', + fileSizeBytes: 14, + }, + ] satisfies typeof result, + result + ) + }) }) describe('findStringInDirectory', function () { @@ -522,3 +602,10 @@ describe('workspaceUtils', () => { }) }) }) + +// for some reason, uri created inline differ in subfields, so skipping them from assertion +function processIndexResults(results: Omit[] | CollectFilesResultItem[]) { + return results + .map(({ zipFilePath, fileUri, ...r }) => ({ ...r })) + .sort((l, r) => l.relativeFilePath.localeCompare(r.relativeFilePath)) +} From bd378854550362810170a4d9164c426e622de256 Mon Sep 17 00:00:00 2001 From: Maxim Hayes <149123719+hayemaxi@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:27:08 -0700 Subject: [PATCH 08/18] docs: add "Q Developer" marketplace tag (#6810) The Q product name should find amazon-q-vscode. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- packages/amazonq/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 7d4ce259787..93aa35df0e7 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -32,7 +32,8 @@ "Codewhisperer", "AI", "Assistant", - "Chatbot" + "Chatbot", + "Q Developer" ], "preview": false, "qna": "https://github.com/aws/aws-toolkit-vscode/issues", From 8505d7baed46a2bb3754018ee8c69eba28e35f9f Mon Sep 17 00:00:00 2001 From: Aidan Ton Date: Wed, 19 Mar 2025 17:23:05 -0700 Subject: [PATCH 09/18] /review: passing referenceTrackerConfiguration to StartCodeFixJob --- .../Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json | 4 ++++ packages/core/src/codewhisperer/client/user-service-2.json | 3 ++- .../src/codewhisperer/commands/startCodeFixGeneration.ts | 6 ++++++ packages/core/src/codewhisperer/service/codeFixHandler.ts | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json diff --git a/packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json b/packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json new file mode 100644 index 00000000000..911f0babb24 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "/review: passing referenceTrackerConfiguration to StartCodeFixJob" +} diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index ed7661bec4c..833245ef183 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -2206,7 +2206,8 @@ "uploadId": { "shape": "UploadId" }, "description": { "shape": "StartCodeFixJobRequestDescriptionString" }, "ruleId": { "shape": "StartCodeFixJobRequestRuleIdString" }, - "codeFixName": { "shape": "CodeFixName" } + "codeFixName": { "shape": "CodeFixName" }, + "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" } } }, "StartCodeFixJobRequestDescriptionString": { diff --git a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts index a255864c7eb..8e9a5240812 100644 --- a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts +++ b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts @@ -18,6 +18,7 @@ import AdmZip from 'adm-zip' import path from 'path' import { TelemetryHelper } from '../util/telemetryHelper' import { tempDirPath } from '../../shared/filesystemUtilities' +import { CodeWhispererSettings } from '../util/codewhispererSettings' export async function startCodeFixGeneration( client: DefaultCodeWhispererClient, @@ -69,6 +70,11 @@ export async function startCodeFixGeneration( end: { line: issue.endLine, character: 0 }, }, issue.recommendation.text, + { + recommendationsWithReferences: CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled() + ? 'ALLOW' + : 'BLOCK', + }, codeFixName, issue.ruleId ) diff --git a/packages/core/src/codewhisperer/service/codeFixHandler.ts b/packages/core/src/codewhisperer/service/codeFixHandler.ts index 854cb848a52..4aa74a91ac7 100644 --- a/packages/core/src/codewhisperer/service/codeFixHandler.ts +++ b/packages/core/src/codewhisperer/service/codeFixHandler.ts @@ -50,6 +50,7 @@ export async function createCodeFixJob( uploadId: string, snippetRange: CodeWhispererUserClient.Range, description: string, + referenceTrackerConfiguration: CodeWhispererUserClient.ReferenceTrackerConfiguration, codeFixName?: string, ruleId?: string ) { @@ -60,6 +61,7 @@ export async function createCodeFixJob( codeFixName, ruleId, description, + referenceTrackerConfiguration, } const resp = await client.startCodeFixJob(req).catch((err) => { From c7179db1cefb3ff7f9c7d45015258ed2498a431a Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:06:37 -0400 Subject: [PATCH 10/18] fix(amazonq): Open multiple VSCode instances crashes Amazon Q Language Server (#6811) ## Problem When running multiple VSCode windows on mac arm64, Amazon Q's language server works correctly in the first window but crashes in additional windows with the error: `EXC_CRASH (SIGKILL (Code Signature Invalid))` ## Root Cause The crash occurs because: - The first VSCode window starts the language server and extracts everything (including the node binary from flare) - When additional VSCode windows are opened, they attempt to re-extract the zip from flare, overriding the current contents - On mac arm64, overwriting the node binary while it's in use by the first VSCode window can apparently cause code signing validation to fail for the next callers of it - This didn't effect windows when I was playing around with it yesterday - Weirdly enough, if you start the language server outside of VSCode, unzip servers.zip again, and then spawn a new language server it doesn't seem to have an issue. My guess is there's some weird interplay with electron owning the spawning of the processes ## Solution Instead of forcefully overriding contents in flare, only copy over the file if its necessary --- - 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/shared/lsp/lspResolver.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/shared/lsp/lspResolver.ts b/packages/core/src/shared/lsp/lspResolver.ts index 189a69a9d7a..5d19e35f836 100644 --- a/packages/core/src/shared/lsp/lspResolver.ts +++ b/packages/core/src/shared/lsp/lspResolver.ts @@ -308,7 +308,14 @@ export class LanguageServerResolver { // attempt to unzip const zipFile = new AdmZip(zip) const extractPath = zip.replace('.zip', '') - zipFile.extractAllTo(extractPath, true) + + /** + * Avoid overwriting existing files during extraction to prevent file corruption. + * On Mac ARM64 when a language server is already running in one VS Code window, + * attempting to extract and overwrite its files from another window can cause + * the newly started language server to crash with 'EXC_CRASH (SIGKILL (Code Signature Invalid))'. + */ + zipFile.extractAllTo(extractPath, false) } catch (e) { return false } From 349b9c9ebec6e4c99575ef84896d7ad529262b05 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Thu, 20 Mar 2025 19:14:22 +0000 Subject: [PATCH 11/18] Release 3.51.0 --- package-lock.json | 4 ++-- packages/toolkit/.changes/3.51.0.json | 10 ++++++++++ .../Feature-7359863e-64bc-4f03-918d-3a6cea89038e.json | 4 ---- packages/toolkit/CHANGELOG.md | 4 ++++ packages/toolkit/package.json | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 packages/toolkit/.changes/3.51.0.json delete mode 100644 packages/toolkit/.changes/next-release/Feature-7359863e-64bc-4f03-918d-3a6cea89038e.json diff --git a/package-lock.json b/package-lock.json index 1d7ededed09..7853a314cbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "prettier": "^3.3.3", "prettier-plugin-sh": "^0.14.0", "pretty-quick": "^4.0.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "^5.0.4", "webpack": "^5.95.0", "webpack-cli": "^5.1.4", @@ -25094,7 +25094,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.51.0-SNAPSHOT", + "version": "3.51.0", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/toolkit/.changes/3.51.0.json b/packages/toolkit/.changes/3.51.0.json new file mode 100644 index 00000000000..53ab636d4d4 --- /dev/null +++ b/packages/toolkit/.changes/3.51.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-03-20", + "version": "3.51.0", + "entries": [ + { + "type": "Feature", + "description": "Update Step Functions marketplace documentation." + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/next-release/Feature-7359863e-64bc-4f03-918d-3a6cea89038e.json b/packages/toolkit/.changes/next-release/Feature-7359863e-64bc-4f03-918d-3a6cea89038e.json deleted file mode 100644 index 47b919d729d..00000000000 --- a/packages/toolkit/.changes/next-release/Feature-7359863e-64bc-4f03-918d-3a6cea89038e.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Update Step Functions marketplace documentation." -} diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index 1e9d54aa949..1793f0df4bb 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.51.0 2025-03-20 + +- **Feature** Update Step Functions marketplace documentation. + ## 3.50.0 2025-03-13 - Miscellaneous non-user-facing changes diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index ef0f219ff52..5648d825201 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.51.0-SNAPSHOT", + "version": "3.51.0", "extensionKind": [ "workspace" ], From ac0b29ee51cf5c893efe201b72798ea2b581bf41 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Thu, 20 Mar 2025 19:14:32 +0000 Subject: [PATCH 12/18] Release 1.52.0 --- package-lock.json | 4 +- packages/amazonq/.changes/1.52.0.json | 50 +++++++++++++++++++ ...-2ce6a8ba-dbc9-4798-92f4-db2abb4de17b.json | 4 -- ...-385a5132-b128-4acb-920a-53990ebdb7c4.json | 4 -- ...-4116a3e0-6c6d-48bf-a991-3f872a68a121.json | 4 -- ...-5bc056f9-796f-45e8-b04c-574821cb5171.json | 4 -- ...-62081c53-09b4-46b0-80b0-6eb853ffabd6.json | 4 -- ...-7bb4ae68-204d-48a1-a510-490d2a2a938a.json | 4 -- ...-a24316fe-1ff9-44e4-9d5c-6b78aa7f0d1e.json | 4 -- ...-b5a4a16d-16a4-40d2-985b-0d76abe475db.json | 4 -- ...-bde47093-ee34-4802-a556-d1e5dc526f4d.json | 4 -- ...-25bf4b40-331e-4b28-8a89-0a19075b6b79.json | 4 -- ...-b0ddb09f-ae4a-453f-b689-a5c6c5f599f9.json | 4 -- packages/amazonq/CHANGELOG.md | 14 ++++++ packages/amazonq/package.json | 2 +- 15 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 packages/amazonq/.changes/1.52.0.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-2ce6a8ba-dbc9-4798-92f4-db2abb4de17b.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-385a5132-b128-4acb-920a-53990ebdb7c4.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-4116a3e0-6c6d-48bf-a991-3f872a68a121.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-5bc056f9-796f-45e8-b04c-574821cb5171.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-7bb4ae68-204d-48a1-a510-490d2a2a938a.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-a24316fe-1ff9-44e4-9d5c-6b78aa7f0d1e.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-b5a4a16d-16a4-40d2-985b-0d76abe475db.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-b0ddb09f-ae4a-453f-b689-a5c6c5f599f9.json diff --git a/package-lock.json b/package-lock.json index 1d7ededed09..4c6299552b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "prettier": "^3.3.3", "prettier-plugin-sh": "^0.14.0", "pretty-quick": "^4.0.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "^5.0.4", "webpack": "^5.95.0", "webpack-cli": "^5.1.4", @@ -24295,7 +24295,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.52.0-SNAPSHOT", + "version": "1.52.0", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/amazonq/.changes/1.52.0.json b/packages/amazonq/.changes/1.52.0.json new file mode 100644 index 00000000000..a4f357edc87 --- /dev/null +++ b/packages/amazonq/.changes/1.52.0.json @@ -0,0 +1,50 @@ +{ + "date": "2025-03-20", + "version": "1.52.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q chat: @Folders and @Files are missing `@` prefix in chat history" + }, + { + "type": "Bug Fix", + "description": "/review: Code Issues ellipses menu displays AWS Toolkit options, if installed." + }, + { + "type": "Bug Fix", + "description": "Amazon Q chat: Progress indicator height is stretched" + }, + { + "type": "Bug Fix", + "description": "Amazon Q chat: Long descriptions in context list are cut off" + }, + { + "type": "Bug Fix", + "description": "Amazon Q chat: Improve responses for saved prompts and workspace rules" + }, + { + "type": "Bug Fix", + "description": "/test: show descriptive error message" + }, + { + "type": "Bug Fix", + "description": "Code Review: Fixed a bug where issues are double counted in the Q chat" + }, + { + "type": "Bug Fix", + "description": "Amazon Q chat: Animation timings are too long" + }, + { + "type": "Bug Fix", + "description": "Fix inline completion failure due to context length exceeding the threshold" + }, + { + "type": "Feature", + "description": "/review: passing referenceTrackerConfiguration to StartCodeFixJob" + }, + { + "type": "Feature", + "description": "/review: rename setting `showInlineCodeSuggestionsWithCodeReferences` to `showCodeWithReferences`" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Bug Fix-2ce6a8ba-dbc9-4798-92f4-db2abb4de17b.json b/packages/amazonq/.changes/next-release/Bug Fix-2ce6a8ba-dbc9-4798-92f4-db2abb4de17b.json deleted file mode 100644 index 3cf406600f7..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-2ce6a8ba-dbc9-4798-92f4-db2abb4de17b.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q chat: @Folders and @Files are missing `@` prefix in chat history" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-385a5132-b128-4acb-920a-53990ebdb7c4.json b/packages/amazonq/.changes/next-release/Bug Fix-385a5132-b128-4acb-920a-53990ebdb7c4.json deleted file mode 100644 index 8e65afb7cc8..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-385a5132-b128-4acb-920a-53990ebdb7c4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "/review: Code Issues ellipses menu displays AWS Toolkit options, if installed." -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-4116a3e0-6c6d-48bf-a991-3f872a68a121.json b/packages/amazonq/.changes/next-release/Bug Fix-4116a3e0-6c6d-48bf-a991-3f872a68a121.json deleted file mode 100644 index 78d0fb2249c..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-4116a3e0-6c6d-48bf-a991-3f872a68a121.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q chat: Progress indicator height is stretched" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-5bc056f9-796f-45e8-b04c-574821cb5171.json b/packages/amazonq/.changes/next-release/Bug Fix-5bc056f9-796f-45e8-b04c-574821cb5171.json deleted file mode 100644 index 3d4628e654b..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-5bc056f9-796f-45e8-b04c-574821cb5171.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q chat: Long descriptions in context list are cut off" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json b/packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json deleted file mode 100644 index e2e4205f6c6..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-62081c53-09b4-46b0-80b0-6eb853ffabd6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q chat: Improve responses for saved prompts and workspace rules" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-7bb4ae68-204d-48a1-a510-490d2a2a938a.json b/packages/amazonq/.changes/next-release/Bug Fix-7bb4ae68-204d-48a1-a510-490d2a2a938a.json deleted file mode 100644 index b7f9ab81cc7..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-7bb4ae68-204d-48a1-a510-490d2a2a938a.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "/test: show descriptive error message" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-a24316fe-1ff9-44e4-9d5c-6b78aa7f0d1e.json b/packages/amazonq/.changes/next-release/Bug Fix-a24316fe-1ff9-44e4-9d5c-6b78aa7f0d1e.json deleted file mode 100644 index ed410a28f2b..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-a24316fe-1ff9-44e4-9d5c-6b78aa7f0d1e.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Code Review: Fixed a bug where issues are double counted in the Q chat" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-b5a4a16d-16a4-40d2-985b-0d76abe475db.json b/packages/amazonq/.changes/next-release/Bug Fix-b5a4a16d-16a4-40d2-985b-0d76abe475db.json deleted file mode 100644 index ec6baa28e1a..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-b5a4a16d-16a4-40d2-985b-0d76abe475db.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q chat: Animation timings are too long" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json b/packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json deleted file mode 100644 index 037bcdbe2ae..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-bde47093-ee34-4802-a556-d1e5dc526f4d.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Fix inline completion failure due to context length exceeding the threshold" -} diff --git a/packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json b/packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json deleted file mode 100644 index 911f0babb24..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-25bf4b40-331e-4b28-8a89-0a19075b6b79.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "/review: passing referenceTrackerConfiguration to StartCodeFixJob" -} diff --git a/packages/amazonq/.changes/next-release/Feature-b0ddb09f-ae4a-453f-b689-a5c6c5f599f9.json b/packages/amazonq/.changes/next-release/Feature-b0ddb09f-ae4a-453f-b689-a5c6c5f599f9.json deleted file mode 100644 index d3d53817245..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-b0ddb09f-ae4a-453f-b689-a5c6c5f599f9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "/review: rename setting `showInlineCodeSuggestionsWithCodeReferences` to `showCodeWithReferences`" -} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 79d01a0624f..f938e36fbd6 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,17 @@ +## 1.52.0 2025-03-20 + +- **Bug Fix** Amazon Q chat: @Folders and @Files are missing `@` prefix in chat history +- **Bug Fix** /review: Code Issues ellipses menu displays AWS Toolkit options, if installed. +- **Bug Fix** Amazon Q chat: Progress indicator height is stretched +- **Bug Fix** Amazon Q chat: Long descriptions in context list are cut off +- **Bug Fix** Amazon Q chat: Improve responses for saved prompts and workspace rules +- **Bug Fix** /test: show descriptive error message +- **Bug Fix** Code Review: Fixed a bug where issues are double counted in the Q chat +- **Bug Fix** Amazon Q chat: Animation timings are too long +- **Bug Fix** Fix inline completion failure due to context length exceeding the threshold +- **Feature** /review: passing referenceTrackerConfiguration to StartCodeFixJob +- **Feature** /review: rename setting `showInlineCodeSuggestionsWithCodeReferences` to `showCodeWithReferences` + ## 1.51.0 2025-03-12 - **Bug Fix** increase scan timeout to reduce front-end timeout errors diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 93aa35df0e7..ffdc6d45f79 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.52.0-SNAPSHOT", + "version": "1.52.0", "extensionKind": [ "workspace" ], From eb2945141c5769c4a3a6bdff0bd64867d68b3072 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Thu, 20 Mar 2025 20:43:08 +0000 Subject: [PATCH 13/18] Update version to snapshot version: 3.52.0-SNAPSHOT --- package-lock.json | 4 ++-- packages/toolkit/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7853a314cbf..23460f5c28d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "prettier": "^3.3.3", "prettier-plugin-sh": "^0.14.0", "pretty-quick": "^4.0.0", - "ts-node": "^10.9.2", + "ts-node": "^10.9.1", "typescript": "^5.0.4", "webpack": "^5.95.0", "webpack-cli": "^5.1.4", @@ -25094,7 +25094,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.51.0", + "version": "3.52.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 5648d825201..daf54fd2771 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.51.0", + "version": "3.52.0-SNAPSHOT", "extensionKind": [ "workspace" ], From 6e26b33dcf7fb3d182819ac37f4e3a1f03e7e475 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Thu, 20 Mar 2025 20:43:18 +0000 Subject: [PATCH 14/18] Update version to snapshot version: 1.53.0-SNAPSHOT --- package-lock.json | 4 ++-- packages/amazonq/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c6299552b3..b25c5390a6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "prettier": "^3.3.3", "prettier-plugin-sh": "^0.14.0", "pretty-quick": "^4.0.0", - "ts-node": "^10.9.2", + "ts-node": "^10.9.1", "typescript": "^5.0.4", "webpack": "^5.95.0", "webpack-cli": "^5.1.4", @@ -24295,7 +24295,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.52.0", + "version": "1.53.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index ffdc6d45f79..5af0194206e 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.52.0", + "version": "1.53.0-SNAPSHOT", "extensionKind": [ "workspace" ], From 119d2f4d39441dbaf80f9652895dfe1b3b5f4cbb Mon Sep 17 00:00:00 2001 From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:55:35 -0400 Subject: [PATCH 15/18] feat(amazonq): re-add basic chat through a language server (#6781) ## Problem - We removed chat a while ago from the `feature/amazonqLSP` branch because we were just starting with inline suggestions ## Solution - Now that we're moving to use chat we can re-add it - Implement explain, refactor, fix, optimize, sendToPrompt, openTab - Add a feature flag for enabling/disabling chat - **note**: amazonqLSP and amazonqChatLSP must be enabled in order for chat support to work - extended the baseinstaller so that individual lsps installers can provider their own resource paths ## Notes - this is the equivalent of https://github.com/aws/language-servers/blob/55253ea258b2d34bcc47b93e9998b1e9898e8f2a/client/vscode/src/chatActivation.ts but integrated with our codebase - since commands require the webview we pass in the view provider to all commands so we can lazy evaluate the webview when required - certain message listeners are only registered _after_ the UI is resolved --- - 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/extensionNode.ts | 16 +- packages/amazonq/src/lsp/chat/activation.ts | 34 +++ packages/amazonq/src/lsp/chat/commands.ts | 79 ++++++ packages/amazonq/src/lsp/chat/messages.ts | 225 ++++++++++++++++++ .../amazonq/src/lsp/chat/webviewProvider.ts | 73 ++++++ packages/amazonq/src/lsp/client.ts | 19 +- packages/amazonq/src/lsp/lspInstaller.ts | 10 +- .../core/src/shared/lsp/baseLspInstaller.ts | 6 +- packages/core/src/shared/lsp/types.ts | 7 +- .../core/src/shared/settings-toolkit.gen.ts | 3 +- packages/toolkit/package.json | 4 + 12 files changed, 454 insertions(+), 24 deletions(-) create mode 100644 packages/amazonq/src/lsp/chat/activation.ts create mode 100644 packages/amazonq/src/lsp/chat/commands.ts create mode 100644 packages/amazonq/src/lsp/chat/messages.ts create mode 100644 packages/amazonq/src/lsp/chat/webviewProvider.ts diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index ae5d669c702..ad89a44ed4d 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -157,7 +157,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is context.subscriptions.push( Experiments.instance.onDidChange(async (event) => { - if (event.key === 'amazonqLSP') { + if (event.key === 'amazonqLSP' || event.key === 'amazonqChatLSP') { 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 1b9200f7b59..d9d36f828eb 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -7,7 +7,15 @@ import * as vscode from 'vscode' import { activateAmazonQCommon, amazonQContextPrefix, deactivateCommon } from './extension' import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq' import { activate as activateQGumby } from 'aws-core-vscode/amazonqGumby' -import { ExtContext, globals, CrashMonitoring, getLogger, isNetworkError, isSageMaker } from 'aws-core-vscode/shared' +import { + ExtContext, + globals, + CrashMonitoring, + getLogger, + isNetworkError, + 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' @@ -43,8 +51,10 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { extensionContext: context, } - await activateCWChat(context) - await activateQGumby(extContext as ExtContext) + if (!Experiments.instance.get('amazonqChatLSP', false)) { + await activateCWChat(context) + await activateQGumby(extContext as ExtContext) + } const authProvider = new CommonAuthViewProvider( context, diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts new file mode 100644 index 00000000000..406b753716f --- /dev/null +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { window } from 'vscode' +import { LanguageClient } from 'vscode-languageclient' +import { AmazonQChatViewProvider } from './webviewProvider' +import { registerCommands } from './commands' +import { registerLanguageServerEventListener, registerMessageListeners } from './messages' +import { globals } from 'aws-core-vscode/shared' + +export function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) { + const provider = new AmazonQChatViewProvider(mynahUIPath) + + globals.context.subscriptions.push( + window.registerWebviewViewProvider(AmazonQChatViewProvider.viewType, provider, { + webviewOptions: { + retainContextWhenHidden: true, + }, + }) + ) + + /** + * Commands are registered independent of the webview being open because when they're executed + * they focus the webview + **/ + registerCommands(provider) + registerLanguageServerEventListener(languageClient, provider) + + provider.onDidResolveWebview(() => { + registerMessageListeners(languageClient, provider, encryptionKey) + }) +} diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts new file mode 100644 index 00000000000..3febc748442 --- /dev/null +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -0,0 +1,79 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { Commands, globals } from 'aws-core-vscode/shared' +import { window } from 'vscode' +import { AmazonQChatViewProvider } from './webviewProvider' + +export function registerCommands(provider: AmazonQChatViewProvider) { + globals.context.subscriptions.push( + registerGenericCommand('aws.amazonq.explainCode', 'Explain', provider), + registerGenericCommand('aws.amazonq.refactorCode', 'Refactor', provider), + registerGenericCommand('aws.amazonq.fixCode', 'Fix', provider), + registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', provider), + Commands.register('aws.amazonq.sendToPrompt', (data) => { + const triggerType = getCommandTriggerType(data) + const selection = getSelectedText() + + void focusAmazonQPanel().then(() => { + void provider.webview?.postMessage({ + command: 'sendToPrompt', + params: { selection: selection, triggerType }, + }) + }) + }), + Commands.register('aws.amazonq.openTab', () => { + void focusAmazonQPanel().then(() => { + void provider.webview?.postMessage({ + command: 'aws/chat/openTab', + params: {}, + }) + }) + }) + ) +} + +function getSelectedText(): string { + const editor = window.activeTextEditor + if (editor) { + const selection = editor.selection + const selectedText = editor.document.getText(selection) + return selectedText + } + + return ' ' +} + +function getCommandTriggerType(data: any): string { + // data is undefined when commands triggered from keybinding or command palette. Currently no + // way to differentiate keybinding and command palette, so both interactions are recorded as keybinding + return data === undefined ? 'hotkeys' : 'contextMenu' +} + +function registerGenericCommand(commandName: string, genericCommand: string, provider: AmazonQChatViewProvider) { + return Commands.register(commandName, (data) => { + const triggerType = getCommandTriggerType(data) + const selection = getSelectedText() + + void focusAmazonQPanel().then(() => { + void provider.webview?.postMessage({ + command: 'genericCommand', + params: { genericCommand, selection, triggerType }, + }) + }) + }) +} + +/** + * Importing focusAmazonQPanel from aws-core-vscode/amazonq leads to several dependencies down the chain not resolving since AmazonQ chat + * is currently only activated on node, but the language server is activated on both web and node. + * + * Instead, we just create our own as a temporary solution + */ +async function focusAmazonQPanel() { + await vscode.commands.executeCommand('aws.amazonq.AmazonQChatView.focus') + await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus') +} diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts new file mode 100644 index 00000000000..4c9d93f7f65 --- /dev/null +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -0,0 +1,225 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + isValidAuthFollowUpType, + INSERT_TO_CURSOR_POSITION, + AUTH_FOLLOW_UP_CLICKED, + CHAT_OPTIONS, + COPY_TO_CLIPBOARD, +} from '@aws/chat-client-ui-types' +import { + ChatResult, + chatRequestType, + ChatParams, + followUpClickNotificationType, + quickActionRequestType, + QuickActionResult, + QuickActionParams, + insertToCursorPositionNotificationType, +} from '@aws/language-server-runtimes/protocol' +import { v4 as uuidv4 } from 'uuid' +import { window } from 'vscode' +import { Disposable, LanguageClient, Position, State, TextDocumentIdentifier } from 'vscode-languageclient' +import * as jose from 'jose' +import { AmazonQChatViewProvider } from './webviewProvider' + +export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { + languageClient.onDidChangeState(({ oldState, newState }) => { + if (oldState === State.Starting && newState === State.Running) { + languageClient.info( + 'Language client received initializeResult from server:', + JSON.stringify(languageClient.initializeResult) + ) + + const chatOptions = languageClient.initializeResult?.awsServerCapabilities?.chatOptions + + void provider.webview?.postMessage({ + command: CHAT_OPTIONS, + params: chatOptions, + }) + } + }) + + languageClient.onTelemetry((e) => { + languageClient.info(`[VSCode Client] Received telemetry event from server ${JSON.stringify(e)}`) + }) +} + +export function registerMessageListeners( + languageClient: LanguageClient, + provider: AmazonQChatViewProvider, + encryptionKey: Buffer +) { + provider.webview?.onDidReceiveMessage(async (message) => { + languageClient.info(`[VSCode Client] Received ${JSON.stringify(message)} from chat`) + + switch (message.command) { + case COPY_TO_CLIPBOARD: + // TODO see what we need to hook this up + languageClient.info('[VSCode Client] Copy to clipboard event received') + break + case INSERT_TO_CURSOR_POSITION: { + const editor = window.activeTextEditor + let textDocument: TextDocumentIdentifier | undefined = undefined + let cursorPosition: Position | undefined = undefined + if (editor) { + cursorPosition = editor.selection.active + textDocument = { uri: editor.document.uri.toString() } + } + + languageClient.sendNotification(insertToCursorPositionNotificationType.method, { + ...message.params, + cursorPosition, + textDocument, + }) + break + } + case AUTH_FOLLOW_UP_CLICKED: + // TODO hook this into auth + languageClient.info('[VSCode Client] AuthFollowUp clicked') + break + case chatRequestType.method: { + const partialResultToken = uuidv4() + const chatDisposable = languageClient.onProgress(chatRequestType, partialResultToken, (partialResult) => + handlePartialResult(partialResult, encryptionKey, provider, message.params.tabId) + ) + + const editor = + window.activeTextEditor || + window.visibleTextEditors.find((editor) => editor.document.languageId !== 'Log') + if (editor) { + message.params.cursorPosition = [editor.selection.active] + message.params.textDocument = { uri: editor.document.uri.toString() } + } + + const chatRequest = await encryptRequest(message.params, encryptionKey) + const chatResult = (await languageClient.sendRequest(chatRequestType.method, { + ...chatRequest, + partialResultToken, + })) as string | ChatResult + void handleCompleteResult( + chatResult, + encryptionKey, + provider, + message.params.tabId, + chatDisposable + ) + break + } + case quickActionRequestType.method: { + const quickActionPartialResultToken = uuidv4() + const quickActionDisposable = languageClient.onProgress( + quickActionRequestType, + quickActionPartialResultToken, + (partialResult) => + handlePartialResult( + partialResult, + encryptionKey, + provider, + message.params.tabId + ) + ) + + const quickActionRequest = await encryptRequest(message.params, encryptionKey) + const quickActionResult = (await languageClient.sendRequest(quickActionRequestType.method, { + ...quickActionRequest, + partialResultToken: quickActionPartialResultToken, + })) as string | ChatResult + void handleCompleteResult( + quickActionResult, + encryptionKey, + provider, + message.params.tabId, + quickActionDisposable + ) + break + } + case followUpClickNotificationType.method: + if (!isValidAuthFollowUpType(message.params.followUp.type)) { + languageClient.sendNotification(followUpClickNotificationType.method, message.params) + } + break + default: + if (isServerEvent(message.command)) { + languageClient.sendNotification(message.command, message.params) + } + break + } + }, undefined) +} + +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 + */ +async function handlePartialResult( + partialResult: string | T, + encryptionKey: Buffer | undefined, + provider: AmazonQChatViewProvider, + tabId: string +) { + const decryptedMessage = + typeof partialResult === 'string' && encryptionKey + ? await decodeRequest(partialResult, encryptionKey) + : (partialResult as T) + + if (decryptedMessage.body) { + void provider.webview?.postMessage({ + command: chatRequestType.method, + params: decryptedMessage, + isPartialResult: true, + tabId: tabId, + }) + } +} + +/** + * Decodes the final chat responses from the language server before sending it to mynah UI. + * Once this is called the answer response is finished + */ +async function handleCompleteResult( + result: string | T, + encryptionKey: Buffer | undefined, + provider: AmazonQChatViewProvider, + tabId: string, + disposable: Disposable +) { + const decryptedMessage = + typeof result === 'string' && encryptionKey ? await decodeRequest(result, encryptionKey) : result + + void provider.webview?.postMessage({ + command: chatRequestType.method, + params: decryptedMessage, + tabId: tabId, + }) + disposable.dispose() +} diff --git a/packages/amazonq/src/lsp/chat/webviewProvider.ts b/packages/amazonq/src/lsp/chat/webviewProvider.ts new file mode 100644 index 00000000000..7bfab17f3ae --- /dev/null +++ b/packages/amazonq/src/lsp/chat/webviewProvider.ts @@ -0,0 +1,73 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EventEmitter, + CancellationToken, + Webview, + WebviewView, + WebviewViewProvider, + WebviewViewResolveContext, + Uri, +} from 'vscode' +import { LanguageServerResolver } from 'aws-core-vscode/shared' + +export class AmazonQChatViewProvider implements WebviewViewProvider { + public static readonly viewType = 'aws.amazonq.AmazonQChatView' + private readonly onDidResolveWebviewEmitter = new EventEmitter() + public readonly onDidResolveWebview = this.onDidResolveWebviewEmitter.event + + webview: Webview | undefined + + constructor(private readonly mynahUIPath: string) {} + + public resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, _token: CancellationToken) { + this.webview = webviewView.webview + + const lspDir = Uri.parse(LanguageServerResolver.defaultDir) + webviewView.webview.options = { + enableScripts: true, + enableCommandUris: true, + localResourceRoots: [lspDir], + } + + const uiPath = webviewView.webview.asWebviewUri(Uri.parse(this.mynahUIPath)).toString() + webviewView.webview.html = getWebviewContent(uiPath) + + this.onDidResolveWebviewEmitter.fire() + } +} + +function getWebviewContent(mynahUIPath: string) { + return ` + + + + + + Chat + + + + + + + ` +} diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index f9ef046221b..297ac21c1d6 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -11,18 +11,16 @@ import { registerInlineCompletion } from '../app/inline/completion' import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { ConnectionMetadata } from '@aws/language-server-runtimes/protocol' -import { - ResourcePaths, - Settings, - oidcClientName, - createServerOptions, - globals, - getLogger, -} from 'aws-core-vscode/shared' +import { Settings, oidcClientName, createServerOptions, globals, Experiments, getLogger } from 'aws-core-vscode/shared' +import { activate } from './chat/activation' +import { AmazonQResourcePaths } from './lspInstaller' const localize = nls.loadMessageBundle() -export async function startLanguageServer(extensionContext: vscode.ExtensionContext, resourcePaths: ResourcePaths) { +export async function startLanguageServer( + extensionContext: vscode.ExtensionContext, + resourcePaths: AmazonQResourcePaths +) { const toDispose = extensionContext.subscriptions const serverModule = resourcePaths.lsp @@ -97,6 +95,9 @@ export async function startLanguageServer(extensionContext: vscode.ExtensionCont return client.onReady().then(async () => { await auth.init() registerInlineCompletion(client) + if (Experiments.instance.get('amazonqChatLSP', false)) { + activate(client, encryptionKey, resourcePaths.mynahUI) + } // Request handler for when the server wants to know about the clients auth connnection client.onRequest(notificationTypes.getConnectionMetadata.method, () => { diff --git a/packages/amazonq/src/lsp/lspInstaller.ts b/packages/amazonq/src/lsp/lspInstaller.ts index ae7fe39cca5..31866588e07 100644 --- a/packages/amazonq/src/lsp/lspInstaller.ts +++ b/packages/amazonq/src/lsp/lspInstaller.ts @@ -8,7 +8,11 @@ import path from 'path' import { getAmazonQLspConfig } from './config' import { LspConfig } from 'aws-core-vscode/amazonq' -export class AmazonQLspInstaller extends BaseLspInstaller.BaseLspInstaller { +export interface AmazonQResourcePaths extends ResourcePaths { + mynahUI: string +} + +export class AmazonQLspInstaller extends BaseLspInstaller.BaseLspInstaller { constructor(lspConfig: LspConfig = getAmazonQLspConfig()) { super(lspConfig, 'amazonqLsp') } @@ -18,11 +22,12 @@ export class AmazonQLspInstaller extends BaseLspInstaller.BaseLspInstaller { await fs.chmod(resourcePaths.node, 0o755) } - protected override resourcePaths(assetDirectory?: string): ResourcePaths { + protected override resourcePaths(assetDirectory?: string): AmazonQResourcePaths { if (!assetDirectory) { return { lsp: this.config.path ?? '', node: getNodeExecutableName(), + mynahUI: '', // TODO make mynah UI configurable } } @@ -30,6 +35,7 @@ export class AmazonQLspInstaller extends BaseLspInstaller.BaseLspInstaller { return { lsp: path.join(assetDirectory, 'servers/aws-lsp-codewhisperer.js'), node: nodePath, + mynahUI: path.join(assetDirectory, 'clients/amazonq-ui.js'), } } } diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index ccc229e9445..4f3bcdf57e7 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -14,7 +14,7 @@ import { Range } from 'semver' import { getLogger } from '../logger/logger' import type { Logger, LogTopic } from '../logger/logger' -export abstract class BaseLspInstaller { +export abstract class BaseLspInstaller { private logger: Logger constructor( @@ -24,7 +24,7 @@ export abstract class BaseLspInstaller { this.logger = getLogger(loggerName) } - async resolve(): Promise { + async resolve(): Promise> { const { id, manifestUrl, supportedVersions, path } = this.config if (path) { const overrideMsg = `Using language server override location: ${path}` @@ -61,5 +61,5 @@ export abstract class BaseLspInstaller { } protected abstract postInstall(assetDirectory: string): Promise - protected abstract resourcePaths(assetDirectory?: string): ResourcePaths + protected abstract resourcePaths(assetDirectory?: string): T } diff --git a/packages/core/src/shared/lsp/types.ts b/packages/core/src/shared/lsp/types.ts index 95f2c61aad5..21bd8ff4e77 100644 --- a/packages/core/src/shared/lsp/types.ts +++ b/packages/core/src/shared/lsp/types.ts @@ -18,12 +18,9 @@ export interface ResourcePaths { lsp: string node: string } -export interface LspResolution extends LspResult { - resourcePaths: ResourcePaths -} -export interface LspResolver { - resolve(): Promise +export interface LspResolution extends LspResult { + resourcePaths: T } export interface TargetContent { diff --git a/packages/core/src/shared/settings-toolkit.gen.ts b/packages/core/src/shared/settings-toolkit.gen.ts index 8e4cc453d03..5cd9854b1fc 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -42,7 +42,8 @@ export const toolkitSettings = { }, "aws.experiments": { "jsonResourceModification": {}, - "amazonqLSP": {} + "amazonqLSP": {}, + "amazonqChatLSP": {} }, "aws.resources.enabledResources": {}, "aws.lambda.recentlyUploaded": {}, diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 8caabc73ba4..22542b07149 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -250,6 +250,10 @@ "amazonqLSP": { "type": "boolean", "default": false + }, + "amazonqChatLSP": { + "type": "boolean", + "default": false } }, "additionalProperties": false From e7b73071818f8a7a14c3be06d3a89823a7d94407 Mon Sep 17 00:00:00 2001 From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:38:22 -0400 Subject: [PATCH 16/18] telemetry(webview): Emit toolkit_X module telemetry on auth webview (#6791) ## Problem: With our telemetry, we do not know when the frontend webview UI has actually loaded. The current process looks like the following: - We create a webview and set the HTML to load, but after that we do not have a formal way to detect if the webview actually loaded the HTML/JS successfully. We only know that the process started (`toolkit_willOpenModule`) ## Solution: Emit certain metrics during the webview loading process to get a better idea of if the webview UI successfully completed its initial load. - `toolkit_willOpenModule`, indicates intent to render a webview. It does not mean the user is seeing anything. - `toolkit_didLoadModule`, indicates the final result of loading the webview - We know a `result: Succeeded` when the frontend send a successful message to the backend. It knows this by ensuring there were no errors and that a certain HTML element can be found, then once the page finishes its initial load it will send a success message to the backend. - On `result: Failed`, what happens is a timer has timed out after 10 seconds. We assume that since there was no response from the frontend, it failed to fully execute the HTML/JS. - State is shared between `toolkit_willOpenModule` and `toolkit_didLoadModule` so that we can connect them through telemetry. This includes `traceId` and the `duration` which is the time between the 2 metrics. This PR only applies to the Login and Reauth page for now, and future Vue webviews will need to implement some things on their end to get this functionality. ## TODO - Generalize this solution in a more robust way for other webviews to easily implement this functionality --- - 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 --- .../webview/messages/messageDispatcher.ts | 25 ++-- packages/core/src/amazonq/webview/ui/main.ts | 2 +- .../login/webview/commonAuthViewProvider.ts | 3 +- .../webview/vue/amazonq/backend_amazonq.ts | 1 + .../core/src/login/webview/vue/backend.ts | 23 ++-- packages/core/src/login/webview/vue/login.vue | 15 ++- .../src/login/webview/vue/reauthenticate.vue | 19 ++- packages/core/src/login/webview/vue/root.vue | 73 ++++++++++- .../src/shared/telemetry/vscodeTelemetry.json | 10 -- packages/core/src/webviews/main.ts | 113 ++++++++++++++++++ 10 files changed, 246 insertions(+), 38 deletions(-) diff --git a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts index dc3bc4c77c8..f17de54e416 100644 --- a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts +++ b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts @@ -16,6 +16,8 @@ import globals from '../../../shared/extensionGlobals' import { openUrl } from '../../../shared/utilities/vsCodeUtils' import { DefaultAmazonQAppInitContext } from '../../apps/initContext' +const qChatModuleName = 'amazonqChat' + export function dispatchWebViewMessagesToApps( webview: Webview, webViewToAppsMessagePublishers: Map> @@ -29,8 +31,8 @@ export function dispatchWebViewMessagesToApps( * This would be equivalent of the duration between "user clicked open q" and "ui has become available" * NOTE: Amazon Q UI is only loaded ONCE. The state is saved between each hide/show of the webview. */ - telemetry.webview_load.emit({ - webviewName: 'amazonq', + telemetry.toolkit_didLoadModule.emit({ + module: qChatModuleName, duration: performance.measure(amazonqMark.uiReady, amazonqMark.open).duration, result: 'Succeeded', }) @@ -86,12 +88,19 @@ export function dispatchWebViewMessagesToApps( } if (msg.type === 'error') { - const event = msg.event === 'webview_load' ? telemetry.webview_load : telemetry.webview_error - event.emit({ - webviewName: 'amazonqChat', - result: 'Failed', - reasonDesc: msg.errorMessage, - }) + if (msg.event === 'toolkit_didLoadModule') { + telemetry.toolkit_didLoadModule.emit({ + module: qChatModuleName, + result: 'Failed', + reasonDesc: msg.errorMessage, + }) + } else { + telemetry.webview_error.emit({ + webviewName: qChatModuleName, + result: 'Failed', + reasonDesc: msg.errorMessage, + }) + } return } diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index d7285d81ba5..5ae03840f8f 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -63,7 +63,7 @@ export const createMynahUI = ( const { error, message } = e ideApi.postMessage({ type: 'error', - event: connector.isUIReady ? 'webview_error' : 'webview_load', + event: connector.isUIReady ? 'webview_error' : 'toolkit_didLoadModule', errorMessage: error ? error.toString() : message, }) }) diff --git a/packages/core/src/login/webview/commonAuthViewProvider.ts b/packages/core/src/login/webview/commonAuthViewProvider.ts index 12caf3c2ad5..d9d6f3b4e30 100644 --- a/packages/core/src/login/webview/commonAuthViewProvider.ts +++ b/packages/core/src/login/webview/commonAuthViewProvider.ts @@ -135,9 +135,10 @@ export class CommonAuthViewProvider implements WebviewViewProvider { enableCommandUris: true, localResourceRoots: [dist, resources], } - webviewView.webview.html = this._getHtmlForWebview(this.extensionContext.extensionUri, webviewView.webview) // register the webview server await this.webView?.setup(webviewView.webview) + + webviewView.webview.html = this._getHtmlForWebview(this.extensionContext.extensionUri, webviewView.webview) } private _getHtmlForWebview(extensionURI: Uri, webview: vscode.Webview) { 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 83c511ad9f6..7132e91afec 100644 --- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts +++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts @@ -27,6 +27,7 @@ const className = 'AmazonQLoginWebview' export class AmazonQLoginWebview extends CommonAuthWebview { public override id: string = 'aws.amazonq.AmazonCommonAuth' public static sourcePath: string = 'vue/src/login/webview/vue/amazonq/index.js' + public override supportsLoadTelemetry: boolean = true override onActiveConnectionModified = new vscode.EventEmitter() diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts index 9bc6c5ae339..43d86feedf9 100644 --- a/packages/core/src/login/webview/vue/backend.ts +++ b/packages/core/src/login/webview/vue/backend.ts @@ -62,18 +62,25 @@ export abstract class CommonAuthWebview extends VueWebview { } private didCall: { login: boolean; reauth: boolean } = { login: false, reauth: false } - public setUiReady(state: 'login' | 'reauth') { - // Prevent telemetry spam, since showing/hiding chat triggers this each time. - // So only emit once. + /** + * Called when the UI load process is completed, regardless of success or failure + * + * @param errorMessage IF an error is caught on the frontend, this is the message. It will result in a failure metric. + * Otherwise we assume success. + */ + public setUiReady(state: 'login' | 'reauth', errorMessage?: string) { + // Only emit once to prevent telemetry spam, since showing/hiding chat triggers this each time. + // TODO: Research how to not trigger this on every show/hide if (this.didCall[state]) { return } - telemetry.webview_load.emit({ - passive: true, - webviewName: state, - result: 'Succeeded', - }) + if (errorMessage) { + this.setLoadFailure(state, errorMessage) + } else { + this.setDidLoad(state) + } + this.didCall[state] = true } diff --git a/packages/core/src/login/webview/vue/login.vue b/packages/core/src/login/webview/vue/login.vue index d21bf911802..831a1dfcb22 100644 --- a/packages/core/src/login/webview/vue/login.vue +++ b/packages/core/src/login/webview/vue/login.vue @@ -145,6 +145,7 @@ >