From 734351177f9b26a855c17d7e265fb4f06412dadf Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Mon, 23 Jun 2025 14:25:17 -0700 Subject: [PATCH 01/20] migrate process env proxy settings to proxyUtil.ts --- .../core/src/shared/lsp/utils/platform.ts | 76 ------------------- .../core/src/shared/utilities/proxyUtil.ts | 71 ++++++++++++++++- 2 files changed, 67 insertions(+), 80 deletions(-) diff --git a/packages/core/src/shared/lsp/utils/platform.ts b/packages/core/src/shared/lsp/utils/platform.ts index 67f3b95c296..87e74e5f129 100644 --- a/packages/core/src/shared/lsp/utils/platform.ts +++ b/packages/core/src/shared/lsp/utils/platform.ts @@ -8,10 +8,6 @@ import { Logger, getLogger } from '../../logger/logger' import { ChildProcess } from '../../utilities/processUtils' import { waitUntil } from '../../utilities/timeoutUtils' import { isDebugInstance } from '../../vscode/env' -import { tmpdir } from 'os' -import { join } from 'path' -import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports -import * as vscode from 'vscode' export function getNodeExecutableName(): string { return process.platform === 'win32' ? 'node.exe' : 'node' @@ -85,53 +81,6 @@ export async function validateNodeExe(nodePath: string[], lsp: string, args: str } } -/** - * Gets proxy settings and certificates from VS Code - */ -export async function getVSCodeSettings(): Promise<{ proxyUrl?: string; certificatePath?: string }> { - const result: { proxyUrl?: string; certificatePath?: string } = {} - const logger = getLogger('amazonqLsp') - - try { - // Get proxy settings from VS Code configuration - const httpConfig = vscode.workspace.getConfiguration('http') - const proxy = httpConfig.get('proxy') - if (proxy) { - result.proxyUrl = proxy - logger.info(`Using proxy from VS Code settings: ${proxy}`) - } - } catch (err) { - logger.error(`Failed to get VS Code settings: ${err}`) - return result - } - try { - const tls = await import('tls') - // @ts-ignore Get system certificates - const systemCerts = tls.getCACertificates('system') - // @ts-ignore Get any existing extra certificates - const extraCerts = tls.getCACertificates('extra') - const allCerts = [...systemCerts, ...extraCerts] - if (allCerts && allCerts.length > 0) { - logger.info(`Found ${allCerts.length} certificates in system's trust store`) - - const tempDir = join(tmpdir(), 'aws-toolkit-vscode') - if (!nodefs.existsSync(tempDir)) { - nodefs.mkdirSync(tempDir, { recursive: true }) - } - - const certPath = join(tempDir, 'vscode-ca-certs.pem') - const certContent = allCerts.join('\n') - - nodefs.writeFileSync(certPath, certContent) - result.certificatePath = certPath - logger.info(`Created certificate file at: ${certPath}`) - } - } catch (err) { - logger.error(`Failed to extract certificates: ${err}`) - } - return result -} - export function createServerOptions({ encryptionKey, executable, @@ -160,31 +109,6 @@ export function createServerOptions({ Object.assign(processEnv, env) } - // Get settings from VS Code - const settings = await getVSCodeSettings() - const logger = getLogger('amazonqLsp') - - // Add proxy settings to the Node.js process - if (settings.proxyUrl) { - processEnv.HTTPS_PROXY = settings.proxyUrl - } - - // Add certificate path if available - if (settings.certificatePath) { - processEnv.NODE_EXTRA_CA_CERTS = settings.certificatePath - logger.info(`Using certificate file: ${settings.certificatePath}`) - } - - // Get SSL verification settings - const httpConfig = vscode.workspace.getConfiguration('http') - const strictSSL = httpConfig.get('proxyStrictSSL', true) - - // Handle SSL certificate verification - if (!strictSSL) { - processEnv.NODE_TLS_REJECT_UNAUTHORIZED = '0' - logger.info('SSL verification disabled via VS Code settings') - } - const lspProcess = new ChildProcess(bin, args, { warnThresholds, spawnOptions: { diff --git a/packages/core/src/shared/utilities/proxyUtil.ts b/packages/core/src/shared/utilities/proxyUtil.ts index 5c37c5e3e46..351badc0d4e 100644 --- a/packages/core/src/shared/utilities/proxyUtil.ts +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -5,9 +5,14 @@ import vscode from 'vscode' import { getLogger } from '../logger/logger' +import { tmpdir } from 'os' +import { join } from 'path' +import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports interface ProxyConfig { proxyUrl: string | undefined + noProxy: string | undefined + proxyStrictSSL: boolean | true certificateAuthority: string | undefined } @@ -23,11 +28,11 @@ export class ProxyUtil { * See documentation here for setting the environement variables which are inherited by Flare LS process: * https://github.com/aws/language-server-runtimes/blob/main/runtimes/docs/proxy.md */ - public static configureProxyForLanguageServer(): void { + public static async configureProxyForLanguageServer(): Promise { try { const proxyConfig = this.getProxyConfiguration() - this.setProxyEnvironmentVariables(proxyConfig) + await this.setProxyEnvironmentVariables(proxyConfig) } catch (err) { this.logger.error(`Failed to configure proxy: ${err}`) } @@ -41,6 +46,13 @@ export class ProxyUtil { const proxyUrl = httpConfig.get('proxy') this.logger.debug(`Proxy URL Setting in VSCode Settings: ${proxyUrl}`) + const noProxy = httpConfig.get('noProxy') + if (noProxy) { + this.logger.info(`Using noProxy from VS Code settings: ${noProxy}`) + } + + const proxyStrictSSL = httpConfig.get('proxyStrictSSL', true) + const amazonQConfig = vscode.workspace.getConfiguration('amazonQ') const proxySettings = amazonQConfig.get<{ certificateAuthority?: string @@ -48,6 +60,8 @@ export class ProxyUtil { return { proxyUrl, + noProxy, + proxyStrictSSL, certificateAuthority: proxySettings.certificateAuthority, } } @@ -55,7 +69,7 @@ export class ProxyUtil { /** * Sets environment variables based on proxy configuration */ - private static setProxyEnvironmentVariables(config: ProxyConfig): void { + private static async setProxyEnvironmentVariables(config: ProxyConfig): Promise { const proxyUrl = config.proxyUrl // Set proxy environment variables if (proxyUrl) { @@ -64,11 +78,60 @@ export class ProxyUtil { this.logger.debug(`Set proxy environment variables: ${proxyUrl}`) } - // Set certificate bundle environment variables if configured + // set NO_PROXY vals + const noProxy = config.noProxy + if (noProxy) { + process.env.NO_PROXY = noProxy + this.logger.debug(`Set NO_PROXY environment variable: ${noProxy}`) + } + + const strictSSL = config.proxyStrictSSL + // Handle SSL certificate verification + if (!strictSSL) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + this.logger.info('SSL verification disabled via VS Code settings') + } + + // Set certificate bundle environment variables if user configured if (config.certificateAuthority) { process.env.NODE_EXTRA_CA_CERTS = config.certificateAuthority process.env.AWS_CA_BUNDLE = config.certificateAuthority this.logger.debug(`Set certificate bundle path: ${config.certificateAuthority}`) + } else { + // Fallback to system certificates if no custom CA is configured + await this.setSystemCertificates() + } + } + + /** + * Sets system certificates as fallback when no custom CA is configured + */ + private static async setSystemCertificates(): Promise { + try { + const tls = await import('tls') + // @ts-ignore Get system certificates + const systemCerts = tls.getCACertificates('system') + // @ts-ignore Get any existing extra certificates + const extraCerts = tls.getCACertificates('extra') + const allCerts = [...systemCerts, ...extraCerts] + if (allCerts && allCerts.length > 0) { + this.logger.debug(`Found ${allCerts.length} certificates in system's trust store`) + + const tempDir = join(tmpdir(), 'aws-toolkit-vscode') + if (!nodefs.existsSync(tempDir)) { + nodefs.mkdirSync(tempDir, { recursive: true }) + } + + const certPath = join(tempDir, 'vscode-ca-certs.pem') + const certContent = allCerts.join('\n') + + nodefs.writeFileSync(certPath, certContent) + process.env.NODE_EXTRA_CA_CERTS = certPath + process.env.AWS_CA_BUNDLE = certPath + this.logger.debug(`Set system certificate bundle path: ${certPath}`) + } + } catch (err) { + this.logger.error(`Failed to extract system certificates: ${err}`) } } } From 7b6252fba71e0ff931a98a250d7123c27fbc57bd Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Mon, 23 Jun 2025 14:40:08 -0700 Subject: [PATCH 02/20] remove env merging from createServerOptions --- packages/core/src/shared/lsp/utils/platform.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/core/src/shared/lsp/utils/platform.ts b/packages/core/src/shared/lsp/utils/platform.ts index 87e74e5f129..fadeefb7e68 100644 --- a/packages/core/src/shared/lsp/utils/platform.ts +++ b/packages/core/src/shared/lsp/utils/platform.ts @@ -4,7 +4,7 @@ */ import { ToolkitError } from '../../errors' -import { Logger, getLogger } from '../../logger/logger' +import { Logger } from '../../logger/logger' import { ChildProcess } from '../../utilities/processUtils' import { waitUntil } from '../../utilities/timeoutUtils' import { isDebugInstance } from '../../vscode/env' @@ -87,14 +87,12 @@ export function createServerOptions({ serverModule, execArgv, warnThresholds, - env, }: { encryptionKey: Buffer executable: string[] serverModule: string execArgv: string[] warnThresholds?: { cpu?: number; memory?: number } - env?: Record }) { return async () => { const bin = executable[0] @@ -103,18 +101,7 @@ export function createServerOptions({ args.unshift('--inspect=6080') } - // Merge environment variables - const processEnv = { ...process.env } - if (env) { - Object.assign(processEnv, env) - } - - const lspProcess = new ChildProcess(bin, args, { - warnThresholds, - spawnOptions: { - env: processEnv, - }, - }) + const lspProcess = new ChildProcess(bin, args, { warnThresholds }) // this is a long running process, awaiting it will never resolve void lspProcess.run() From ebfbc669daa65212c34ce25ae3f92926d4110d78 Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Mon, 23 Jun 2025 16:03:37 -0700 Subject: [PATCH 03/20] No need to set CA certs when SSL verification is disabled --- packages/core/src/shared/utilities/proxyUtil.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/shared/utilities/proxyUtil.ts b/packages/core/src/shared/utilities/proxyUtil.ts index 351badc0d4e..a8d2056d7f4 100644 --- a/packages/core/src/shared/utilities/proxyUtil.ts +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -90,6 +90,7 @@ export class ProxyUtil { if (!strictSSL) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' this.logger.info('SSL verification disabled via VS Code settings') + return // No need to set CA certs when SSL verification is disabled } // Set certificate bundle environment variables if user configured From 771fae85e748db5f572b8ec9e390b2fab14209ea Mon Sep 17 00:00:00 2001 From: Diler Zaza <95944688+l0minous@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:23:15 -0700 Subject: [PATCH 04/20] fix(stepfunctions): Add document URI check for save telemetry and enable deployment from WFS (#7315) ## Problem - Save telemetry was being recorded for all document saves in VS Code, not just for the active workflow studio document. - Save & Deploy functionality required closing Workflow Studio before starting deployment ## Solution - Add URI comparison check to ensure telemetry is only recorded when the saved document matches the current workflow studio document. - Refactored publishStateMachine.ts to accept an optional TextDocument parameter and updated activation.ts to support new interface - Removed closeCustomEditorMessageHandler call from saveFileAndDeployMessageHandler --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Diler Zaza --- packages/core/src/stepFunctions/activation.ts | 2 +- .../commands/publishStateMachine.ts | 23 ++++++++++++------- .../workflowStudio/handleMessage.ts | 13 ++++++++--- .../workflowStudio/workflowStudioEditor.ts | 20 ++++++++-------- ...-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json | 4 ++++ ...-de3cdda1-252e-4d04-96cb-7fb935649c0e.json | 4 ++++ 6 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json create mode 100644 packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json diff --git a/packages/core/src/stepFunctions/activation.ts b/packages/core/src/stepFunctions/activation.ts index 4898fc36b54..ab37cb7a09a 100644 --- a/packages/core/src/stepFunctions/activation.ts +++ b/packages/core/src/stepFunctions/activation.ts @@ -96,7 +96,7 @@ async function registerStepFunctionCommands( }), Commands.register('aws.stepfunctions.publishStateMachine', async (node?: any) => { const region: string | undefined = node?.regionCode - await publishStateMachine(awsContext, outputChannel, region) + await publishStateMachine({ awsContext: awsContext, outputChannel: outputChannel, region: region }) }) ) } diff --git a/packages/core/src/stepFunctions/commands/publishStateMachine.ts b/packages/core/src/stepFunctions/commands/publishStateMachine.ts index e07b21b86f2..385a412478f 100644 --- a/packages/core/src/stepFunctions/commands/publishStateMachine.ts +++ b/packages/core/src/stepFunctions/commands/publishStateMachine.ts @@ -15,14 +15,21 @@ import { refreshStepFunctionsTree } from '../explorer/stepFunctionsNodes' import { PublishStateMachineWizard, PublishStateMachineWizardState } from '../wizards/publishStateMachineWizard' const localize = nls.loadMessageBundle() -export async function publishStateMachine( - awsContext: AwsContext, - outputChannel: vscode.OutputChannel, +interface publishStateMachineParams { + awsContext: AwsContext + outputChannel: vscode.OutputChannel region?: string -) { + text?: vscode.TextDocument +} +export async function publishStateMachine(params: publishStateMachineParams) { const logger: Logger = getLogger() + let textDocument: vscode.TextDocument | undefined - const textDocument = vscode.window.activeTextEditor?.document + if (params.text) { + textDocument = params.text + } else { + textDocument = vscode.window.activeTextEditor?.document + } if (!textDocument) { logger.error('Could not get active text editor for state machine definition') @@ -53,17 +60,17 @@ export async function publishStateMachine( } try { - const response = await new PublishStateMachineWizard(region).run() + const response = await new PublishStateMachineWizard(params.region).run() if (!response) { return } const client = new DefaultStepFunctionsClient(response.region) if (response?.createResponse) { - await createStateMachine(response.createResponse, text, outputChannel, response.region, client) + await createStateMachine(response.createResponse, text, params.outputChannel, response.region, client) refreshStepFunctionsTree(response.region) } else if (response?.updateResponse) { - await updateStateMachine(response.updateResponse, text, outputChannel, response.region, client) + await updateStateMachine(response.updateResponse, text, params.outputChannel, response.region, client) } } catch (err) { logger.error(err as Error) diff --git a/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts b/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts index 13477db19e2..2b548ab957f 100644 --- a/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts +++ b/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts @@ -202,15 +202,22 @@ async function saveFileMessageHandler(request: SaveFileRequestMessage, context: } /** - * Handler for saving a file and starting the state machine deployment flow, while also switching to default editor. + * Handler for saving a file and starting the state machine deployment flow while staying in WFS view. * Triggered when the user triggers 'Save and Deploy' action in WFS * @param request The request message containing the file contents. * @param context The webview context containing the necessary information for saving the file. */ async function saveFileAndDeployMessageHandler(request: SaveFileRequestMessage, context: WebviewContext) { await saveFileMessageHandler(request, context) - await closeCustomEditorMessageHandler(context) - await publishStateMachine(globals.awsContext, globals.outputChannel) + await publishStateMachine({ + awsContext: globals.awsContext, + outputChannel: globals.outputChannel, + text: context.textDocument, + }) + + telemetry.ui_click.emit({ + elementId: 'stepfunctions_saveAndDeploy', + }) } /** diff --git a/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts b/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts index da1a9f2e9bf..ba719856516 100644 --- a/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts +++ b/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts @@ -147,16 +147,18 @@ export class WorkflowStudioEditor { // The text document acts as our model, thus we send and event to the webview on file save to trigger update contextObject.disposables.push( - vscode.workspace.onDidSaveTextDocument(async () => { - await telemetry.stepfunctions_saveFile.run(async (span) => { - span.record({ - id: contextObject.fileId, - saveType: 'MANUAL_SAVE', - source: 'VSCODE', - isInvalidJson: isInvalidJsonFile(contextObject.textDocument), + vscode.workspace.onDidSaveTextDocument(async (savedDocument) => { + if (savedDocument.uri.toString() === this.documentUri.toString()) { + await telemetry.stepfunctions_saveFile.run(async (span) => { + span.record({ + id: contextObject.fileId, + saveType: 'MANUAL_SAVE', + source: 'VSCODE', + isInvalidJson: isInvalidJsonFile(contextObject.textDocument), + }) + await broadcastFileChange(contextObject, 'MANUAL_SAVE') }) - await broadcastFileChange(contextObject, 'MANUAL_SAVE') - }) + } }) ) diff --git a/packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json b/packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json new file mode 100644 index 00000000000..1d0f2041fa8 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "State Machine deployments can now be initiated directly from Workflow Studio without closing the editor" +} diff --git a/packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json b/packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json new file mode 100644 index 00000000000..2e1c167dccd --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Step Function performance metrics now accurately reflect only Workflow Studio document activity" +} From f1dc9b846832e856ae41352602ba60feb16812ff Mon Sep 17 00:00:00 2001 From: David <60020664+dhasani23@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:02:30 -0700 Subject: [PATCH 05/20] feat(amazonq): enable client-side build (#7226) ## Problem Instead of running `mvn dependency:copy-dependencies` and `mvn clean install`, we have a JAR that we can execute which will gather all of the project dependencies as well as some important metadata stored in a `compilations.json` file which our service will use to improve the quality of transformations. ## Solution Remove Maven shell commands; add custom JAR execution. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: David Hasani --- ...-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json | 4 + .../test/e2e/amazonq/transformByQ.test.ts | 9 +- .../resources/amazonQCT/QCT-Maven-6-16.jar | Bin 0 -> 150250 bytes .../chat/controller/controller.ts | 40 +---- .../chat/controller/messenger/messenger.ts | 8 +- packages/core/src/amazonqGumby/errors.ts | 6 - .../src/codewhisperer/client/codewhisperer.ts | 8 +- .../commands/startTransformByQ.ts | 78 ++++------ .../src/codewhisperer/models/constants.ts | 26 ++-- .../core/src/codewhisperer/models/model.ts | 27 ++-- .../transformByQ/transformApiHandler.ts | 145 ++++++++++-------- .../transformByQ/transformFileHandler.ts | 16 +- .../transformByQ/transformMavenHandler.ts | 139 ++++------------- .../transformationResultsViewProvider.ts | 4 +- packages/core/src/dev/config.ts | 3 - .../commands/transformByQ.test.ts | 23 +-- .../core/src/testInteg/perf/zipcode.test.ts | 1 - 17 files changed, 210 insertions(+), 327 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json create mode 100644 packages/core/resources/amazonQCT/QCT-Maven-6-16.jar diff --git a/packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json b/packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json new file mode 100644 index 00000000000..8028e402f9f --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "/transform: run all builds client-side" +} diff --git a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts index 4493a7c2387..7a9273a1e84 100644 --- a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts +++ b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts @@ -129,8 +129,6 @@ describe('Amazon Q Code Transformation', function () { waitIntervalInMs: 1000, }) - // TO-DO: add this back when releasing CSB - /* const customDependencyVersionPrompt = tab.getChatItems().pop() assert.strictEqual( customDependencyVersionPrompt?.body?.includes('You can optionally upload a YAML file'), @@ -139,11 +137,10 @@ describe('Amazon Q Code Transformation', function () { tab.clickCustomFormButton({ id: 'gumbyTransformFormContinue' }) // 2 additional chat messages get sent after Continue button clicked; wait for both of them - await tab.waitForEvent(() => tab.getChatItems().length > 13, { + await tab.waitForEvent(() => tab.getChatItems().length > 10, { waitTimeoutInMs: 5000, waitIntervalInMs: 1000, }) - */ const sourceJdkPathPrompt = tab.getChatItems().pop() assert.strictEqual(sourceJdkPathPrompt?.body?.includes('Enter the path to JDK 8'), true) @@ -151,7 +148,7 @@ describe('Amazon Q Code Transformation', function () { tab.addChatMessage({ prompt: '/dummy/path/to/jdk8' }) // 2 additional chat messages get sent after JDK path submitted; wait for both of them - await tab.waitForEvent(() => tab.getChatItems().length > 10, { + await tab.waitForEvent(() => tab.getChatItems().length > 12, { waitTimeoutInMs: 5000, waitIntervalInMs: 1000, }) @@ -173,7 +170,7 @@ describe('Amazon Q Code Transformation', function () { text: 'View summary', }) - await tab.waitForEvent(() => tab.getChatItems().length > 11, { + await tab.waitForEvent(() => tab.getChatItems().length > 13, { waitTimeoutInMs: 5000, waitIntervalInMs: 1000, }) diff --git a/packages/core/resources/amazonQCT/QCT-Maven-6-16.jar b/packages/core/resources/amazonQCT/QCT-Maven-6-16.jar new file mode 100644 index 0000000000000000000000000000000000000000..bdc734b4d7b60a2436ea58f275e81c284ad1bcad GIT binary patch literal 150250 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¥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

TUy_C2)4&sFE1zaw}1 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

Dbvs+GeC3K#D{J%ikfXlN7yT<-%@_ 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 literal 0 HcmV?d00001 diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index 57367143cd4..3b40ad9882f 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -41,13 +41,9 @@ import { } from '../../errors' import * as CodeWhispererConstants from '../../../codewhisperer/models/constants' import MessengerUtils, { ButtonActions, GumbyCommands } from './messenger/messengerUtils' -import { CancelActionPositions, JDKToTelemetryValue, telemetryUndefined } from '../../telemetry/codeTransformTelemetry' +import { CancelActionPositions } from '../../telemetry/codeTransformTelemetry' import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { - telemetry, - CodeTransformJavaTargetVersionsAllowed, - CodeTransformJavaSourceVersionsAllowed, -} from '../../../shared/telemetry/telemetry' +import { telemetry } from '../../../shared/telemetry/telemetry' import { CodeTransformTelemetryState } from '../../telemetry/codeTransformTelemetryState' import DependencyVersions from '../../models/dependencies' import { getStringHash } from '../../../shared/utilities/textUtilities' @@ -308,7 +304,6 @@ export class GumbyController { } private async validateLanguageUpgradeProjects(message: any) { - let telemetryJavaVersion = JDKToTelemetryValue(JDKVersion.UNSUPPORTED) as CodeTransformJavaSourceVersionsAllowed try { const validProjects = await telemetry.codeTransform_validateProject.run(async () => { telemetry.record({ @@ -317,12 +312,6 @@ export class GumbyController { }) const validProjects = await getValidLanguageUpgradeCandidateProjects() - if (validProjects.length > 0) { - // validProjects[0].JDKVersion will be undefined if javap errors out or no .class files found, so call it UNSUPPORTED - const javaVersion = validProjects[0].JDKVersion ?? JDKVersion.UNSUPPORTED - telemetryJavaVersion = JDKToTelemetryValue(javaVersion) as CodeTransformJavaSourceVersionsAllowed - } - telemetry.record({ codeTransformLocalJavaVersion: telemetryJavaVersion }) return validProjects }) return validProjects @@ -384,7 +373,7 @@ export class GumbyController { break case ButtonActions.CONTINUE_TRANSFORMATION_FORM: this.messenger.sendMessage( - CodeWhispererConstants.continueWithoutYamlMessage, + CodeWhispererConstants.continueWithoutConfigFileMessage, message.tabID, 'ai-prompt' ) @@ -437,9 +426,7 @@ export class GumbyController { userChoice: skipTestsSelection, }) this.messenger.sendSkipTestsSelectionMessage(skipTestsSelection, message.tabID) - this.promptJavaHome('source', message.tabID) - // TO-DO: delete line above and uncomment line below when releasing CSB - // await this.messenger.sendCustomDependencyVersionMessage(message.tabID) + await this.messenger.sendCustomDependencyVersionMessage(message.tabID) }) } @@ -465,16 +452,9 @@ export class GumbyController { const fromJDKVersion: JDKVersion = message.formSelectedValues['GumbyTransformJdkFromForm'] telemetry.record({ - // TODO: remove JavaSource/TargetVersionsAllowed when BI is updated to use source/target - codeTransformJavaSourceVersionsAllowed: JDKToTelemetryValue( - fromJDKVersion - ) as CodeTransformJavaSourceVersionsAllowed, - codeTransformJavaTargetVersionsAllowed: JDKToTelemetryValue( - toJDKVersion - ) as CodeTransformJavaTargetVersionsAllowed, source: fromJDKVersion, target: toJDKVersion, - codeTransformProjectId: pathToProject === undefined ? telemetryUndefined : getStringHash(pathToProject), + codeTransformProjectId: pathToProject === undefined ? undefined : getStringHash(pathToProject), userChoice: 'Confirm-Java', }) @@ -503,7 +483,7 @@ export class GumbyController { const schema: string = message.formSelectedValues['GumbyTransformSQLSchemaForm'] telemetry.record({ - codeTransformProjectId: pathToProject === undefined ? telemetryUndefined : getStringHash(pathToProject), + codeTransformProjectId: pathToProject === undefined ? undefined : getStringHash(pathToProject), source: transformByQState.getSourceDB(), target: transformByQState.getTargetDB(), userChoice: 'Confirm-SQL', @@ -563,7 +543,7 @@ export class GumbyController { canSelectMany: false, openLabel: 'Select', filters: { - 'YAML file': ['yaml'], // restrict user to only pick a .yaml file + File: ['yaml', 'yml'], // restrict user to only pick a .yaml file }, }) if (!fileUri || fileUri.length === 0) { @@ -576,7 +556,7 @@ export class GumbyController { this.messenger.sendUnrecoverableErrorResponse('invalid-custom-versions-file', message.tabID) return } - this.messenger.sendMessage('Received custom dependency version YAML file.', message.tabID, 'ai-prompt') + this.messenger.sendMessage(CodeWhispererConstants.receivedValidConfigFileMessage, message.tabID, 'ai-prompt') transformByQState.setCustomDependencyVersionFilePath(fileUri[0].fsPath) this.promptJavaHome('source', message.tabID) } @@ -660,17 +640,13 @@ export class GumbyController { const pathToJavaHome = extractPath(data.message) if (pathToJavaHome) { transformByQState.setSourceJavaHome(pathToJavaHome) - // TO-DO: delete line below and uncomment the block below when releasing CSB - await this.prepareLanguageUpgradeProject(data.tabID) // if source and target JDK versions are the same, just re-use the source JAVA_HOME and start the build - /* if (transformByQState.getTargetJDKVersion() === transformByQState.getSourceJDKVersion()) { transformByQState.setTargetJavaHome(pathToJavaHome) await this.prepareLanguageUpgradeProject(data.tabID) } else { this.promptJavaHome('target', data.tabID) } - */ } else { this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID) } diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 5265cb5b888..0880e2556d9 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -410,7 +410,7 @@ export class Messenger { message = CodeWhispererConstants.noPomXmlFoundChatMessage break case 'could-not-compile-project': - message = CodeWhispererConstants.cleanInstallErrorChatMessage + message = CodeWhispererConstants.cleanTestCompileErrorChatMessage break case 'invalid-java-home': message = CodeWhispererConstants.noJavaHomeFoundChatMessage @@ -704,7 +704,7 @@ ${codeSnippet} } public async sendCustomDependencyVersionMessage(tabID: string) { - const message = CodeWhispererConstants.chooseYamlMessage + const message = CodeWhispererConstants.chooseConfigFileMessage const buttons: ChatItemButton[] = [] buttons.push({ @@ -731,7 +731,7 @@ ${codeSnippet} tabID ) ) - const sampleYAML = `name: "custom-dependency-management" + const sampleYAML = `name: "dependency-upgrade" description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21" dependencyManagement: @@ -744,7 +744,7 @@ dependencyManagement: targetVersion: "3.0.0" originType: "THIRD_PARTY" plugins: - - identifier: "com.example.plugin" + - identifier: "com.example:plugin" targetVersion: "1.2.0" versionProperty: "plugin.version" # Optional` diff --git a/packages/core/src/amazonqGumby/errors.ts b/packages/core/src/amazonqGumby/errors.ts index d6805159569..c77bbcfc4bd 100644 --- a/packages/core/src/amazonqGumby/errors.ts +++ b/packages/core/src/amazonqGumby/errors.ts @@ -30,12 +30,6 @@ export class NoMavenJavaProjectsFoundError extends ToolkitError { } } -export class ZipExceedsSizeLimitError extends ToolkitError { - constructor() { - super('Zip file exceeds size limit', { code: 'ZipFileExceedsSizeLimit' }) - } -} - export class AlternateDependencyVersionsNotFoundError extends Error { constructor() { super('No available versions for dependency update') diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 35f699b24c2..051254d1873 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -262,7 +262,7 @@ export class DefaultCodeWhispererClient { /** * @description Use this function to get the status of the code transformation. We should * be polling this function periodically to get updated results. When this function - * returns COMPLETED we know the transformation is done. + * returns PARTIALLY_COMPLETED or COMPLETED we know the transformation is done. */ public async codeModernizerGetCodeTransformation( request: CodeWhispererUserClient.GetTransformationRequest @@ -272,15 +272,15 @@ export class DefaultCodeWhispererClient { } /** - * @description After the job has been PAUSED we need to get user intervention. Once that user - * intervention has been handled we can resume the transformation job. + * @description During client-side build, or after the job has been PAUSED we need to get user intervention. + * Once that user action has been handled we can resume the transformation job. * @params transformationJobId - String id returned from StartCodeTransformationResponse * @params userActionStatus - String to determine what action the user took, if any. */ public async codeModernizerResumeTransformation( request: CodeWhispererUserClient.ResumeTransformationRequest ): Promise> { - return (await this.createUserSdkClient()).resumeTransformation(request).promise() + return (await this.createUserSdkClient(8)).resumeTransformation(request).promise() } /** diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index 74e50f0890e..56e54a97a8a 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode' import * as fs from 'fs' // eslint-disable-line no-restricted-imports +import os from 'os' import path from 'path' import { getLogger } from '../../shared/logger/logger' import * as CodeWhispererConstants from '../models/constants' @@ -16,7 +17,6 @@ import { jobPlanProgress, FolderInfo, ZipManifest, - TransformByQStatus, TransformationType, TransformationCandidateProject, RegionProfile, @@ -43,7 +43,6 @@ import { validateOpenProjects, } from '../service/transformByQ/transformProjectValidationHandler' import { - getVersionData, prepareProjectDependencies, runMavenDependencyUpdateCommands, } from '../service/transformByQ/transformMavenHandler' @@ -82,7 +81,7 @@ import { AuthUtil } from '../util/authUtil' export function getFeedbackCommentData() { const jobId = transformByQState.getJobId() - const s = `Q CodeTransform jobId: ${jobId ? jobId : 'none'}` + const s = `Q CodeTransformation jobId: ${jobId ? jobId : 'none'}` return s } @@ -110,10 +109,10 @@ export async function processSQLConversionTransformFormInput(pathToProject: stri export async function compileProject() { try { - const dependenciesFolder: FolderInfo = getDependenciesFolderInfo() + const dependenciesFolder: FolderInfo = await getDependenciesFolderInfo() transformByQState.setDependencyFolderInfo(dependenciesFolder) - const modulePath = transformByQState.getProjectPath() - await prepareProjectDependencies(dependenciesFolder, modulePath) + const projectPath = transformByQState.getProjectPath() + await prepareProjectDependencies(dependenciesFolder.path, projectPath) } catch (err) { // open build-logs.txt file to show user error logs await writeAndShowBuildLogs(true) @@ -175,8 +174,7 @@ export async function humanInTheLoopRetryLogic(jobId: string, profile: RegionPro if (status === 'PAUSED') { const hilStatusFailure = await initiateHumanInTheLoopPrompt(jobId) if (hilStatusFailure) { - // We rejected the changes and resumed the job and should - // try to resume normal polling asynchronously + // resume polling void humanInTheLoopRetryLogic(jobId, profile) } } else { @@ -184,9 +182,7 @@ export async function humanInTheLoopRetryLogic(jobId: string, profile: RegionPro } } catch (error) { status = 'FAILED' - // TODO if we encounter error in HIL, do we stop job? await finalizeTransformByQ(status) - // bubble up error to callee function throw error } } @@ -225,11 +221,9 @@ export async function preTransformationUploadCode() { const payloadFilePath = zipCodeResult.tempFilePath const zipSize = zipCodeResult.fileSize - const dependenciesCopied = zipCodeResult.dependenciesCopied telemetry.record({ codeTransformTotalByteSize: zipSize, - codeTransformDependenciesCopied: dependenciesCopied, }) transformByQState.setPayloadFilePath(payloadFilePath) @@ -408,7 +402,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { // 7) We need to take that output of maven and use CreateUploadUrl const uploadFolderInfo = humanInTheLoopManager.getUploadFolderInfo() - await prepareProjectDependencies(uploadFolderInfo, uploadFolderInfo.path) + await prepareProjectDependencies(uploadFolderInfo.path, uploadFolderInfo.path) // zipCode side effects deletes the uploadFolderInfo right away const uploadResult = await zipCode({ dependenciesFolder: uploadFolderInfo, @@ -449,13 +443,11 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { await terminateHILEarly(jobId) void humanInTheLoopRetryLogic(jobId, profile) } finally { - // Always delete the dependency directories telemetry.codeTransform_humanInTheLoop.emit({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), codeTransformJobId: jobId, codeTransformMetadata: CodeTransformTelemetryState.instance.getCodeTransformMetaDataString(), result: hilResult, - // TODO: make a generic reason field for telemetry logging so we don't log sensitive PII data reason: hilResult === MetadataResult.Fail ? 'Runtime error occurred' : undefined, }) await HumanInTheLoopManager.instance.cleanUpArtifacts() @@ -504,7 +496,7 @@ export async function startTransformationJob( throw new JobStartError() } - await sleep(2000) // sleep before polling job to prevent ThrottlingException + await sleep(5000) // sleep before polling job status to prevent ThrottlingException throwIfCancelled() return jobId @@ -523,9 +515,7 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string, prof transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage) } - // Since we don't yet have a good way of knowing what the error was, - // we try to fetch any build failure artifacts that may exist so that we can optionally - // show them to the user if they exist. + // try to download pre-build error logs if available let pathToLog = '' try { const tempToolkitFolder = await makeTemporaryToolkitFolder() @@ -651,6 +641,7 @@ export async function setTransformationToRunningState() { transformByQState.resetSessionJobHistory() transformByQState.setJobId('') // so that details for last job are not overwritten when running one job after another transformByQState.setPolledJobStatus('') // so that previous job's status does not display at very beginning of this job + transformByQState.setHasSeenTransforming(false) CodeTransformTelemetryState.instance.setStartTime() transformByQState.setStartTime( @@ -693,23 +684,17 @@ export async function postTransformationJob() { const durationInMs = calculateTotalLatency(CodeTransformTelemetryState.instance.getStartTime()) const resultStatusMessage = transformByQState.getStatus() - if (transformByQState.getTransformationType() !== TransformationType.SQL_CONVERSION) { - // the below is only applicable when user is doing a Java 8/11 language upgrade - const versionInfo = await getVersionData() - const mavenVersionInfoMessage = `${versionInfo[0]} (${transformByQState.getMavenName()})` - const javaVersionInfoMessage = `${versionInfo[1]} (${transformByQState.getMavenName()})` - - telemetry.codeTransform_totalRunTime.emit({ - buildSystemVersion: mavenVersionInfoMessage, - codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), - codeTransformJobId: transformByQState.getJobId(), - codeTransformResultStatusMessage: resultStatusMessage, - codeTransformRunTimeLatency: durationInMs, - codeTransformLocalJavaVersion: javaVersionInfoMessage, - result: resultStatusMessage === TransformByQStatus.Succeeded ? MetadataResult.Pass : MetadataResult.Fail, - reason: `${resultStatusMessage}-${chatMessage}`, - }) - } + telemetry.codeTransform_totalRunTime.emit({ + codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), + codeTransformJobId: transformByQState.getJobId(), + codeTransformResultStatusMessage: resultStatusMessage, + codeTransformRunTimeLatency: durationInMs, + reason: transformByQState.getPolledJobStatus(), + result: + transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded() + ? MetadataResult.Pass + : MetadataResult.Fail, + }) let notificationMessage = '' @@ -739,9 +724,14 @@ export async function postTransformationJob() { }) } - if (transformByQState.getPayloadFilePath() !== '') { + if (transformByQState.getPayloadFilePath()) { // delete original upload ZIP at very end of transformation - fs.rmSync(transformByQState.getPayloadFilePath(), { recursive: true, force: true }) + fs.rmSync(transformByQState.getPayloadFilePath(), { force: true }) + } + // delete temporary build logs file + const logFilePath = path.join(os.tmpdir(), 'build-logs.txt') + if (fs.existsSync(logFilePath)) { + fs.rmSync(logFilePath, { force: true }) } // attempt download for user @@ -754,17 +744,15 @@ export async function postTransformationJob() { export async function transformationJobErrorHandler(error: any) { if (!transformByQState.isCancelled()) { // means some other error occurred; cancellation already handled by now with stopTransformByQ + await stopJob(transformByQState.getJobId()) transformByQState.setToFailed() transformByQState.setPolledJobStatus('FAILED') // jobFailureErrorNotification should always be defined here - let displayedErrorMessage = + const displayedErrorMessage = transformByQState.getJobFailureErrorNotification() ?? CodeWhispererConstants.failedToCompleteJobNotification - if (transformByQState.getJobFailureMetadata() !== '') { - displayedErrorMessage += ` ${transformByQState.getJobFailureMetadata()}` - transformByQState.setJobFailureErrorChatMessage( - `${transformByQState.getJobFailureErrorChatMessage()} ${transformByQState.getJobFailureMetadata()}` - ) - } + transformByQState.setJobFailureErrorChatMessage( + transformByQState.getJobFailureErrorChatMessage() ?? CodeWhispererConstants.failedToCompleteJobChatMessage + ) void vscode.window .showErrorMessage(displayedErrorMessage, CodeWhispererConstants.amazonQFeedbackText) .then((choice) => { diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 319127cba20..5691d154625 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -580,7 +580,7 @@ export const invalidMetadataFileUnsupportedTargetDB = 'I can only convert SQL for migrations to Aurora PostgreSQL or Amazon RDS for PostgreSQL target databases. The provided .sct file indicates another target database for this migration.' export const invalidCustomVersionsFileMessage = - 'Your .YAML file is not formatted correctly. Make sure that the .YAML file you upload follows the format of the sample file provided.' + "I wasn't able to parse the dependency upgrade file. Check that it's configured properly and try again. For an example of the required dependency upgrade file format, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file)." export const invalidMetadataFileErrorParsing = "It looks like the .sct file you provided isn't valid. Make sure that you've uploaded the .zip file you retrieved from your schema conversion in AWS DMS." @@ -640,10 +640,14 @@ export const jobCancelledNotification = 'You cancelled the transformation.' export const continueWithoutHilMessage = 'I will continue transforming your code without upgrading this dependency.' -export const continueWithoutYamlMessage = 'Ok, I will continue without this information.' +export const continueWithoutConfigFileMessage = + 'Ok, I will continue the transformation without additional dependency upgrade information.' -export const chooseYamlMessage = - 'You can optionally upload a YAML file to specify which dependency versions to upgrade to.' +export const receivedValidConfigFileMessage = + 'The dependency upgrade file looks good. I will use this information to upgrade the dependencies you specified.' + +export const chooseConfigFileMessage = + 'Would you like to provide a custom dependency upgrade file? You can specify first-party dependencies to upgrade in a YAML file, and I will upgrade them during the JDK upgrade (for example, Java 8 to 17). You can initiate a separate transformation (17 to 17 or 21 to 21) after the initial JDK upgrade to transform third-party dependencies.\n\nWithout a YAML file, I can perform a minimum JDK upgrade, and then you can initiate a separate transformation to upgrade all third-party dependencies as part of a maximum transformation. For an example dependency upgrade file, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).' export const enterJavaHomePlaceholder = 'Enter the path to your Java installation' @@ -656,7 +660,7 @@ export const jobCompletedNotification = 'Amazon Q transformed your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes. ' export const upgradeLibrariesMessage = - 'After successfully building in Java 17 or 21, an additional transformation is required to upgrade your libraries and dependencies. Choose the same source code version and target code version (for example, 17 to 17) to do this.' + 'After successfully transforming to Java 17 or 21, an additional transformation is required to upgrade your libraries and dependencies. Choose the same source code version and target code version (for example, 17 to 17) to do this.' export const jobPartiallyCompletedChatMessage = `I transformed part of your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation. ` @@ -720,14 +724,14 @@ export const linkToBillingInfo = 'https://aws.amazon.com/q/developer/pricing/' export const dependencyFolderName = 'transformation_dependencies_temp_' -export const cleanInstallErrorChatMessage = `Sorry, I couldn\'t run the Maven clean install command to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` +export const cleanTestCompileErrorChatMessage = `I could not run \`mvn clean test-compile\` to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` -export const cleanInstallErrorNotification = `Amazon Q could not run the Maven clean install command to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` +export const cleanTestCompileErrorNotification = `Amazon Q could not run \`mvn clean test-compile\` to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` export const enterJavaHomeChatMessage = 'Enter the path to JDK' export const projectPromptChatMessage = - 'I can upgrade your Java project. To start the transformation, I need some information from you. Choose the project you want to upgrade and the target code version to upgrade to. Then, choose Confirm.\n\nAfter successfully building in Java 17 or 21, an additional transformation is required to upgrade your libraries and dependencies. Choose the same source code version and target code version (for example, 17 to 17) to do this.' + "I can upgrade your Java project. To start the transformation, I need some information from you. Choose the project you want to upgrade and the target code version to upgrade to. Then, choose Confirm.\n\nAfter successfully transforming to Java 17 or 21, an additional transformation is required to upgrade your libraries and dependencies. Choose the same source code version and target code version (for example, 17 to 17) to do this.\n\nI will perform the transformation based on your project's requests, descriptions, and content. To maintain security, avoid including external, unvetted artifacts in your project repository prior to starting the transformation and always validate transformed code for both functionality and security." export const windowsJavaHomeHelpChatMessage = 'To find the JDK path, run the following commands in a new terminal: `cd "C:/Program Files/Java"` and then `dir`. If you see your JDK version, run `cd ` and then `cd` to show the path.' @@ -738,10 +742,6 @@ export const macJavaVersionHomeHelpChatMessage = (version: number) => export const linuxJavaHomeHelpChatMessage = 'To find the JDK path, run the following command in a new terminal: `update-java-alternatives --list`' -export const projectSizeTooLargeChatMessage = `Sorry, your project size exceeds the Amazon Q Code Transformation upload limit of 2GB. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootProjectSize}).` - -export const projectSizeTooLargeNotification = `Your project size exceeds the Amazon Q Code Transformation upload limit of 2GB. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootProjectSize}).` - export const JDK8VersionNumber = '52' export const JDK11VersionNumber = '55' @@ -759,7 +759,7 @@ export const chooseProjectSchemaFormMessage = 'To continue, choose the project a export const skipUnitTestsFormTitle = 'Choose to skip unit tests' export const skipUnitTestsFormMessage = - 'I will build your project using `mvn clean test` by default. If you would like me to build your project without running unit tests, I will use `mvn clean test-compile`.' + 'I will build generated code in your local environment, not on the server side. For information on how I scan code to reduce security risks associated with building the code in your local environment, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#java-local-builds).\n\nI will build your project using `mvn clean test` by default. If you would like me to build your project without running unit tests, I will use `mvn clean test-compile`.' export const runUnitTestsMessage = 'Run unit tests' diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 279469353fb..72483feec51 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -675,16 +675,15 @@ export enum BuildSystem { Unknown = 'Unknown', } -// TO-DO: include the custom YAML file path here somewhere? export class ZipManifest { sourcesRoot: string = 'sources/' dependenciesRoot: string = 'dependencies/' - buildLogs: string = 'build-logs.txt' version: string = '1.0' hilCapabilities: string[] = ['HIL_1pDependency_VersionUpgrade'] - // TO-DO: add 'CLIENT_SIDE_BUILD' here when releasing - transformCapabilities: string[] = ['EXPLAINABILITY_V1', 'SELECTIVE_TRANSFORMATION_V2'] + transformCapabilities: string[] = ['EXPLAINABILITY_V1', 'SELECTIVE_TRANSFORMATION_V2', 'CLIENT_SIDE_BUILD'] noInteractiveMode: boolean = true + dependencyUpgradeConfigFile?: string = undefined + compilationsJsonFile: string = 'compilations.json' customBuildCommand: string = 'clean test' requestedConversions?: { sqlConversion?: { @@ -782,7 +781,7 @@ export class TransformByQState { private polledJobStatus: string = '' - private jobFailureMetadata: string = '' + private hasSeenTransforming: boolean = false private payloadFilePath: string = '' @@ -831,6 +830,10 @@ export class TransformByQState { return this.transformByQState === TransformByQStatus.PartiallySucceeded } + public getHasSeenTransforming() { + return this.hasSeenTransforming + } + public getTransformationType() { return this.transformationType } @@ -923,10 +926,6 @@ export class TransformByQState { return this.projectCopyFilePath } - public getJobFailureMetadata() { - return this.jobFailureMetadata - } - public getPayloadFilePath() { return this.payloadFilePath } @@ -1007,6 +1006,10 @@ export class TransformByQState { this.transformByQState = TransformByQStatus.PartiallySucceeded } + public setHasSeenTransforming(hasSeen: boolean) { + this.hasSeenTransforming = hasSeen + } + public setTransformationType(type: TransformationType) { this.transformationType = type } @@ -1091,10 +1094,6 @@ export class TransformByQState { this.projectCopyFilePath = filePath } - public setJobFailureMetadata(data: string) { - this.jobFailureMetadata = data - } - public setPayloadFilePath(payloadFilePath: string) { this.payloadFilePath = payloadFilePath } @@ -1153,9 +1152,9 @@ export class TransformByQState { public setJobDefaults() { this.setToNotStarted() + this.hasSeenTransforming = false this.jobFailureErrorNotification = undefined this.jobFailureErrorChatMessage = undefined - this.jobFailureMetadata = '' this.payloadFilePath = '' this.metadataPathSQL = '' this.customVersionPath = '' diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index e284207540d..20ef306f7ab 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -40,13 +40,12 @@ import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/cod import { calculateTotalLatency } from '../../../amazonqGumby/telemetry/codeTransformTelemetry' import { MetadataResult } from '../../../shared/telemetry/telemetryClient' import request from '../../../shared/request' -import { JobStoppedError, ZipExceedsSizeLimitError } from '../../../amazonqGumby/errors' +import { JobStoppedError } from '../../../amazonqGumby/errors' import { createLocalBuildUploadZip, extractOriginalProjectSources, writeAndShowBuildLogs } from './transformFileHandler' import { createCodeWhispererChatStreamingClient } from '../../../shared/clients/codewhispererChatClient' import { downloadExportResultArchive } from '../../../shared/utilities/download' import { ExportContext, ExportIntent, TransformationDownloadArtifactType } from '@amzn/codewhisperer-streaming' import fs from '../../../shared/fs/fs' -import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' import { encodeHTML } from '../../../shared/utilities/textUtilities' import { convertToTimeString } from '../../../shared/datetime' import { getAuthType } from '../../../auth/utils' @@ -55,7 +54,6 @@ import { setContext } from '../../../shared/vscode/setContext' import { AuthUtil } from '../../util/authUtil' import { DiffModel } from './transformationResultsViewProvider' import { spawnSync } from 'child_process' // eslint-disable-line no-restricted-imports -import { isClientSideBuildEnabled } from '../../../dev/config' export function getSha256(buffer: Buffer) { const hasher = crypto.createHash('sha256') @@ -187,12 +185,13 @@ export async function stopJob(jobId: string) { return } + getLogger().info(`CodeTransformation: Stopping transformation job with ID: ${jobId}`) + try { await codeWhisperer.codeWhispererClient.codeModernizerStopCodeTransformation({ transformationJobId: jobId, }) } catch (e: any) { - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: StopTransformation error = %O`, e) throw new Error('Stop job failed') } @@ -218,7 +217,6 @@ export async function uploadPayload( }) } catch (e: any) { const errorMessage = `Creating the upload URL failed due to: ${(e as Error).message}` - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: CreateUploadUrl error: = %O`, e) throw new Error(errorMessage) } @@ -309,24 +307,20 @@ export function createZipManifest({ hilZipParams }: IZipManifestParams) { interface IZipCodeParams { dependenciesFolder?: FolderInfo - humanInTheLoopFlag?: boolean projectPath?: string zipManifest: ZipManifest | HilZipManifest } interface ZipCodeResult { - dependenciesCopied: boolean tempFilePath: string fileSize: number } export async function zipCode( - { dependenciesFolder, humanInTheLoopFlag, projectPath, zipManifest }: IZipCodeParams, + { dependenciesFolder, projectPath, zipManifest }: IZipCodeParams, zip: AdmZip = new AdmZip() ) { let tempFilePath = undefined - let logFilePath = undefined - let dependenciesCopied = false try { throwIfCancelled() @@ -384,65 +378,48 @@ export async function zipCode( continue } const relativePath = path.relative(dependenciesFolder.path, file) - // const paddedPath = path.join(`dependencies/${dependenciesFolder.name}`, relativePath) - const paddedPath = path.join(`dependencies/`, relativePath) - zip.addLocalFile(file, path.dirname(paddedPath)) + if (relativePath.includes('compilations.json')) { + let fileContents = await nodefs.promises.readFile(file, 'utf-8') + if (os.platform() === 'win32') { + fileContents = fileContents.replace(/\\\\/g, '/') + } + zip.addFile('compilations.json', Buffer.from(fileContents, 'utf-8')) + } else { + zip.addLocalFile(file, path.dirname(relativePath)) + } dependencyFilesSize += (await nodefs.promises.stat(file)).size } getLogger().info(`CodeTransformation: dependency files size = ${dependencyFilesSize}`) - dependenciesCopied = true } - // TO-DO: decide where exactly to put the YAML file / what to name it if (transformByQState.getCustomDependencyVersionFilePath() && zipManifest instanceof ZipManifest) { zip.addLocalFile( transformByQState.getCustomDependencyVersionFilePath(), - 'custom-upgrades', - 'dependency-versions.yaml' + 'sources', + 'dependency_upgrade.yml' ) + zipManifest.dependencyUpgradeConfigFile = 'dependency_upgrade.yml' } zip.addFile('manifest.json', Buffer.from(JSON.stringify(zipManifest)), 'utf-8') throwIfCancelled() - // add text file with logs from mvn clean install and mvn copy-dependencies - logFilePath = await writeAndShowBuildLogs() - // We don't add build-logs.txt file to the manifest if we are - // uploading HIL artifacts - if (!humanInTheLoopFlag) { - zip.addLocalFile(logFilePath) - } - tempFilePath = path.join(os.tmpdir(), 'zipped-code.zip') await fs.writeFile(tempFilePath, zip.toBuffer()) - if (dependenciesFolder && (await fs.exists(dependenciesFolder.path))) { + if (dependenciesFolder?.path) { await fs.delete(dependenciesFolder.path, { recursive: true, force: true }) } } catch (e: any) { getLogger().error(`CodeTransformation: zipCode error = ${e}`) throw Error('Failed to zip project') - } finally { - if (logFilePath) { - await fs.delete(logFilePath) - } } - const zipSize = (await nodefs.promises.stat(tempFilePath)).size + const fileSize = (await nodefs.promises.stat(tempFilePath)).size - const exceedsLimit = zipSize > CodeWhispererConstants.uploadZipSizeLimitInBytes + getLogger().info(`CodeTransformation: created ZIP of size ${fileSize} at ${tempFilePath}`) - getLogger().info(`CodeTransformation: created ZIP of size ${zipSize} at ${tempFilePath}`) - - if (exceedsLimit) { - void vscode.window.showErrorMessage(CodeWhispererConstants.projectSizeTooLargeNotification) - transformByQState.getChatControllers()?.transformationFinished.fire({ - message: CodeWhispererConstants.projectSizeTooLargeChatMessage, - tabID: ChatSessionManager.Instance.getSession().tabID, - }) - throw new ZipExceedsSizeLimitError() - } - return { dependenciesCopied: dependenciesCopied, tempFilePath: tempFilePath, fileSize: zipSize } as ZipCodeResult + return { tempFilePath: tempFilePath, fileSize: fileSize } as ZipCodeResult } export async function startJob(uploadId: string, profile: RegionProfile | undefined) { @@ -465,7 +442,6 @@ export async function startJob(uploadId: string, profile: RegionProfile | undefi return response.transformationJobId } catch (e: any) { const errorMessage = `Starting the job failed due to: ${(e as Error).message}` - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: StartTransformation error = %O`, e) throw new Error(errorMessage) } @@ -652,12 +628,9 @@ export async function getTransformationPlan(jobId: string, profile: RegionProfil return plan } catch (e: any) { const errorMessage = (e as Error).message - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: GetTransformationPlan error = %O`, e) - /* Means API call failed - * If response is defined, means a display/parsing error occurred, so continue transformation - */ + // GetTransformationPlan API call failed, but if response is defined, a display/parsing error occurred, so continue transformation if (response === undefined) { throw new Error(errorMessage) } @@ -672,7 +645,6 @@ export async function getTransformationSteps(jobId: string, profile: RegionProfi }) return response.transformationPlan.transformationSteps.slice(1) // skip step 0 (contains supplemental info) } catch (e: any) { - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: GetTransformationPlan error = %O`, e) throw e } @@ -692,6 +664,9 @@ export async function pollTransformationJob(jobId: string, validStates: string[] if (CodeWhispererConstants.validStatesForBuildSucceeded.includes(status)) { jobPlanProgress['buildCode'] = StepProgress.Succeeded } + if (status === 'TRANSFORMING') { + transformByQState.setHasSeenTransforming(true) + } // emit metric when job status changes if (status !== transformByQState.getPolledJobStatus()) { telemetry.codeTransform_jobStatusChanged.emit({ @@ -728,16 +703,23 @@ export async function pollTransformationJob(jobId: string, validStates: string[] // final plan is complete; show to user isPlanComplete = true } + // for JDK upgrades without a YAML file, we show a static plan so no need to keep refreshing it + if ( + plan && + transformByQState.getSourceJDKVersion() !== transformByQState.getTargetJDKVersion() && + !transformByQState.getCustomDependencyVersionFilePath() + ) { + isPlanComplete = true + } } if (validStates.includes(status)) { break } - // TO-DO: remove isClientSideBuildEnabled when releasing CSB + // TO-DO: later, handle case where PlannerAgent needs to run mvn dependency:tree during PLANNING stage; not needed for now if ( - isClientSideBuildEnabled && - status === 'TRANSFORMING' && + transformByQState.getHasSeenTransforming() && transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE ) { // client-side build is N/A for SQL conversions @@ -762,7 +744,6 @@ export async function pollTransformationJob(jobId: string, validStates: string[] await sleep(CodeWhispererConstants.transformationJobPollingIntervalSeconds * 1000) } catch (e: any) { getLogger().error(`CodeTransformation: GetTransformation error = %O`, e) - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) throw e } } @@ -844,17 +825,24 @@ async function processClientInstructions(jobId: string, clientInstructionsPath: const destinationPath = path.join(os.tmpdir(), `originalCopy_${jobId}_${artifactId}`) await extractOriginalProjectSources(destinationPath) getLogger().info(`CodeTransformation: copied project to ${destinationPath}`) - const diffModel = new DiffModel() - diffModel.parseDiff(clientInstructionsPath, path.join(destinationPath, 'sources'), true) - // show user the diff.patch - const doc = await vscode.workspace.openTextDocument(clientInstructionsPath) - await vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.One }) + const diffContents = await fs.readFileText(clientInstructionsPath) + if (diffContents.trim()) { + const diffModel = new DiffModel() + diffModel.parseDiff(clientInstructionsPath, path.join(destinationPath, 'sources'), true) + // show user the diff.patch + const doc = await vscode.workspace.openTextDocument(clientInstructionsPath) + await vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.One }) + } else { + // still need to set the project copy so that we can use it below + transformByQState.setProjectCopyFilePath(path.join(destinationPath, 'sources')) + getLogger().info(`CodeTransformation: diff.patch is empty`) + } await runClientSideBuild(transformByQState.getProjectCopyFilePath(), artifactId) } -export async function runClientSideBuild(projectCopyPath: string, clientInstructionArtifactId: string) { +export async function runClientSideBuild(projectCopyDir: string, clientInstructionArtifactId: string) { const baseCommand = transformByQState.getMavenName() - const args = [] + const args = ['clean'] if (transformByQState.getCustomBuildCommand() === CodeWhispererConstants.skipUnitTestsBuildCommand) { args.push('test-compile') } else { @@ -864,22 +852,22 @@ export async function runClientSideBuild(projectCopyPath: string, clientInstruct const argString = args.join(' ') const spawnResult = spawnSync(baseCommand, args, { - cwd: projectCopyPath, + cwd: projectCopyDir, shell: true, encoding: 'utf-8', env: environment, }) - const buildLogs = `Intermediate build result from running ${baseCommand} ${argString}:\n\n${spawnResult.stdout}` + const buildLogs = `Intermediate build result from running mvn ${argString}:\n\n${spawnResult.stdout}` transformByQState.clearBuildLog() transformByQState.appendToBuildLog(buildLogs) await writeAndShowBuildLogs() - const uploadZipBaseDir = path.join( + const uploadZipDir = path.join( os.tmpdir(), `clientInstructionsResult_${transformByQState.getJobId()}_${clientInstructionArtifactId}` ) - const uploadZipPath = await createLocalBuildUploadZip(uploadZipBaseDir, spawnResult.status, spawnResult.stdout) + const uploadZipPath = await createLocalBuildUploadZip(uploadZipDir, spawnResult.status, spawnResult.stdout) // upload build results const uploadContext: UploadContext = { @@ -892,10 +880,33 @@ export async function runClientSideBuild(projectCopyPath: string, clientInstruct try { await uploadPayload(uploadZipPath, AuthUtil.instance.regionProfileManager.activeRegionProfile, uploadContext) await resumeTransformationJob(transformByQState.getJobId(), 'COMPLETED') + } catch (err: any) { + getLogger().error(`CodeTransformation: upload client build results / resumeTransformation error = %O`, err) + transformByQState.setJobFailureErrorChatMessage( + `${CodeWhispererConstants.failedToCompleteJobGenericChatMessage} ${err.message}` + ) + transformByQState.setJobFailureErrorNotification( + `${CodeWhispererConstants.failedToCompleteJobGenericNotification} ${err.message}` + ) + // in case server-side execution times out, still call resumeTransformationJob + if (err.message.includes('find a step in desired state:AWAITING_CLIENT_ACTION')) { + getLogger().info('CodeTransformation: resuming job after server-side execution timeout') + await resumeTransformationJob(transformByQState.getJobId(), 'COMPLETED') + } else { + throw err + } } finally { - await fs.delete(projectCopyPath, { recursive: true }) - await fs.delete(uploadZipBaseDir, { recursive: true }) - getLogger().info(`CodeTransformation: Just deleted project copy and uploadZipBaseDir after client-side build`) + await fs.delete(projectCopyDir, { recursive: true }) + await fs.delete(uploadZipDir, { recursive: true }) + await fs.delete(uploadZipPath, { force: true }) + const exportZipDir = path.join( + os.tmpdir(), + `downloadClientInstructions_${transformByQState.getJobId()}_${clientInstructionArtifactId}` + ) + await fs.delete(exportZipDir, { recursive: true }) + getLogger().info( + `CodeTransformation: deleted projectCopy, clientInstructionsResult, and downloadClientInstructions directories/files` + ) } } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts index fd74ca7b147..88f34a799d1 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts @@ -9,7 +9,7 @@ import * as os from 'os' import xml2js = require('xml2js') import * as CodeWhispererConstants from '../../models/constants' import { existsSync, readFileSync, writeFileSync } from 'fs' // eslint-disable-line no-restricted-imports -import { BuildSystem, DB, FolderInfo, TransformationType, transformByQState } from '../../models/model' +import { BuildSystem, DB, FolderInfo, transformByQState } from '../../models/model' import { IManifestFile } from '../../../amazonqFeatureDev/models' import fs from '../../../shared/fs/fs' import globals from '../../../shared/extensionGlobals' @@ -18,9 +18,10 @@ import { AbsolutePathDetectedError } from '../../../amazonqGumby/errors' import { getLogger } from '../../../shared/logger/logger' import AdmZip from 'adm-zip' -export function getDependenciesFolderInfo(): FolderInfo { +export async function getDependenciesFolderInfo(): Promise { const dependencyFolderName = `${CodeWhispererConstants.dependencyFolderName}${globals.clock.Date.now()}` const dependencyFolderPath = path.join(os.tmpdir(), dependencyFolderName) + await fs.mkdir(dependencyFolderPath) return { name: dependencyFolderName, path: dependencyFolderPath, @@ -31,15 +32,12 @@ export async function writeAndShowBuildLogs(isLocalInstall: boolean = false) { const logFilePath = path.join(os.tmpdir(), 'build-logs.txt') writeFileSync(logFilePath, transformByQState.getBuildLog()) const doc = await vscode.workspace.openTextDocument(logFilePath) - if ( - !transformByQState.getBuildLog().includes('clean install succeeded') && - transformByQState.getTransformationType() !== TransformationType.SQL_CONVERSION - ) { + const logs = transformByQState.getBuildLog().toLowerCase() + if (logs.includes('intermediate build result') || logs.includes('maven jar failed')) { // only show the log if the build failed; show it in second column for intermediate builds only const options = isLocalInstall ? undefined : { viewColumn: vscode.ViewColumn.Two } await vscode.window.showTextDocument(doc, options) } - return logFilePath } export async function createLocalBuildUploadZip(baseDir: string, exitCode: number | null, stdout: string) { @@ -174,8 +172,7 @@ export async function validateSQLMetadataFile(fileContents: string, message: any } export function setMaven() { - // for now, just use regular Maven since the Maven executables can - // cause permissions issues when building if user has not ran 'chmod' + // avoid using maven wrapper since we can run into permissions issues transformByQState.setMavenName('mvn') } @@ -214,7 +211,6 @@ export async function getJsonValuesFromManifestFile( return { hilCapability: jsonValues?.hilType, pomFolderName: jsonValues?.pomFolderName, - // TODO remove this forced version sourcePomVersion: jsonValues?.sourcePomVersion || '1.0', pomArtifactId: jsonValues?.pomArtifactId, pomGroupId: jsonValues?.pomGroupId, diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts index ebcbfec8970..b38a6ef1da8 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts @@ -8,153 +8,72 @@ import { getLogger } from '../../../shared/logger/logger' import * as CodeWhispererConstants from '../../models/constants' // Consider using ChildProcess once we finalize all spawnSync calls import { spawnSync } from 'child_process' // eslint-disable-line no-restricted-imports -import { CodeTransformBuildCommand, telemetry } from '../../../shared/telemetry/telemetry' -import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' -import { ToolkitError } from '../../../shared/errors' import { setMaven } from './transformFileHandler' import { throwIfCancelled } from './transformApiHandler' import { sleep } from '../../../shared/utilities/timeoutUtils' +import path from 'path' +import globals from '../../../shared/extensionGlobals' -function installProjectDependencies(dependenciesFolder: FolderInfo, modulePath: string) { - telemetry.codeTransform_localBuildProject.run(() => { - telemetry.record({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId() }) +function collectDependenciesAndMetadata(dependenciesFolderPath: string, workingDirPath: string) { + getLogger().info('CodeTransformation: running mvn clean test-compile with maven JAR') - // will always be 'mvn' - const baseCommand = transformByQState.getMavenName() - - const args = [`-Dmaven.repo.local=${dependenciesFolder.path}`, 'clean', 'install', '-q'] - - transformByQState.appendToBuildLog(`Running ${baseCommand} ${args.join(' ')}`) - - if (transformByQState.getCustomBuildCommand() === CodeWhispererConstants.skipUnitTestsBuildCommand) { - args.push('-DskipTests') - } - - let environment = process.env - - if (transformByQState.getSourceJavaHome()) { - environment = { ...process.env, JAVA_HOME: transformByQState.getSourceJavaHome() } - } - - const argString = args.join(' ') - const spawnResult = spawnSync(baseCommand, args, { - cwd: modulePath, - shell: true, - encoding: 'utf-8', - env: environment, - maxBuffer: CodeWhispererConstants.maxBufferSize, - }) - - const mavenBuildCommand = transformByQState.getMavenName() - telemetry.record({ codeTransformBuildCommand: mavenBuildCommand as CodeTransformBuildCommand }) - - if (spawnResult.status !== 0) { - let errorLog = '' - errorLog += spawnResult.error ? JSON.stringify(spawnResult.error) : '' - errorLog += `${spawnResult.stderr}\n${spawnResult.stdout}` - transformByQState.appendToBuildLog(`${baseCommand} ${argString} failed: \n ${errorLog}`) - getLogger().error( - `CodeTransformation: Error in running Maven command ${baseCommand} ${argString} = ${errorLog}` - ) - throw new ToolkitError(`Maven ${argString} error`, { code: 'MavenExecutionError' }) - } else { - transformByQState.appendToBuildLog(`mvn clean install succeeded`) - } - }) -} - -function copyProjectDependencies(dependenciesFolder: FolderInfo, modulePath: string) { const baseCommand = transformByQState.getMavenName() + const jarPath = globals.context.asAbsolutePath(path.join('resources', 'amazonQCT', 'QCT-Maven-6-16.jar')) + + getLogger().info('CodeTransformation: running Maven extension with JAR') const args = [ - 'dependency:copy-dependencies', - `-DoutputDirectory=${dependenciesFolder.path}`, - '-Dmdep.useRepositoryLayout=true', - '-Dmdep.copyPom=true', - '-Dmdep.addParentPoms=true', - '-q', + `-Dmaven.ext.class.path=${jarPath}`, + `-Dcom.amazon.aws.developer.transform.jobDirectory=${dependenciesFolderPath}`, + 'clean', + 'test-compile', ] let environment = process.env - if (transformByQState.getSourceJavaHome()) { + if (transformByQState.getSourceJavaHome() !== undefined) { environment = { ...process.env, JAVA_HOME: transformByQState.getSourceJavaHome() } } const spawnResult = spawnSync(baseCommand, args, { - cwd: modulePath, + cwd: workingDirPath, shell: true, encoding: 'utf-8', env: environment, - maxBuffer: CodeWhispererConstants.maxBufferSize, }) + + getLogger().info( + `CodeTransformation: Ran mvn clean test-compile with maven JAR; status code = ${spawnResult.status}}` + ) + if (spawnResult.status !== 0) { let errorLog = '' errorLog += spawnResult.error ? JSON.stringify(spawnResult.error) : '' errorLog += `${spawnResult.stderr}\n${spawnResult.stdout}` - getLogger().info( - `CodeTransformation: Maven command ${baseCommand} ${args} failed, but still continuing with transformation: ${errorLog}` - ) - throw new Error('Maven copy-deps error') + errorLog = errorLog.toLowerCase().replace('elasticgumby', 'QCT') + transformByQState.appendToBuildLog(`mvn clean test-compile with maven JAR failed:\n${errorLog}`) + getLogger().error(`CodeTransformation: Error in running mvn clean test-compile with maven JAR = ${errorLog}`) + throw new Error('mvn clean test-compile with maven JAR failed') } + getLogger().info( + `CodeTransformation: mvn clean test-compile with maven JAR succeeded; dependencies copied to ${dependenciesFolderPath}` + ) } -export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, rootPomPath: string) { +export async function prepareProjectDependencies(dependenciesFolderPath: string, workingDirPath: string) { setMaven() - getLogger().info('CodeTransformation: running Maven copy-dependencies') // pause to give chat time to update await sleep(100) try { - copyProjectDependencies(dependenciesFolder, rootPomPath) - } catch (err) { - // continue in case of errors - getLogger().info( - `CodeTransformation: Maven copy-dependencies failed, but transformation will continue and may succeed` - ) - } - - getLogger().info('CodeTransformation: running Maven install') - try { - installProjectDependencies(dependenciesFolder, rootPomPath) + collectDependenciesAndMetadata(dependenciesFolderPath, workingDirPath) } catch (err) { - void vscode.window.showErrorMessage(CodeWhispererConstants.cleanInstallErrorNotification) + getLogger().error('CodeTransformation: collectDependenciesAndMetadata failed') + void vscode.window.showErrorMessage(CodeWhispererConstants.cleanTestCompileErrorNotification) throw err } - throwIfCancelled() void vscode.window.showInformationMessage(CodeWhispererConstants.buildSucceededNotification) } -export async function getVersionData() { - const baseCommand = transformByQState.getMavenName() - const projectPath = transformByQState.getProjectPath() - const args = ['-v'] - const spawnResult = spawnSync(baseCommand, args, { cwd: projectPath, shell: true, encoding: 'utf-8' }) - - let localMavenVersion: string | undefined = '' - let localJavaVersion: string | undefined = '' - - try { - const localMavenVersionIndex = spawnResult.stdout.indexOf('Apache Maven') - const localMavenVersionString = spawnResult.stdout.slice(localMavenVersionIndex + 13).trim() - localMavenVersion = localMavenVersionString.slice(0, localMavenVersionString.indexOf(' ')).trim() - } catch (e: any) { - localMavenVersion = undefined // if this happens here or below, user most likely has JAVA_HOME incorrectly defined - } - - try { - const localJavaVersionIndex = spawnResult.stdout.indexOf('Java version: ') - const localJavaVersionString = spawnResult.stdout.slice(localJavaVersionIndex + 14).trim() - localJavaVersion = localJavaVersionString.slice(0, localJavaVersionString.indexOf(',')).trim() // will match value of JAVA_HOME - } catch (e: any) { - localJavaVersion = undefined - } - - getLogger().info( - `CodeTransformation: Ran ${baseCommand} to get Maven version = ${localMavenVersion} and Java version = ${localJavaVersion} with project JDK = ${transformByQState.getSourceJDKVersion()}` - ) - return [localMavenVersion, localJavaVersion] -} - export function runMavenDependencyUpdateCommands(dependenciesFolder: FolderInfo) { const baseCommand = transformByQState.getMavenName() diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index e5de2099753..0b678f8120d 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -165,7 +165,9 @@ export class DiffModel { throw new Error(CodeWhispererConstants.noChangesMadeMessage) } - const changedFiles = parsePatch(diffContents) + let changedFiles = parsePatch(diffContents) + // exclude dependency_upgrade.yml from patch application + changedFiles = changedFiles.filter((file) => !file.oldFileName?.includes('dependency_upgrade')) getLogger().info('CodeTransformation: parsed patch file successfully') // if doing intermediate client-side build, pathToWorkspace is the path to the unzipped project's 'sources' directory (re-using upload ZIP) // otherwise, we are at the very end of the transformation and need to copy the changed files in the project to show the diff(s) diff --git a/packages/core/src/dev/config.ts b/packages/core/src/dev/config.ts index b4df78f64b0..d5fa49b2426 100644 --- a/packages/core/src/dev/config.ts +++ b/packages/core/src/dev/config.ts @@ -10,6 +10,3 @@ export const betaUrl = { amazonq: '', toolkit: '', } - -// TO-DO: remove when releasing CSB -export const isClientSideBuildEnabled = false diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index edb2524ee68..554d24c855a 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -53,18 +53,21 @@ import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports describe('transformByQ', function () { let fetchStub: sinon.SinonStub let tempDir: string - const validCustomVersionsFile = `name: "custom-dependency-management" + const validCustomVersionsFile = `name: "dependency-upgrade" description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21" dependencyManagement: dependencies: - identifier: "com.example:library1" - targetVersion: "2.1.0" - versionProperty: "library1.version" - originType: "FIRST_PARTY" + targetVersion: "2.1.0" + versionProperty: "library1.version" # Optional + originType: "FIRST_PARTY" # or "THIRD_PARTY" + - identifier: "com.example:library2" + targetVersion: "3.0.0" + originType: "THIRD_PARTY" plugins: - - identifier: "com.example.plugin" - targetVersion: "1.2.0" - versionProperty: "plugin.version"` + - identifier: "com.example:plugin" + targetVersion: "1.2.0" + versionProperty: "plugin.version" # Optional` const validSctFile = ` @@ -119,6 +122,7 @@ dependencyManagement: }) afterEach(async function () { + fetchStub.restore() sinon.restore() await fs.delete(tempDir, { recursive: true }) }) @@ -405,7 +409,6 @@ dependencyManagement: path: tempDir, name: tempFileName, }, - humanInTheLoopFlag: false, projectPath: tempDir, zipManifest: transformManifest, }).then((zipCodeResult) => { @@ -462,7 +465,7 @@ dependencyManagement: ] for (const folder of m2Folders) { - const folderPath = path.join(tempDir, folder) + const folderPath = path.join(tempDir, 'dependencies', folder) await fs.mkdir(folderPath) for (const file of filesToAdd) { await fs.writeFile(path.join(folderPath, file), 'sample content for the test file') @@ -476,7 +479,6 @@ dependencyManagement: path: tempDir, name: tempFileName, }, - humanInTheLoopFlag: false, projectPath: tempDir, zipManifest: new ZipManifest(), }).then((zipCodeResult) => { @@ -662,7 +664,6 @@ dependencyManagement: message: expectedMessage, } ) - sinon.assert.callCount(fetchStub, 4) }) it('should not retry upload on non-retriable error', async () => { diff --git a/packages/core/src/testInteg/perf/zipcode.test.ts b/packages/core/src/testInteg/perf/zipcode.test.ts index f5e81086152..71303e493c9 100644 --- a/packages/core/src/testInteg/perf/zipcode.test.ts +++ b/packages/core/src/testInteg/perf/zipcode.test.ts @@ -54,7 +54,6 @@ function performanceTestWrapper(numberOfFiles: number, fileSize: number) { path: setup.tempDir, name: setup.tempFileName, }, - humanInTheLoopFlag: false, projectPath: setup.tempDir, zipManifest: setup.transformQManifest, }) From 3e41c839dcc34970d37046ee5149d82d012d69fc Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Tue, 24 Jun 2025 14:55:43 -0700 Subject: [PATCH 06/20] changelog --- .../Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json diff --git a/packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json b/packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json new file mode 100644 index 00000000000..f38b0f1375f --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Added automatic system certificate detection and VSCode proxy settings support" +} From 696e143515d3981a82656a9fd1715d62ed57e93e Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Tue, 24 Jun 2025 15:08:24 -0700 Subject: [PATCH 07/20] missing await --- packages/amazonq/src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 65caea3b2c8..9ca13136eab 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -122,7 +122,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is } // Configure proxy settings early - ProxyUtil.configureProxyForLanguageServer() + await ProxyUtil.configureProxyForLanguageServer() // This contains every lsp agnostic things (auth, security scan, code scan) await activateCodeWhisperer(extContext as ExtContext) From f5cf3bde1d47dac4c18c405a872385c0a6530fef Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Tue, 24 Jun 2025 17:15:43 -0700 Subject: [PATCH 08/20] fix(amazonq): fix for amazon q app initialization failure on sagemaker --- .../Bug Fix-sagemaker-al2-support.json | 4 ++ packages/amazonq/src/lsp/client.ts | 43 ++++++++++++++++--- .../core/src/shared/extensionUtilities.ts | 19 ++++++++ packages/core/src/shared/vscode/env.ts | 32 +++++++++++++- 4 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json diff --git a/packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json b/packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json new file mode 100644 index 00000000000..9f15457f59e --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Improved Amazon Linux 2 support with better SageMaker environment detection" +} diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index c359ac73ded..6cbb05dd582 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -38,6 +38,7 @@ import { isAmazonLinux2, getClientId, extensionVersion, + isSageMaker, } from 'aws-core-vscode/shared' import { processUtils } from 'aws-core-vscode/shared' import { activate } from './chat/activation' @@ -53,11 +54,24 @@ import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChat const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') -export const glibcLinker: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER || '' -export const glibcPath: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH || '' - export function hasGlibcPatch(): boolean { - return glibcLinker.length > 0 && glibcPath.length > 0 + // Skip GLIBC patching for SageMaker environments + if (isSageMaker()) { + getLogger('amazonqLsp').info('SageMaker environment detected in hasGlibcPatch, skipping GLIBC patching') + return false // Return false to ensure SageMaker doesn't try to use GLIBC patching + } + + // Check for environment variables (for CDM) + const glibcLinker = process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER || '' + const glibcPath = process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH || '' + + if (glibcLinker.length > 0 && glibcPath.length > 0) { + getLogger('amazonqLsp').info('GLIBC patching environment variables detected') + return true + } + + // No environment variables, no patching needed + return false } export async function startLanguageServer( @@ -82,9 +96,24 @@ export async function startLanguageServer( const traceServerEnabled = Settings.instance.isSet(`${clientId}.trace.server`) let executable: string[] = [] // apply the GLIBC 2.28 path to node js runtime binary - if (isAmazonLinux2() && hasGlibcPatch()) { - executable = [glibcLinker, '--library-path', glibcPath, resourcePaths.node] - getLogger('amazonqLsp').info(`Patched node runtime with GLIBC to ${executable}`) + if (isSageMaker()) { + // SageMaker doesn't need GLIBC patching + getLogger('amazonqLsp').info('SageMaker environment detected, skipping GLIBC patching') + executable = [resourcePaths.node] + } else if (isAmazonLinux2() && hasGlibcPatch()) { + // Use environment variables if available (for CDM) + if (process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER && process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH) { + executable = [ + process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER, + '--library-path', + process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH, + resourcePaths.node, + ] + getLogger('amazonqLsp').info(`Patched node runtime with GLIBC using env vars to ${executable}`) + } else { + // No environment variables, use the node executable directly + executable = [resourcePaths.node] + } } else { executable = [resourcePaths.node] } diff --git a/packages/core/src/shared/extensionUtilities.ts b/packages/core/src/shared/extensionUtilities.ts index 9675f060951..d498d64ea16 100644 --- a/packages/core/src/shared/extensionUtilities.ts +++ b/packages/core/src/shared/extensionUtilities.ts @@ -170,12 +170,31 @@ export function isCloud9(flavor: 'classic' | 'codecatalyst' | 'any' = 'any'): bo return (flavor === 'classic' && !codecat) || (flavor === 'codecatalyst' && codecat) } +/** + * Checks if the current environment has SageMaker-specific environment variables + * @returns true if SageMaker environment variables are detected + */ +function hasSageMakerEnvVars(): boolean { + return ( + process.env.SAGEMAKER_APP_TYPE !== undefined || + process.env.SAGEMAKER_INTERNAL_IMAGE_URI !== undefined || + process.env.STUDIO_LOGGING_DIR?.includes('/var/log/studio') === true + ) +} + /** * * @param appName to identify the proper SM instance * @returns true if the current system is SageMaker(SMAI or SMUS) */ export function isSageMaker(appName: 'SMAI' | 'SMUS' = 'SMAI'): boolean { + // Check for SageMaker-specific environment variables first + if (hasSageMakerEnvVars() || process.env.SERVICE_NAME === sageMakerUnifiedStudio) { + getLogger().debug('SageMaker environment detected via environment variables') + return true + } + + // Fall back to app name checks switch (appName) { case 'SMAI': return vscode.env.appName === sageMakerAppname diff --git a/packages/core/src/shared/vscode/env.ts b/packages/core/src/shared/vscode/env.ts index 02d46ae6695..8f53465c6b5 100644 --- a/packages/core/src/shared/vscode/env.ts +++ b/packages/core/src/shared/vscode/env.ts @@ -125,13 +125,41 @@ export function isRemoteWorkspace(): boolean { } /** - * There is Amazon Linux 2. + * Checks if the current environment is running on Amazon Linux 2. * - * Use {@link isCloudDesktop()} to know if we are specifically using internal Amazon Linux 2. + * This function attempts to detect if we're running in a container on an AL2 host + * by checking both the OS release and container-specific indicators. * * Example: `5.10.220-188.869.amzn2int.x86_64` or `5.10.236-227.928.amzn2.x86_64` (Cloud Dev Machine) */ export function isAmazonLinux2() { + // First check if we're in a SageMaker environment, which should not be treated as AL2 + // even if the underlying host is AL2 + if ( + process.env.SAGEMAKER_APP_TYPE || + process.env.SERVICE_NAME === 'SageMakerUnifiedStudio' || + process.env.SAGEMAKER_INTERNAL_IMAGE_URI + ) { + return false + } + + // Check if we're in a container environment that's not AL2 + if (process.env.container === 'docker' || process.env.DOCKER_HOST || process.env.DOCKER_BUILDKIT) { + // Additional check for container OS - if we can determine it's not AL2 + try { + const fs = require('fs') + if (fs.existsSync('/etc/os-release')) { + const osRelease = fs.readFileSync('/etc/os-release', 'utf8') + if (!osRelease.includes('Amazon Linux 2') && !osRelease.includes('amzn2')) { + return false + } + } + } catch (e) { + // If we can't read the file, fall back to the os.release() check + } + } + + // Standard check for AL2 in the OS release string return (os.release().includes('.amzn2int.') || os.release().includes('.amzn2.')) && process.platform === 'linux' } From cac600f109519fad7f3037327892246862d80957 Mon Sep 17 00:00:00 2001 From: David <60020664+dhasani23@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:44:40 -0700 Subject: [PATCH 09/20] fix(amazonq): minor text update (#7554) ## Problem Minor text update request. ## Solution Update text. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. Co-authored-by: David Hasani --- .../amazonqGumby/chat/controller/messenger/messenger.ts | 5 ++++- packages/core/src/codewhisperer/models/constants.ts | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 0880e2556d9..699e3b77938 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -704,7 +704,10 @@ ${codeSnippet} } public async sendCustomDependencyVersionMessage(tabID: string) { - const message = CodeWhispererConstants.chooseConfigFileMessage + let message = CodeWhispererConstants.chooseConfigFileMessageLibraryUpgrade + if (transformByQState.getSourceJDKVersion() !== transformByQState.getTargetJDKVersion()) { + message = CodeWhispererConstants.chooseConfigFileMessageJdkUpgrade + } const buttons: ChatItemButton[] = [] buttons.push({ diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 5691d154625..9f8d4a5c950 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -646,8 +646,11 @@ export const continueWithoutConfigFileMessage = export const receivedValidConfigFileMessage = 'The dependency upgrade file looks good. I will use this information to upgrade the dependencies you specified.' -export const chooseConfigFileMessage = - 'Would you like to provide a custom dependency upgrade file? You can specify first-party dependencies to upgrade in a YAML file, and I will upgrade them during the JDK upgrade (for example, Java 8 to 17). You can initiate a separate transformation (17 to 17 or 21 to 21) after the initial JDK upgrade to transform third-party dependencies.\n\nWithout a YAML file, I can perform a minimum JDK upgrade, and then you can initiate a separate transformation to upgrade all third-party dependencies as part of a maximum transformation. For an example dependency upgrade file, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).' +export const chooseConfigFileMessageJdkUpgrade = + 'Would you like to provide a dependency upgrade file? You can specify first party dependencies and their versions in a YAML file, and I will upgrade them during the JDK upgrade transformation. For an example dependency upgrade file, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).' + +export const chooseConfigFileMessageLibraryUpgrade = + 'Would you like to provide a dependency upgrade file? You can specify third party dependencies and their versions in a YAML file, and I will only upgrade these dependencies during the library upgrade transformation. For an example dependency upgrade file, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).' export const enterJavaHomePlaceholder = 'Enter the path to your Java installation' From 053a5bd440ed3709c2df5d772fba247d0bb781db Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Wed, 25 Jun 2025 12:13:17 -0700 Subject: [PATCH 10/20] fix(amazonq): fix to move isSagemaker to env --- .../core/src/shared/extensionUtilities.ts | 16 ++---------- packages/core/src/shared/vscode/env.ts | 25 +++++++++++++++---- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/core/src/shared/extensionUtilities.ts b/packages/core/src/shared/extensionUtilities.ts index d498d64ea16..8037dfa0381 100644 --- a/packages/core/src/shared/extensionUtilities.ts +++ b/packages/core/src/shared/extensionUtilities.ts @@ -11,7 +11,7 @@ import { getLogger } from './logger/logger' import { VSCODE_EXTENSION_ID, extensionAlphaVersion } from './extensions' import { Ec2MetadataClient } from './clients/ec2MetadataClient' import { DefaultEc2MetadataClient } from './clients/ec2MetadataClient' -import { extensionVersion, getCodeCatalystDevEnvId } from './vscode/env' +import { extensionVersion, getCodeCatalystDevEnvId, hasSageMakerEnvVars } from './vscode/env' import globals from './extensionGlobals' import { once } from './utilities/functionUtils' import { @@ -170,18 +170,6 @@ export function isCloud9(flavor: 'classic' | 'codecatalyst' | 'any' = 'any'): bo return (flavor === 'classic' && !codecat) || (flavor === 'codecatalyst' && codecat) } -/** - * Checks if the current environment has SageMaker-specific environment variables - * @returns true if SageMaker environment variables are detected - */ -function hasSageMakerEnvVars(): boolean { - return ( - process.env.SAGEMAKER_APP_TYPE !== undefined || - process.env.SAGEMAKER_INTERNAL_IMAGE_URI !== undefined || - process.env.STUDIO_LOGGING_DIR?.includes('/var/log/studio') === true - ) -} - /** * * @param appName to identify the proper SM instance @@ -189,7 +177,7 @@ function hasSageMakerEnvVars(): boolean { */ export function isSageMaker(appName: 'SMAI' | 'SMUS' = 'SMAI'): boolean { // Check for SageMaker-specific environment variables first - if (hasSageMakerEnvVars() || process.env.SERVICE_NAME === sageMakerUnifiedStudio) { + if (hasSageMakerEnvVars()) { getLogger().debug('SageMaker environment detected via environment variables') return true } diff --git a/packages/core/src/shared/vscode/env.ts b/packages/core/src/shared/vscode/env.ts index 8f53465c6b5..5ee891cc7d3 100644 --- a/packages/core/src/shared/vscode/env.ts +++ b/packages/core/src/shared/vscode/env.ts @@ -124,6 +124,25 @@ export function isRemoteWorkspace(): boolean { return vscode.env.remoteName === 'ssh-remote' } +/** + * Checks if the current environment has SageMaker-specific environment variables + * @returns true if SageMaker environment variables are detected + */ +export function hasSageMakerEnvVars(): boolean { + // Check both old and new environment variable names + // SageMaker is renaming their environment variables in their Docker images + return ( + // Original environment variables + process.env.SAGEMAKER_APP_TYPE !== undefined || + process.env.SAGEMAKER_INTERNAL_IMAGE_URI !== undefined || + process.env.STUDIO_LOGGING_DIR?.includes('/var/log/studio') === true || + // New environment variables (update these with the actual new names) + process.env.SM_APP_TYPE !== undefined || + process.env.SM_INTERNAL_IMAGE_URI !== undefined || + process.env.SERVICE_NAME === 'SageMakerUnifiedStudio' + ) +} + /** * Checks if the current environment is running on Amazon Linux 2. * @@ -135,11 +154,7 @@ export function isRemoteWorkspace(): boolean { export function isAmazonLinux2() { // First check if we're in a SageMaker environment, which should not be treated as AL2 // even if the underlying host is AL2 - if ( - process.env.SAGEMAKER_APP_TYPE || - process.env.SERVICE_NAME === 'SageMakerUnifiedStudio' || - process.env.SAGEMAKER_INTERNAL_IMAGE_URI - ) { + if (hasSageMakerEnvVars()) { return false } From 05c57ff758182b6a2c7dc8a725989b8f39adc466 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 25 Jun 2025 20:29:29 +0000 Subject: [PATCH 11/20] Release 1.79.0 --- package-lock.json | 4 ++-- packages/amazonq/.changes/1.79.0.json | 18 ++++++++++++++++++ ...x-a06c2136-a87a-41af-9304-454bc77aaecc.json | 4 ---- .../Bug Fix-sagemaker-al2-support.json | 4 ---- ...e-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json | 4 ---- packages/amazonq/CHANGELOG.md | 6 ++++++ packages/amazonq/package.json | 2 +- 7 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 packages/amazonq/.changes/1.79.0.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json diff --git a/package-lock.json b/package-lock.json index e9418440268..342807fcc57 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", @@ -25679,7 +25679,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.79.0-SNAPSHOT", + "version": "1.79.0", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/amazonq/.changes/1.79.0.json b/packages/amazonq/.changes/1.79.0.json new file mode 100644 index 00000000000..51d910cca2b --- /dev/null +++ b/packages/amazonq/.changes/1.79.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-06-25", + "version": "1.79.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Added automatic system certificate detection and VSCode proxy settings support" + }, + { + "type": "Bug Fix", + "description": "Improved Amazon Linux 2 support with better SageMaker environment detection" + }, + { + "type": "Feature", + "description": "/transform: run all builds client-side" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json b/packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json deleted file mode 100644 index f38b0f1375f..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-a06c2136-a87a-41af-9304-454bc77aaecc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Added automatic system certificate detection and VSCode proxy settings support" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json b/packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json deleted file mode 100644 index 9f15457f59e..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-sagemaker-al2-support.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Improved Amazon Linux 2 support with better SageMaker environment detection" -} diff --git a/packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json b/packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json deleted file mode 100644 index 8028e402f9f..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-d46a67ff-b237-46cc-b6e7-8de8f2e87f45.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "/transform: run all builds client-side" -} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index c3e17d8a77b..e4d0ff47c77 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.79.0 2025-06-25 + +- **Bug Fix** Added automatic system certificate detection and VSCode proxy settings support +- **Bug Fix** Improved Amazon Linux 2 support with better SageMaker environment detection +- **Feature** /transform: run all builds client-side + ## 1.78.0 2025-06-20 - **Bug Fix** Resolve missing chat options in Amazon Q chat interface. diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index f52c8c1beb0..20b89d6596e 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.79.0-SNAPSHOT", + "version": "1.79.0", "extensionKind": [ "workspace" ], From 787a3bc7797ab4b88a2b39c8d9d635476961c6fa Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 25 Jun 2025 20:40:07 +0000 Subject: [PATCH 12/20] Release 3.67.0 --- package-lock.json | 4 ++-- packages/toolkit/.changes/3.67.0.json | 18 ++++++++++++++++++ ...x-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json | 4 ---- ...x-de3cdda1-252e-4d04-96cb-7fb935649c0e.json | 4 ---- ...e-2fdc05d7-db85-4a4f-8af5-ec729fade8fd.json | 4 ---- packages/toolkit/CHANGELOG.md | 6 ++++++ packages/toolkit/package.json | 2 +- 7 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 packages/toolkit/.changes/3.67.0.json delete mode 100644 packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json delete mode 100644 packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json delete mode 100644 packages/toolkit/.changes/next-release/Feature-2fdc05d7-db85-4a4f-8af5-ec729fade8fd.json diff --git a/package-lock.json b/package-lock.json index e9418440268..8b78bec1449 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", @@ -27393,7 +27393,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.67.0-SNAPSHOT", + "version": "3.67.0", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/toolkit/.changes/3.67.0.json b/packages/toolkit/.changes/3.67.0.json new file mode 100644 index 00000000000..21522dc5d87 --- /dev/null +++ b/packages/toolkit/.changes/3.67.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-06-25", + "version": "3.67.0", + "entries": [ + { + "type": "Bug Fix", + "description": "State Machine deployments can now be initiated directly from Workflow Studio without closing the editor" + }, + { + "type": "Bug Fix", + "description": "Step Function performance metrics now accurately reflect only Workflow Studio document activity" + }, + { + "type": "Feature", + "description": "AccessAnalyzer: CheckNoPublicAccess custom policy check supports additional resource types." + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json b/packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json deleted file mode 100644 index 1d0f2041fa8..00000000000 --- a/packages/toolkit/.changes/next-release/Bug Fix-8beaac0b-fb5e-42bd-8ecf-a185266a9f04.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "State Machine deployments can now be initiated directly from Workflow Studio without closing the editor" -} diff --git a/packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json b/packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json deleted file mode 100644 index 2e1c167dccd..00000000000 --- a/packages/toolkit/.changes/next-release/Bug Fix-de3cdda1-252e-4d04-96cb-7fb935649c0e.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Step Function performance metrics now accurately reflect only Workflow Studio document activity" -} diff --git a/packages/toolkit/.changes/next-release/Feature-2fdc05d7-db85-4a4f-8af5-ec729fade8fd.json b/packages/toolkit/.changes/next-release/Feature-2fdc05d7-db85-4a4f-8af5-ec729fade8fd.json deleted file mode 100644 index 9feefa5d96f..00000000000 --- a/packages/toolkit/.changes/next-release/Feature-2fdc05d7-db85-4a4f-8af5-ec729fade8fd.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "AccessAnalyzer: CheckNoPublicAccess custom policy check supports additional resource types." -} diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index d978b0c9a82..a4592dd068d 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.67.0 2025-06-25 + +- **Bug Fix** State Machine deployments can now be initiated directly from Workflow Studio without closing the editor +- **Bug Fix** Step Function performance metrics now accurately reflect only Workflow Studio document activity +- **Feature** AccessAnalyzer: CheckNoPublicAccess custom policy check supports additional resource types. + ## 3.66.0 2025-06-18 - Miscellaneous non-user-facing changes diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index e4a6639c3a1..6ae78f3916a 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.67.0-SNAPSHOT", + "version": "3.67.0", "extensionKind": [ "workspace" ], From 926b86a0ea9709d0309cfa4f58f53b9eb1a7dec7 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 25 Jun 2025 21:02:20 +0000 Subject: [PATCH 13/20] Update version to snapshot version: 3.68.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 8b78bec1449..61ac8c4010c 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", @@ -27393,7 +27393,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.67.0", + "version": "3.68.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 6ae78f3916a..2b141e2a4e7 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.67.0", + "version": "3.68.0-SNAPSHOT", "extensionKind": [ "workspace" ], From fd252bb6ffbe2dd9371aecbab871441c915e18fe Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 25 Jun 2025 21:03:28 +0000 Subject: [PATCH 14/20] Update version to snapshot version: 1.80.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 342807fcc57..3a0a2c8a1b1 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", @@ -25679,7 +25679,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.79.0", + "version": "1.80.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 20b89d6596e..764c60637ac 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.79.0", + "version": "1.80.0-SNAPSHOT", "extensionKind": [ "workspace" ], From 00d7c13afc25c8f124eaeb54731d15eaa6847665 Mon Sep 17 00:00:00 2001 From: Tyrone Smith Date: Wed, 25 Jun 2025 14:58:40 -0700 Subject: [PATCH 15/20] fix(amazonq): Re-enable experimental proxy support --- packages/core/src/shared/utilities/proxyUtil.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/shared/utilities/proxyUtil.ts b/packages/core/src/shared/utilities/proxyUtil.ts index a8d2056d7f4..f03c2b18a5e 100644 --- a/packages/core/src/shared/utilities/proxyUtil.ts +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -70,6 +70,12 @@ export class ProxyUtil { * Sets environment variables based on proxy configuration */ private static async setProxyEnvironmentVariables(config: ProxyConfig): Promise { + // Always enable experimental proxy support for better handling of both explicit and transparent proxies + process.env.EXPERIMENTAL_HTTP_PROXY_SUPPORT = 'true' + + // Load built-in bundle and system OS trust store + process.env.NODE_OPTIONS = '--use-system-ca' + const proxyUrl = config.proxyUrl // Set proxy environment variables if (proxyUrl) { From e07833d92eb05dde8a7954103e5296f4fbd1849b Mon Sep 17 00:00:00 2001 From: zelzhou Date: Fri, 27 Jun 2025 11:07:03 -0700 Subject: [PATCH 16/20] refactor(stepfunctions): migrate to sdkv3 (#7560) ## Problem step functions still uses sdk v2 ## Solution - Refactor `DefaultStepFunctionsClient` to `StepFunctionsClient` using ClientWrapper. - Refactor references to `DefaultStepFunctionsClient` with `StepFunctionsClient` and reference sdk v3 types ## Verification - Verified CreateStateMachine, UpdateStateMachine works and creates/updates StateMachine - Verified AWS Explorer lists all StateMachines using ListStateMachine - Verified StartExecution works - Verified TestState, ListIAMRoles works in Workflow Studio, which also fixes the bug that variables cannot be used in TestState in Workflow Studio - fixes https://github.com/aws/aws-toolkit-vscode/issues/6819 ![TestStateWithVariables](https://github.com/user-attachments/assets/ab981622-b773-4983-a9ce-a70c8fcfc711) --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- package-lock.json | 525 ++++++++++++++++++ packages/core/package.json | 1 + .../core/src/shared/clients/stepFunctions.ts | 68 +++ .../src/shared/clients/stepFunctionsClient.ts | 79 --- .../downloadStateMachineDefinition.ts | 15 +- .../commands/publishStateMachine.ts | 6 +- .../explorer/stepFunctionsNodes.ts | 8 +- packages/core/src/stepFunctions/utils.ts | 4 +- .../executeStateMachine.ts | 15 +- .../wizards/publishStateMachineWizard.ts | 10 +- .../src/stepFunctions/workflowStudio/types.ts | 3 +- .../workflowStudioApiHandler.ts | 6 +- .../explorer/stepFunctionNodes.test.ts | 4 +- .../workflowStudioApiHandler.test.ts | 4 +- ...-3da4aea6-cfe4-40dc-a3d6-8dbd35e120e6.json | 4 + 15 files changed, 638 insertions(+), 114 deletions(-) create mode 100644 packages/core/src/shared/clients/stepFunctions.ts delete mode 100644 packages/core/src/shared/clients/stepFunctionsClient.ts create mode 100644 packages/toolkit/.changes/next-release/Bug Fix-3da4aea6-cfe4-40dc-a3d6-8dbd35e120e6.json diff --git a/package-lock.json b/package-lock.json index 5f8822e496b..ec87f1b7064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7211,6 +7211,530 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-sfn": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sfn/-/client-sfn-3.693.0.tgz", + "integrity": "sha512-B2K3aXGnP7eD1ITEIx4kO43l1N5OLqHdLW4AUbwoopwU5qzicc9jADrthXpGxymJI8AhJz9T2WtLmceBU2EpNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-ssm": { "version": "3.693.0", "license": "Apache-2.0", @@ -25710,6 +26234,7 @@ "@aws-sdk/client-iam": "<3.731.0", "@aws-sdk/client-lambda": "<3.731.0", "@aws-sdk/client-s3": "<3.731.0", + "@aws-sdk/client-sfn": "<3.731.0", "@aws-sdk/client-ssm": "<3.731.0", "@aws-sdk/client-sso": "<3.731.0", "@aws-sdk/client-sso-oidc": "<3.731.0", diff --git a/packages/core/package.json b/packages/core/package.json index d1287b5db07..29edf144931 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -517,6 +517,7 @@ "@aws-sdk/client-ssm": "<3.731.0", "@aws-sdk/client-sso": "<3.731.0", "@aws-sdk/client-sso-oidc": "<3.731.0", + "@aws-sdk/client-sfn": "<3.731.0", "@aws-sdk/credential-provider-env": "<3.731.0", "@aws-sdk/credential-provider-process": "<3.731.0", "@aws-sdk/credential-provider-sso": "<3.731.0", diff --git a/packages/core/src/shared/clients/stepFunctions.ts b/packages/core/src/shared/clients/stepFunctions.ts new file mode 100644 index 00000000000..22483307654 --- /dev/null +++ b/packages/core/src/shared/clients/stepFunctions.ts @@ -0,0 +1,68 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CreateStateMachineCommand, + CreateStateMachineCommandInput, + CreateStateMachineCommandOutput, + DescribeStateMachineCommand, + DescribeStateMachineCommandInput, + DescribeStateMachineCommandOutput, + ListStateMachinesCommand, + ListStateMachinesCommandInput, + ListStateMachinesCommandOutput, + SFNClient, + StartExecutionCommand, + StartExecutionCommandInput, + StartExecutionCommandOutput, + StateMachineListItem, + TestStateCommand, + TestStateCommandInput, + TestStateCommandOutput, + UpdateStateMachineCommand, + UpdateStateMachineCommandInput, + UpdateStateMachineCommandOutput, +} from '@aws-sdk/client-sfn' +import { ClientWrapper } from './clientWrapper' + +export class StepFunctionsClient extends ClientWrapper { + public constructor(regionCode: string) { + super(regionCode, SFNClient) + } + + public async *listStateMachines( + request: ListStateMachinesCommandInput = {} + ): AsyncIterableIterator { + do { + const response: ListStateMachinesCommandOutput = await this.makeRequest(ListStateMachinesCommand, request) + if (response.stateMachines) { + yield* response.stateMachines + } + request.nextToken = response.nextToken + } while (request.nextToken) + } + + public async getStateMachineDetails( + request: DescribeStateMachineCommandInput + ): Promise { + return this.makeRequest(DescribeStateMachineCommand, request) + } + + public async executeStateMachine(request: StartExecutionCommandInput): Promise { + return this.makeRequest(StartExecutionCommand, request) + } + + public async createStateMachine(request: CreateStateMachineCommandInput): Promise { + return this.makeRequest(CreateStateMachineCommand, request) + } + + public async updateStateMachine(request: UpdateStateMachineCommandInput): Promise { + return this.makeRequest(UpdateStateMachineCommand, request) + } + + public async testState(request: TestStateCommandInput): Promise { + return this.makeRequest(TestStateCommand, request) + } +} diff --git a/packages/core/src/shared/clients/stepFunctionsClient.ts b/packages/core/src/shared/clients/stepFunctionsClient.ts deleted file mode 100644 index 66d45bcd58a..00000000000 --- a/packages/core/src/shared/clients/stepFunctionsClient.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { StepFunctions } from 'aws-sdk' -import globals from '../extensionGlobals' -import { ClassToInterfaceType } from '../utilities/tsUtils' - -export type StepFunctionsClient = ClassToInterfaceType -export class DefaultStepFunctionsClient { - public constructor(public readonly regionCode: string) {} - - public async *listStateMachines(): AsyncIterableIterator { - const client = await this.createSdkClient() - - const request: StepFunctions.ListStateMachinesInput = {} - do { - const response: StepFunctions.ListStateMachinesOutput = await client.listStateMachines(request).promise() - - if (response.stateMachines) { - yield* response.stateMachines - } - - request.nextToken = response.nextToken - } while (request.nextToken) - } - - public async getStateMachineDetails(arn: string): Promise { - const client = await this.createSdkClient() - - const request: StepFunctions.DescribeStateMachineInput = { - stateMachineArn: arn, - } - - const response: StepFunctions.DescribeStateMachineOutput = await client.describeStateMachine(request).promise() - - return response - } - - public async executeStateMachine(arn: string, input?: string): Promise { - const client = await this.createSdkClient() - - const request: StepFunctions.StartExecutionInput = { - stateMachineArn: arn, - input: input, - } - - const response: StepFunctions.StartExecutionOutput = await client.startExecution(request).promise() - - return response - } - - public async createStateMachine( - params: StepFunctions.CreateStateMachineInput - ): Promise { - const client = await this.createSdkClient() - - return client.createStateMachine(params).promise() - } - - public async updateStateMachine( - params: StepFunctions.UpdateStateMachineInput - ): Promise { - const client = await this.createSdkClient() - - return client.updateStateMachine(params).promise() - } - - public async testState(params: StepFunctions.TestStateInput): Promise { - const client = await this.createSdkClient() - - return await client.testState(params).promise() - } - - private async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService(StepFunctions, undefined, this.regionCode) - } -} diff --git a/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts b/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts index f26fc8a793b..e89dad1ffcd 100644 --- a/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts +++ b/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts @@ -7,10 +7,10 @@ import * as nls from 'vscode-nls' import * as os from 'os' const localize = nls.loadMessageBundle() -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import * as path from 'path' import * as vscode from 'vscode' -import { DefaultStepFunctionsClient, StepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { getLogger, Logger } from '../../shared/logger/logger' import { Result } from '../../shared/telemetry/telemetry' @@ -28,10 +28,11 @@ export async function downloadStateMachineDefinition(params: { let downloadResult: Result = 'Succeeded' const stateMachineName = params.stateMachineNode.details.name try { - const client: StepFunctionsClient = new DefaultStepFunctionsClient(params.stateMachineNode.regionCode) - const stateMachineDetails: StepFunctions.DescribeStateMachineOutput = await client.getStateMachineDetails( - params.stateMachineNode.details.stateMachineArn - ) + const client: StepFunctionsClient = new StepFunctionsClient(params.stateMachineNode.regionCode) + const stateMachineDetails: StepFunctions.DescribeStateMachineCommandOutput = + await client.getStateMachineDetails({ + stateMachineArn: params.stateMachineNode.details.stateMachineArn, + }) if (params.isPreviewAndRender) { const doc = await vscode.workspace.openTextDocument({ @@ -53,7 +54,7 @@ export async function downloadStateMachineDefinition(params: { if (fileInfo) { const filePath = fileInfo.fsPath - await fs.writeFile(filePath, stateMachineDetails.definition, 'utf8') + await fs.writeFile(filePath, stateMachineDetails.definition || '', 'utf8') const openPath = vscode.Uri.file(filePath) const doc = await vscode.workspace.openTextDocument(openPath) await vscode.window.showTextDocument(doc) diff --git a/packages/core/src/stepFunctions/commands/publishStateMachine.ts b/packages/core/src/stepFunctions/commands/publishStateMachine.ts index 385a412478f..799d5b2a724 100644 --- a/packages/core/src/stepFunctions/commands/publishStateMachine.ts +++ b/packages/core/src/stepFunctions/commands/publishStateMachine.ts @@ -7,7 +7,7 @@ import { load } from 'js-yaml' import * as vscode from 'vscode' import * as nls from 'vscode-nls' import { AwsContext } from '../../shared/awsContext' -import { DefaultStepFunctionsClient, StepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { getLogger, Logger } from '../../shared/logger/logger' import { showViewLogsMessage } from '../../shared/utilities/messages' import { VALID_SFN_PUBLISH_FORMATS, YAML_FORMATS } from '../constants/aslFormats' @@ -64,7 +64,7 @@ export async function publishStateMachine(params: publishStateMachineParams) { if (!response) { return } - const client = new DefaultStepFunctionsClient(response.region) + const client = new StepFunctionsClient(response.region) if (response?.createResponse) { await createStateMachine(response.createResponse, text, params.outputChannel, response.region, client) @@ -109,7 +109,7 @@ async function createStateMachine( wizardResponse.name ) ) - outputChannel.appendLine(result.stateMachineArn) + outputChannel.appendLine(result.stateMachineArn || '') logger.info(`Created "${result.stateMachineArn}"`) } catch (err) { const msg = localize( diff --git a/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts b/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts index 2fcf62a9eb6..8b693d3bb9e 100644 --- a/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts +++ b/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts @@ -7,9 +7,9 @@ import * as os from 'os' import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import * as vscode from 'vscode' -import { DefaultStepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' @@ -40,7 +40,7 @@ export class StepFunctionsNode extends AWSTreeNodeBase { public constructor( public override readonly regionCode: string, - private readonly client = new DefaultStepFunctionsClient(regionCode) + private readonly client = new StepFunctionsClient(regionCode) ) { super('Step Functions', vscode.TreeItemCollapsibleState.Collapsed) this.stateMachineNodes = new Map() @@ -101,7 +101,7 @@ export class StateMachineNode extends AWSTreeNodeBase implements AWSResourceNode } public get arn(): string { - return this.details.stateMachineArn + return this.details.stateMachineArn || '' } public get name(): string { diff --git a/packages/core/src/stepFunctions/utils.ts b/packages/core/src/stepFunctions/utils.ts index fedea23acc5..f578d6cda86 100644 --- a/packages/core/src/stepFunctions/utils.ts +++ b/packages/core/src/stepFunctions/utils.ts @@ -5,10 +5,10 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import * as yaml from 'js-yaml' import * as vscode from 'vscode' -import { StepFunctionsClient } from '../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../shared/clients/stepFunctions' import { DiagnosticSeverity, DocumentLanguageSettings, diff --git a/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts b/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts index 985f2494e52..b4e47bc65f6 100644 --- a/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts +++ b/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { DefaultStepFunctionsClient } from '../../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../../shared/clients/stepFunctions' import { getLogger } from '../../../shared/logger/logger' import { Result } from '../../../shared/telemetry/telemetry' @@ -56,11 +56,14 @@ export class ExecuteStateMachineWebview extends VueWebview { this.channel.appendLine('') try { - const client = new DefaultStepFunctionsClient(this.stateMachine.region) - const startExecResponse = await client.executeStateMachine(this.stateMachine.arn, input) + const client = new StepFunctionsClient(this.stateMachine.region) + const startExecResponse = await client.executeStateMachine({ + stateMachineArn: this.stateMachine.arn, + input, + }) this.logger.info('started execution for Step Functions State Machine') this.channel.appendLine(localize('AWS.stepFunctions.executeStateMachine.info.started', 'Execution started')) - this.channel.appendLine(startExecResponse.executionArn) + this.channel.appendLine(startExecResponse.executionArn || '') } catch (e) { executeResult = 'Failed' const error = e as Error @@ -82,8 +85,8 @@ const Panel = VueWebview.compilePanel(ExecuteStateMachineWebview) export async function executeStateMachine(context: ExtContext, node: StateMachineNode): Promise { const wv = new Panel(context.extensionContext, context.outputChannel, { - arn: node.details.stateMachineArn, - name: node.details.name, + arn: node.details.stateMachineArn || '', + name: node.details.name || '', region: node.regionCode, }) diff --git a/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts b/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts index 866d7f5bb03..ed141ac1894 100644 --- a/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts +++ b/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts @@ -22,7 +22,7 @@ import { Wizard, WIZARD_BACK } from '../../shared/wizards/wizard' import { isStepFunctionsRole } from '../utils' import { createRolePrompter } from '../../shared/ui/common/roles' import { IamClient } from '../../shared/clients/iam' -import { DefaultStepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' export enum PublishStateMachineAction { QuickCreate, @@ -109,14 +109,14 @@ function createStepFunctionsRolePrompter(region: string) { } async function* listStateMachines(region: string) { - const client = new DefaultStepFunctionsClient(region) + const client = new StepFunctionsClient(region) for await (const machine of client.listStateMachines()) { yield [ { - label: machine.name, - data: machine.stateMachineArn, - description: machine.stateMachineArn, + label: machine.name || '', + data: machine.stateMachineArn || '', + description: machine.stateMachineArn || '', }, ] } diff --git a/packages/core/src/stepFunctions/workflowStudio/types.ts b/packages/core/src/stepFunctions/workflowStudio/types.ts index 989ef4517d6..5edf944eb2c 100644 --- a/packages/core/src/stepFunctions/workflowStudio/types.ts +++ b/packages/core/src/stepFunctions/workflowStudio/types.ts @@ -2,7 +2,8 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import { IAM, StepFunctions } from 'aws-sdk' +import { IAM } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import * as vscode from 'vscode' export enum WorkflowMode { diff --git a/packages/core/src/stepFunctions/workflowStudio/workflowStudioApiHandler.ts b/packages/core/src/stepFunctions/workflowStudio/workflowStudioApiHandler.ts index 8b7244cd844..6c0fb850b9d 100644 --- a/packages/core/src/stepFunctions/workflowStudio/workflowStudioApiHandler.ts +++ b/packages/core/src/stepFunctions/workflowStudio/workflowStudioApiHandler.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import { IamClient, IamRole } from '../../shared/clients/iam' -import { DefaultStepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { ApiAction, ApiCallRequestMessage, Command, MessageType, WebviewContext } from './types' import { telemetry } from '../../shared/telemetry/telemetry' import { ListRolesRequest } from '@aws-sdk/client-iam' @@ -15,7 +15,7 @@ export class WorkflowStudioApiHandler { region: string, private readonly context: WebviewContext, private readonly clients = { - sfn: new DefaultStepFunctionsClient(region), + sfn: new StepFunctionsClient(region), iam: new IamClient(region), } ) {} diff --git a/packages/core/src/test/stepFunctions/explorer/stepFunctionNodes.test.ts b/packages/core/src/test/stepFunctions/explorer/stepFunctionNodes.test.ts index 0f6df30e3e7..61b4f2b1813 100644 --- a/packages/core/src/test/stepFunctions/explorer/stepFunctionNodes.test.ts +++ b/packages/core/src/test/stepFunctions/explorer/stepFunctionNodes.test.ts @@ -15,14 +15,14 @@ import { } from '../../utilities/explorerNodeAssertions' import { asyncGenerator } from '../../../shared/utilities/collectionUtils' import globals from '../../../shared/extensionGlobals' -import { DefaultStepFunctionsClient } from '../../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../../shared/clients/stepFunctions' import { stub } from '../../utilities/stubber' const regionCode = 'someregioncode' describe('StepFunctionsNode', function () { function createStatesClient(...stateMachineNames: string[]) { - const client = stub(DefaultStepFunctionsClient, { regionCode }) + const client = stub(StepFunctionsClient, { regionCode }) client.listStateMachines.returns( asyncGenerator( stateMachineNames.map((name) => { diff --git a/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts b/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts index 32c9160c1c1..c16534abc4d 100644 --- a/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts +++ b/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts @@ -16,7 +16,7 @@ import { } from '../../../stepFunctions/workflowStudio/types' import * as vscode from 'vscode' import { assertTelemetry } from '../../testUtil' -import { DefaultStepFunctionsClient } from '../../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../../shared/clients/stepFunctions' import { IamClient } from '../../../shared/clients/iam' describe('WorkflowStudioApiHandler', function () { @@ -64,7 +64,7 @@ describe('WorkflowStudioApiHandler', function () { fileId: '', } - const sfnClient = new DefaultStepFunctionsClient('us-east-1') + const sfnClient = new StepFunctionsClient('us-east-1') apiHandler = new WorkflowStudioApiHandler('us-east-1', context, { sfn: sfnClient, iam: new IamClient('us-east-1'), diff --git a/packages/toolkit/.changes/next-release/Bug Fix-3da4aea6-cfe4-40dc-a3d6-8dbd35e120e6.json b/packages/toolkit/.changes/next-release/Bug Fix-3da4aea6-cfe4-40dc-a3d6-8dbd35e120e6.json new file mode 100644 index 00000000000..4394f843b31 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-3da4aea6-cfe4-40dc-a3d6-8dbd35e120e6.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "[StepFunctions]: Cannot call TestState with variables in Workflow Studio" +} From dfa7e511e3ed602d6a9aeb43841c8b71e73b8759 Mon Sep 17 00:00:00 2001 From: Tyrone Smith Date: Fri, 27 Jun 2025 11:09:06 -0700 Subject: [PATCH 17/20] fix(amazonq): Remove incompatible Node 18 flag --- packages/core/src/shared/utilities/proxyUtil.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/src/shared/utilities/proxyUtil.ts b/packages/core/src/shared/utilities/proxyUtil.ts index f03c2b18a5e..14e628e87f7 100644 --- a/packages/core/src/shared/utilities/proxyUtil.ts +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -73,9 +73,6 @@ export class ProxyUtil { // Always enable experimental proxy support for better handling of both explicit and transparent proxies process.env.EXPERIMENTAL_HTTP_PROXY_SUPPORT = 'true' - // Load built-in bundle and system OS trust store - process.env.NODE_OPTIONS = '--use-system-ca' - const proxyUrl = config.proxyUrl // Set proxy environment variables if (proxyUrl) { From 9bd9d997cf46c37218fd76ea7dfb1aa09b4bd66d Mon Sep 17 00:00:00 2001 From: Ralph Flora Date: Mon, 30 Jun 2025 10:09:00 -0700 Subject: [PATCH 18/20] feat(amazonq): Add next edit suggestion (#7555) --- package-lock.json | 173 +++++- package.json | 2 + packages/amazonq/package.json | 34 ++ .../src/app/inline/EditRendering/diffUtils.ts | 142 +++++ .../app/inline/EditRendering/displayImage.ts | 377 ++++++++++++ .../app/inline/EditRendering/imageRenderer.ts | 56 ++ .../app/inline/EditRendering/svgGenerator.ts | 548 ++++++++++++++++++ packages/amazonq/src/app/inline/activation.ts | 3 + packages/amazonq/src/app/inline/completion.ts | 64 +- .../src/app/inline/cursorUpdateManager.ts | 190 ++++++ .../src/app/inline/recommendationService.ts | 135 +++-- .../amazonq/src/app/inline/sessionManager.ts | 2 +- .../amazonq/src/app/inline/webViewPanel.ts | 450 ++++++++++++++ packages/amazonq/src/lsp/client.ts | 5 + .../apps/inline/recommendationService.test.ts | 200 ++++++- .../inline/EditRendering/diffUtils.test.ts | 105 ++++ .../inline/EditRendering/displayImage.test.ts | 176 ++++++ .../EditRendering/imageRenderer.test.ts | 271 +++++++++ .../inline/EditRendering/svgGenerator.test.ts | 278 +++++++++ .../app/inline/cursorUpdateManager.test.ts | 239 ++++++++ .../test/unit/app/inline/webViewPanel.test.ts | 153 +++++ packages/core/package.json | 6 +- packages/core/src/shared/index.ts | 2 + .../core/src/shared/settings-toolkit.gen.ts | 3 +- .../core/src/shared/utilities/diffUtils.ts | 50 ++ packages/core/src/shared/vscode/setContext.ts | 1 + packages/toolkit/package.json | 4 + packages/webpack.web.config.js | 3 + 28 files changed, 3593 insertions(+), 79 deletions(-) create mode 100644 packages/amazonq/src/app/inline/EditRendering/diffUtils.ts create mode 100644 packages/amazonq/src/app/inline/EditRendering/displayImage.ts create mode 100644 packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts create mode 100644 packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts create mode 100644 packages/amazonq/src/app/inline/cursorUpdateManager.ts create mode 100644 packages/amazonq/src/app/inline/webViewPanel.ts create mode 100644 packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts create mode 100644 packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts create mode 100644 packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts create mode 100644 packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts create mode 100644 packages/amazonq/test/unit/app/inline/cursorUpdateManager.test.ts create mode 100644 packages/amazonq/test/unit/app/inline/webViewPanel.test.ts diff --git a/package-lock.json b/package-lock.json index ec87f1b7064..4c207195030 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ ], "dependencies": { "@types/node": "^22.7.5", + "jaro-winkler": "^0.2.8", "vscode-nls": "^5.2.0", "vscode-nls-dev": "^4.0.4" }, @@ -24,6 +25,7 @@ "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", + "@types/jaro-winkler": "^0.2.4", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", "@types/webpack-env": "^1.18.5", @@ -13629,6 +13631,25 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@svgdotjs/svg.js": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", + "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "license": "MIT", @@ -13871,6 +13892,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jaro-winkler": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jaro-winkler/-/jaro-winkler-0.2.4.tgz", + "integrity": "sha512-TNVu6vL0Z3h+hYcW78IRloINA0y0MTVJ1PFVtVpBSgk+ejmaH5aVfcVghzNXZ0fa6gXe4zapNMQtMGWOJKTLig==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/js-yaml": { "version": "4.0.5", "dev": true, @@ -14077,6 +14105,13 @@ "@types/node": "*" } }, + "node_modules/@types/svgdom": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/svgdom/-/svgdom-0.1.2.tgz", + "integrity": "sha512-ZFwX8cDhbz6jiv3JZdMVYq8SSWHOUchChPmRoMwdIu3lz89aCu/gVK9TdR1eeb0ARQ8+5rtjUKrk1UR8hh0dhQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tcp-port-used": { "version": "1.0.1", "dev": true, @@ -15756,6 +15791,15 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browser-stdout": { "version": "1.3.1", "dev": true, @@ -17087,6 +17131,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { "version": "5.1.0", "license": "BSD-3-Clause", @@ -18158,7 +18208,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -18421,6 +18470,23 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.3", "license": "MIT", @@ -19312,6 +19378,21 @@ "node": ">= 4" } }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/immediate": { "version": "3.0.6", "dev": true, @@ -19887,6 +19968,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jaro-winkler": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/jaro-winkler/-/jaro-winkler-0.2.8.tgz", + "integrity": "sha512-yr+mElb6dWxA1mzFu0+26njV5DWAQRNTi5pB6fFMm79zHrfAs3d0qjhe/IpZI4AHIUJkzvu5QXQRWOw2O0GQyw==", + "license": "MIT" + }, "node_modules/jest-worker": { "version": "27.5.1", "dev": true, @@ -22455,6 +22542,15 @@ "dev": true, "license": "MIT" }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -23097,6 +23193,12 @@ "lowercase-keys": "^2.0.0" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.13.1", "dev": true, @@ -24073,6 +24175,27 @@ "svg2ttf": "svg2ttf.js" } }, + "node_modules/svgdom": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/svgdom/-/svgdom-0.1.21.tgz", + "integrity": "sha512-PrMx2aEzjRgyK9nbff6/NOzNmGcRnkjwO9p3JnHISmqPTMGtBPi4uFp59fVhI9PqRp8rVEWgmXFbkgYRsTnapg==", + "license": "MIT", + "dependencies": { + "fontkit": "^2.0.4", + "image-size": "^1.2.1", + "sax": "^1.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/svgdom/node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/svgicons2svgfont": { "version": "10.0.6", "dev": true, @@ -24391,6 +24514,12 @@ "next-tick": "1" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.1", "dev": true, @@ -24537,7 +24666,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsscmp": { @@ -24746,6 +24877,32 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "dev": true, @@ -26257,6 +26414,7 @@ "@smithy/service-error-classification": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.0", "@smithy/util-retry": "^4.0.1", + "@svgdotjs/svg.js": "^3.0.16", "@vscode/debugprotocol": "^1.57.0", "@zip.js/zip.js": "^2.7.41", "adm-zip": "^0.5.10", @@ -26275,6 +26433,7 @@ "http2": "^3.3.6", "i18n-ts": "^1.0.5", "immutable": "^4.3.0", + "jaro-winkler": "^0.2.8", "jose": "5.4.1", "js-yaml": "^4.1.0", "jsonc-parser": "^3.2.0", @@ -26287,6 +26446,7 @@ "semver": "^7.5.4", "stream-buffers": "^3.0.2", "strip-ansi": "^5.2.0", + "svgdom": "^0.1.0", "tcp-port-used": "^1.0.1", "vscode-languageclient": "^6.1.4", "vscode-languageserver": "^6.1.1", @@ -26331,6 +26491,7 @@ "@types/sinon": "^10.0.5", "@types/sinonjs__fake-timers": "^8.1.2", "@types/stream-buffers": "^3.0.7", + "@types/svgdom": "^0.1.2", "@types/tcp-port-used": "^1.0.1", "@types/uuid": "^9.0.1", "@types/whatwg-url": "^11.0.4", @@ -29577,10 +29738,6 @@ "tree-kill": "cli.js" } }, - "src.gen/@amzn/amazon-q-developer-streaming-client/node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, "src.gen/@amzn/amazon-q-developer-streaming-client/node_modules/typescript": { "version": "5.2.2", "dev": true, @@ -31131,10 +31288,6 @@ "tree-kill": "cli.js" } }, - "src.gen/@amzn/codewhisperer-streaming/node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, "src.gen/@amzn/codewhisperer-streaming/node_modules/typescript": { "version": "5.2.2", "dev": true, diff --git a/package.json b/package.json index 5134b42aaa3..8dab28aba35 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", + "@types/jaro-winkler": "^0.2.4", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", "@types/webpack-env": "^1.18.5", @@ -74,6 +75,7 @@ }, "dependencies": { "@types/node": "^22.7.5", + "jaro-winkler": "^0.2.8", "vscode-nls": "^5.2.0", "vscode-nls-dev": "^4.0.4" } diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 764c60637ac..9b6c3dd50bd 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -828,6 +828,18 @@ "command": "aws.amazonq.clearCache", "title": "%AWS.amazonq.clearCache%", "category": "%AWS.amazonq.title%" + }, + { + "command": "aws.amazonq.inline.acceptEdit", + "title": "%aws.amazonq.inline.acceptEdit%" + }, + { + "command": "aws.amazonq.inline.rejectEdit", + "title": "%aws.amazonq.inline.rejectEdit%" + }, + { + "command": "aws.amazonq.toggleNextEditPredictionPanel", + "title": "%aws.amazonq.toggleNextEditPredictionPanel%" } ], "keybindings": [ @@ -837,6 +849,18 @@ "mac": "cmd+alt+i", "linux": "meta+alt+i" }, + { + "command": "aws.amazonq.inline.debugAcceptEdit", + "key": "ctrl+alt+a", + "mac": "cmd+alt+a", + "when": "editorTextFocus" + }, + { + "command": "aws.amazonq.inline.debugRejectEdit", + "key": "ctrl+alt+r", + "mac": "cmd+alt+r", + "when": "editorTextFocus" + }, { "command": "aws.amazonq.explainCode", "win": "win+alt+e", @@ -917,6 +941,16 @@ "command": "aws.amazonq.inline.waitForUserDecisionRejectAll", "key": "escape", "when": "editorTextFocus && aws.codewhisperer.connected && amazonq.inline.codelensShortcutEnabled" + }, + { + "command": "aws.amazonq.inline.acceptEdit", + "key": "tab", + "when": "editorTextFocus && aws.amazonq.editSuggestionActive" + }, + { + "command": "aws.amazonq.inline.rejectEdit", + "key": "escape", + "when": "editorTextFocus && aws.amazonq.editSuggestionActive" } ], "icons": { diff --git a/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts b/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts new file mode 100644 index 00000000000..24014d692ea --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts @@ -0,0 +1,142 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +// TODO: deprecate this file in favor of core/shared/utils/diffUtils +import { applyPatch } from 'diff' + +export type LineDiff = + | { type: 'added'; content: string } + | { type: 'removed'; content: string } + | { type: 'modified'; before: string; after: string } + +/** + * Apply a unified diff to original code to generate modified code + * @param originalCode The original code as a string + * @param unifiedDiff The unified diff content + * @returns The modified code after applying the diff + */ +export function applyUnifiedDiff( + docText: string, + unifiedDiff: string +): { appliedCode: string; addedCharacterCount: number; deletedCharacterCount: number } { + try { + const { addedCharacterCount, deletedCharacterCount } = getAddedAndDeletedCharCount(unifiedDiff) + // First try the standard diff package + try { + const result = applyPatch(docText, unifiedDiff) + if (result !== false) { + return { + appliedCode: result, + addedCharacterCount: addedCharacterCount, + deletedCharacterCount: deletedCharacterCount, + } + } + } catch (error) {} + + // Parse the unified diff to extract the changes + const diffLines = unifiedDiff.split('\n') + let result = docText + + // Find all hunks in the diff + const hunkStarts = diffLines + .map((line, index) => (line.startsWith('@@ ') ? index : -1)) + .filter((index) => index !== -1) + + // Process each hunk + for (const hunkStart of hunkStarts) { + // Parse the hunk header + const hunkHeader = diffLines[hunkStart] + const match = hunkHeader.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/) + + if (!match) { + continue + } + + const oldStart = parseInt(match[1]) + const oldLines = parseInt(match[2]) + + // Extract the content lines for this hunk + let i = hunkStart + 1 + const contentLines = [] + while (i < diffLines.length && !diffLines[i].startsWith('@@')) { + contentLines.push(diffLines[i]) + i++ + } + + // Build the old and new text + let oldText = '' + let newText = '' + + for (const line of contentLines) { + if (line.startsWith('-')) { + oldText += line.substring(1) + '\n' + } else if (line.startsWith('+')) { + newText += line.substring(1) + '\n' + } else if (line.startsWith(' ')) { + oldText += line.substring(1) + '\n' + newText += line.substring(1) + '\n' + } + } + + // Remove trailing newline if it was added + oldText = oldText.replace(/\n$/, '') + newText = newText.replace(/\n$/, '') + + // Find the text to replace in the document + const docLines = docText.split('\n') + const startLine = oldStart - 1 // Convert to 0-based + const endLine = startLine + oldLines + + // Extract the text that should be replaced + const textToReplace = docLines.slice(startLine, endLine).join('\n') + + // Replace the text + result = result.replace(textToReplace, newText) + } + return { + appliedCode: result, + addedCharacterCount: addedCharacterCount, + deletedCharacterCount: deletedCharacterCount, + } + } catch (error) { + return { + appliedCode: docText, // Return original text if all methods fail + addedCharacterCount: 0, + deletedCharacterCount: 0, + } + } +} + +export function getAddedAndDeletedCharCount(diff: string): { + addedCharacterCount: number + deletedCharacterCount: number +} { + let addedCharacterCount = 0 + let deletedCharacterCount = 0 + let i = 0 + const lines = diff.split('\n') + while (i < lines.length) { + const line = lines[i] + if (line.startsWith('+') && !line.startsWith('+++')) { + addedCharacterCount += line.length - 1 + } else if (line.startsWith('-') && !line.startsWith('---')) { + const removedLine = line.substring(1) + deletedCharacterCount += removedLine.length + + // Check if this is a modified line rather than a pure deletion + const nextLine = lines[i + 1] + if (nextLine && nextLine.startsWith('+') && !nextLine.startsWith('+++') && nextLine.includes(removedLine)) { + // This is a modified line, not a pure deletion + // We've already counted the deletion, so we'll just increment i to skip the next line + // since we'll process the addition on the next iteration + i += 1 + } + } + i += 1 + } + return { + addedCharacterCount, + deletedCharacterCount, + } +} diff --git a/packages/amazonq/src/app/inline/EditRendering/displayImage.ts b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts new file mode 100644 index 00000000000..80d5231f113 --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts @@ -0,0 +1,377 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger, setContext } from 'aws-core-vscode/shared' +import * as vscode from 'vscode' +import { diffLines } from 'diff' +import { LanguageClient } from 'vscode-languageclient' +import { CodeWhispererSession } from '../sessionManager' +import { LogInlineCompletionSessionResultsParams } from '@aws/language-server-runtimes/protocol' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' +import path from 'path' +import { imageVerticalOffset } from './svgGenerator' + +export class EditDecorationManager { + private imageDecorationType: vscode.TextEditorDecorationType + private removedCodeDecorationType: vscode.TextEditorDecorationType + private currentImageDecoration: vscode.DecorationOptions | undefined + private currentRemovedCodeDecorations: vscode.DecorationOptions[] = [] + private acceptHandler: (() => void) | undefined + private rejectHandler: (() => void) | undefined + + constructor() { + this.registerCommandHandlers() + this.imageDecorationType = vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + }) + + this.removedCodeDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 0, 0, 0.2)', + }) + } + + private imageToDecoration(image: vscode.Uri, range: vscode.Range) { + return { + range, + renderOptions: { + after: { + contentIconPath: image, + verticalAlign: 'text-top', + width: '100%', + height: 'auto', + margin: '1px 0', + }, + }, + hoverMessage: new vscode.MarkdownString('Edit suggestion. Press [Tab] to accept or [Esc] to reject.'), + } + } + + /** + * Highlights code that will be removed using the provided highlight ranges + * @param editor The active text editor + * @param startLine The line where the edit starts + * @param highlightRanges Array of ranges specifying which parts to highlight + * @returns Array of decoration options + */ + private highlightRemovedLines( + editor: vscode.TextEditor, + startLine: number, + highlightRanges: Array<{ line: number; start: number; end: number }> + ): vscode.DecorationOptions[] { + const decorations: vscode.DecorationOptions[] = [] + + // Group ranges by line for more efficient processing + const rangesByLine = new Map>() + + // Process each range and adjust line numbers relative to document + for (const range of highlightRanges) { + const documentLine = startLine + range.line + + // Skip if line is out of bounds + if (documentLine >= editor.document.lineCount) { + continue + } + + // Add to ranges map, grouped by line + if (!rangesByLine.has(documentLine)) { + rangesByLine.set(documentLine, []) + } + rangesByLine.get(documentLine)!.push({ + start: range.start, + end: range.end, + }) + } + + // Process each line with ranges + for (const [lineNumber, ranges] of rangesByLine.entries()) { + const lineLength = editor.document.lineAt(lineNumber).text.length + + if (ranges.length === 0) { + continue + } + + // Check if we should highlight the entire line + if (ranges.length === 1 && ranges[0].start === 0 && ranges[0].end >= lineLength) { + // Highlight entire line + const range = new vscode.Range( + new vscode.Position(lineNumber, 0), + new vscode.Position(lineNumber, lineLength) + ) + decorations.push({ range }) + } else { + // Create individual decorations for each range on this line + for (const range of ranges) { + const end = Math.min(range.end, lineLength) + if (range.start < end) { + const vsRange = new vscode.Range( + new vscode.Position(lineNumber, range.start), + new vscode.Position(lineNumber, end) + ) + decorations.push({ range: vsRange }) + } + } + } + } + + return decorations + } + + /** + * Displays an edit suggestion as an SVG image in the editor and highlights removed code + */ + public displayEditSuggestion( + editor: vscode.TextEditor, + svgImage: vscode.Uri, + startLine: number, + onAccept: () => void, + onReject: () => void, + originalCode: string, + newCode: string, + originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }> + ): void { + this.clearDecorations(editor) + + void setContext('aws.amazonq.editSuggestionActive' as any, true) + + this.acceptHandler = onAccept + this.rejectHandler = onReject + + // Get the line text to determine the end position + const lineText = editor.document.lineAt(Math.max(0, startLine - imageVerticalOffset)).text + const endPosition = new vscode.Position(Math.max(0, startLine - imageVerticalOffset), lineText.length) + const range = new vscode.Range(endPosition, endPosition) + + this.currentImageDecoration = this.imageToDecoration(svgImage, range) + + // Apply image decoration + editor.setDecorations(this.imageDecorationType, [this.currentImageDecoration]) + + // Highlight removed code with red background + this.currentRemovedCodeDecorations = this.highlightRemovedLines(editor, startLine, originalCodeHighlightRanges) + editor.setDecorations(this.removedCodeDecorationType, this.currentRemovedCodeDecorations) + } + + /** + * Clears all edit suggestion decorations + */ + public clearDecorations(editor: vscode.TextEditor): void { + editor.setDecorations(this.imageDecorationType, []) + editor.setDecorations(this.removedCodeDecorationType, []) + this.currentImageDecoration = undefined + this.currentRemovedCodeDecorations = [] + this.acceptHandler = undefined + this.rejectHandler = undefined + void setContext('aws.amazonq.editSuggestionActive' as any, false) + } + + /** + * Registers command handlers for accepting/rejecting suggestions + */ + public registerCommandHandlers(): void { + // Register Tab key handler for accepting suggestion + vscode.commands.registerCommand('aws.amazonq.inline.acceptEdit', () => { + if (this.acceptHandler) { + this.acceptHandler() + } + }) + + // Register Esc key handler for rejecting suggestion + vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', () => { + if (this.rejectHandler) { + this.rejectHandler() + } + }) + } + + /** + * Disposes resources + */ + public dispose(): void { + this.imageDecorationType.dispose() + this.removedCodeDecorationType.dispose() + } + + // Use process-wide singleton to prevent multiple instances on Windows + static readonly decorationManagerKey = Symbol.for('aws.amazonq.decorationManager') + + static getDecorationManager(): EditDecorationManager { + const globalObj = global as any + if (!globalObj[this.decorationManagerKey]) { + globalObj[this.decorationManagerKey] = new EditDecorationManager() + } + return globalObj[this.decorationManagerKey] + } +} + +export const decorationManager = EditDecorationManager.getDecorationManager() + +/** + * Function to replace editor's content with new code + */ +function replaceEditorContent(editor: vscode.TextEditor, newCode: string): void { + const document = editor.document + const fullRange = new vscode.Range( + 0, + 0, + document.lineCount - 1, + document.lineAt(document.lineCount - 1).text.length + ) + + void editor.edit((editBuilder) => { + editBuilder.replace(fullRange, newCode) + }) +} + +/** + * Calculates the end position of the actual edited content by finding the last changed part + */ +function getEndOfEditPosition(originalCode: string, newCode: string): vscode.Position { + const changes = diffLines(originalCode, newCode) + let lineOffset = 0 + + // Track the end position of the last added chunk + let lastChangeEndLine = 0 + let lastChangeEndColumn = 0 + let foundAddedContent = false + + for (const part of changes) { + if (part.added) { + foundAddedContent = true + + // Calculate lines in this added part + const lines = part.value.split('\n') + const linesCount = lines.length + + // Update position to the end of this added chunk + lastChangeEndLine = lineOffset + linesCount - 1 + + // Get the length of the last line in this added chunk + lastChangeEndColumn = lines[linesCount - 1].length + } + + // Update line offset (skip removed parts) + if (!part.removed) { + const partLineCount = part.value.split('\n').length + lineOffset += partLineCount - 1 + } + } + + // If we found added content, return position at the end of the last addition + if (foundAddedContent) { + return new vscode.Position(lastChangeEndLine, lastChangeEndColumn) + } + + // Fallback to current cursor position if no changes were found + const editor = vscode.window.activeTextEditor + return editor ? editor.selection.active : new vscode.Position(0, 0) +} + +/** + * Helper function to display SVG decorations + */ +export async function displaySvgDecoration( + editor: vscode.TextEditor, + svgImage: vscode.Uri, + startLine: number, + newCode: string, + originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }>, + session: CodeWhispererSession, + languageClient: LanguageClient, + item: InlineCompletionItemWithReferences, + addedCharacterCount: number, + deletedCharacterCount: number +) { + const originalCode = editor.document.getText() + + decorationManager.displayEditSuggestion( + editor, + svgImage, + startLine, + () => { + // Handle accept + getLogger().info('Edit suggestion accepted') + + // Replace content + replaceEditorContent(editor, newCode) + + // Move cursor to end of the actual changed content + const endPosition = getEndOfEditPosition(originalCode, newCode) + editor.selection = new vscode.Selection(endPosition, endPosition) + + // Move cursor to end of the actual changed content + editor.selection = new vscode.Selection(endPosition, endPosition) + + decorationManager.clearDecorations(editor) + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [item.itemId]: { + seen: true, + accepted: true, + discarded: false, + }, + }, + totalSessionDisplayTime: Date.now() - session.requestStartTime, + firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, + addedCharacterCount: addedCharacterCount, + deletedCharacterCount: deletedCharacterCount, + } + languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + }, + () => { + // Handle reject + getLogger().info('Edit suggestion rejected') + decorationManager.clearDecorations(editor) + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [item.itemId]: { + seen: true, + accepted: false, + discarded: false, + }, + }, + // addedCharacterCount: addedCharacterCount, + // deletedCharacterCount: deletedCharacterCount, + } + languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + }, + originalCode, + newCode, + originalCodeHighlightRanges + ) +} + +export function deactivate() { + decorationManager.dispose() +} + +let decorationType: vscode.TextEditorDecorationType | undefined + +export function decorateLinesWithGutterIcon(lineNumbers: number[]) { + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + + // Dispose previous decoration if it exists + if (decorationType) { + decorationType.dispose() + } + + // Create a new gutter decoration with a small green dot + decorationType = vscode.window.createTextEditorDecorationType({ + gutterIconPath: vscode.Uri.file( + path.join(__dirname, 'media', 'green-dot.svg') // put your svg file in a `media` folder + ), + gutterIconSize: 'contain', + }) + + const decorations: vscode.DecorationOptions[] = lineNumbers.map((line) => ({ + range: new vscode.Range(new vscode.Position(line, 0), new vscode.Position(line, 0)), + })) + + editor.setDecorations(decorationType, decorations) +} diff --git a/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts new file mode 100644 index 00000000000..2dd6bd67712 --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts @@ -0,0 +1,56 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { displaySvgDecoration } from './displayImage' +import { SvgGenerationService } from './svgGenerator' +import { getLogger } from 'aws-core-vscode/shared' +import { LanguageClient } from 'vscode-languageclient' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' +import { CodeWhispererSession } from '../sessionManager' + +export async function showEdits( + item: InlineCompletionItemWithReferences, + editor: vscode.TextEditor | undefined, + session: CodeWhispererSession, + languageClient: LanguageClient +) { + if (!editor) { + return + } + try { + const svgGenerationService = new SvgGenerationService() + // Generate your SVG image with the file contents + const currentFile = editor.document.uri.fsPath + const { + svgImage, + startLine, + newCode, + origionalCodeHighlightRange, + addedCharacterCount, + deletedCharacterCount, + } = await svgGenerationService.generateDiffSvg(currentFile, item.insertText as string) + + if (svgImage) { + // display the SVG image + await displaySvgDecoration( + editor, + svgImage, + startLine, + newCode, + origionalCodeHighlightRange, + session, + languageClient, + item, + addedCharacterCount, + deletedCharacterCount + ) + } else { + getLogger('nextEditPrediction').error('SVG image generation returned an empty result.') + } + } catch (error) { + getLogger('nextEditPrediction').error(`Error generating SVG image: ${error}`) + } +} diff --git a/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts new file mode 100644 index 00000000000..8c7a9d57fd9 --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts @@ -0,0 +1,548 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { diffChars } from 'diff' +import * as vscode from 'vscode' +import { ToolkitError, getLogger } from 'aws-core-vscode/shared' +import { diffUtilities } from 'aws-core-vscode/shared' +import { applyUnifiedDiff } from './diffUtils' +type Range = { line: number; start: number; end: number } + +const logger = getLogger('nextEditPrediction') +export const imageVerticalOffset = 1 + +export class SvgGenerationService { + /** + * Generates an SVG image representing a code diff + * @param originalCode The original code + * @param newCode The new code with editsss + * @param theme The editor theme information + * @param offSet The margin to add to the left of the image + */ + public async generateDiffSvg( + filePath: string, + udiff: string + ): Promise<{ + svgImage: vscode.Uri + startLine: number + newCode: string + origionalCodeHighlightRange: Range[] + addedCharacterCount: number + deletedCharacterCount: number + }> { + const textDoc = await vscode.workspace.openTextDocument(filePath) + const originalCode = textDoc.getText() + if (originalCode === '') { + logger.error(`udiff format error`) + throw new ToolkitError('udiff format error') + } + const { addedCharacterCount, deletedCharacterCount } = applyUnifiedDiff(originalCode, udiff) + const newCode = await diffUtilities.getPatchedCode(filePath, udiff) + const modifiedLines = diffUtilities.getModifiedLinesFromUnifiedDiff(udiff) + // TODO remove + // eslint-disable-next-line aws-toolkits/no-json-stringify-in-log + logger.info(`Line mapping: ${JSON.stringify(modifiedLines)}`) + + const { createSVGWindow } = await import('svgdom') + + const svgjs = await import('@svgdotjs/svg.js') + const SVG = svgjs.SVG + const registerWindow = svgjs.registerWindow + + // Get editor theme info + const currentTheme = this.getEditorTheme() + + // Get edit diffs with highlight + const { addedLines, removedLines } = this.getEditedLinesFromDiff(udiff) + const highlightRanges = this.generateHighlightRanges(removedLines, addedLines, modifiedLines) + const diffAddedWithHighlight = this.getHighlightEdit(addedLines, highlightRanges.addedRanges) + + // Create SVG window, document, and container + const window = createSVGWindow() + const document = window.document + registerWindow(window, document) + const draw = SVG(document.documentElement) as any + + // Calculate dimensions based on code content + const { offset, editStartLine } = this.calculatePosition( + originalCode.split('\n'), + newCode.split('\n'), + addedLines, + currentTheme + ) + const { width, height } = this.calculateDimensions(addedLines, currentTheme) + draw.size(width + offset, height) + + // Generate CSS for syntax highlighting HTML content based on theme + const styles = this.generateStyles(currentTheme) + const htmlContent = this.generateHtmlContent(diffAddedWithHighlight, styles, offset) + + // Create foreignObject to embed HTML + const foreignObject = draw.foreignObject(width + offset, height) + foreignObject.node.innerHTML = htmlContent.trim() + + const svgData = draw.svg() + const svgResult = `data:image/svg+xml;base64,${Buffer.from(svgData).toString('base64')}` + + return { + svgImage: vscode.Uri.parse(svgResult), + startLine: editStartLine, + newCode: newCode, + origionalCodeHighlightRange: highlightRanges.removedRanges, + addedCharacterCount, + deletedCharacterCount, + } + } + + private calculateDimensions(newLines: string[], currentTheme: editorThemeInfo): { width: number; height: number } { + // Calculate appropriate width and height based on diff content + const maxLineLength = Math.max(...newLines.map((line) => line.length)) + + const headerFrontSize = Math.ceil(currentTheme.fontSize * 0.66) + + // Estimate width based on character count and font size + const width = Math.max(41 * headerFrontSize * 0.7, maxLineLength * currentTheme.fontSize * 0.7) + + // Calculate height based on diff line count and line height + const totalLines = newLines.length + 1 // +1 for header + const height = totalLines * currentTheme.lingHeight + 25 // +25 for padding + + return { width, height } + } + + private generateStyles(theme: editorThemeInfo): string { + // Generate CSS styles based on editor theme + const fontSize = theme.fontSize + const headerFrontSize = Math.ceil(fontSize * 0.66) + const lineHeight = theme.lingHeight + const foreground = theme.foreground + const bordeColor = 'rgba(212, 212, 212, 0.5)' + const background = theme.background || '#1e1e1e' + const diffRemoved = theme.diffRemoved || 'rgba(255, 0, 0, 0.2)' + const diffAdded = 'rgba(72, 128, 72, 0.52)' + return ` + .code-container { + font-family: ${'monospace'}; + color: ${foreground}; + font-size: ${fontSize}px; + line-height: ${lineHeight}px; + background-color: ${background}; + border: 1px solid ${bordeColor}; + border-radius: 0px; + padding-top: 3px; + padding-bottom: 5px; + padding-left: 10px; + } + .diff-header { + color: ${theme.foreground || '#d4d4d4'}; + margin: 0; + font-size: ${headerFrontSize}px; + padding: 0px; + } + .diff-removed { + background-color: ${diffRemoved}; + white-space: pre-wrap; /* Preserve whitespace */ + text-decoration: line-through; + opacity: 0.7; + } + .diff-changed { + white-space: pre-wrap; /* Preserve whitespace */ + background-color: ${diffAdded}; + } + ` + } + + private generateHtmlContent(diffLines: string[], styles: string, offSet: number): string { + return ` +

+ +
+
Q: Press [Tab] to accept or [Esc] to reject:
+ ${diffLines.map((line) => `
${line}
`).join('')} +
+
+ ` + } + + /** + * Extract added and removed lines from the unified diff + * @param unifiedDiff The unified diff string + * @returns Object containing arrays of added and removed lines + */ + private getEditedLinesFromDiff(unifiedDiff: string): { addedLines: string[]; removedLines: string[] } { + const addedLines: string[] = [] + const removedLines: string[] = [] + const diffLines = unifiedDiff.split('\n') + + // Find all hunks in the diff + const hunkStarts = diffLines + .map((line, index) => (line.startsWith('@@ ') ? index : -1)) + .filter((index) => index !== -1) + + // Process each hunk to find added and removed lines + for (const hunkStart of hunkStarts) { + const hunkHeader = diffLines[hunkStart] + const match = hunkHeader.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/) + + if (!match) { + continue + } + + // Extract the content lines for this hunk + let i = hunkStart + 1 + while (i < diffLines.length && !diffLines[i].startsWith('@@')) { + // Include lines that were added (start with '+') + if (diffLines[i].startsWith('+') && !diffLines[i].startsWith('+++')) { + const lineContent = diffLines[i].substring(1) + addedLines.push(lineContent) + } + // Include lines that were removed (start with '-') + else if (diffLines[i].startsWith('-') && !diffLines[i].startsWith('---')) { + const lineContent = diffLines[i].substring(1) + removedLines.push(lineContent) + } + i++ + } + } + + return { addedLines, removedLines } + } + + /** + * Applies highlighting to code lines based on the specified ranges + * @param newLines Array of code lines to highlight + * @param highlightRanges Array of ranges specifying which parts of the lines to highlight + * @returns Array of HTML strings with appropriate spans for highlighting + */ + private getHighlightEdit(newLines: string[], highlightRanges: Range[]): string[] { + const result: string[] = [] + + // Group ranges by line for easier lookup + const rangesByLine = new Map() + for (const range of highlightRanges) { + if (!rangesByLine.has(range.line)) { + rangesByLine.set(range.line, []) + } + rangesByLine.get(range.line)!.push(range) + } + + // Process each line of code + for (let lineIndex = 0; lineIndex < newLines.length; lineIndex++) { + const line = newLines[lineIndex] + // Get ranges for this line + const lineRanges = rangesByLine.get(lineIndex) || [] + + // If no ranges for this line, leave it as-is with HTML escaping + if (lineRanges.length === 0) { + result.push(this.escapeHtml(line)) + continue + } + + // Sort ranges by start position to ensure correct ordering + lineRanges.sort((a, b) => a.start - b.start) + + // Build the highlighted line + let highlightedLine = '' + let currentPos = 0 + + for (const range of lineRanges) { + // Add text before the current range (with HTML escaping) + if (range.start > currentPos) { + const beforeText = line.substring(currentPos, range.start) + highlightedLine += this.escapeHtml(beforeText) + } + + // Add the highlighted part (with HTML escaping) + const highlightedText = line.substring(range.start, range.end) + highlightedLine += `${this.escapeHtml(highlightedText)}` + + // Update current position + currentPos = range.end + } + + // Add any remaining text after the last range (with HTML escaping) + if (currentPos < line.length) { + const afterText = line.substring(currentPos) + highlightedLine += this.escapeHtml(afterText) + } + + result.push(highlightedLine) + } + + return result + } + + private getEditorTheme(): editorThemeInfo { + const editorConfig = vscode.workspace.getConfiguration('editor') + const fontSize = editorConfig.get('fontSize', 12) // Default to 12 if not set + const lineHeightSetting = editorConfig.get('lineHeight', 0) // Default to 0 if not set + + /** + * Calculate effective line height, documented as such: + * Use 0 to automatically compute the line height from the font size. + * Values between 0 and 8 will be used as a multiplier with the font size. + * Values greater than or equal to 8 will be used as effective values. + */ + let effectiveLineHeight: number + if (lineHeightSetting > 0 && lineHeightSetting < 8) { + effectiveLineHeight = lineHeightSetting * fontSize + } else if (lineHeightSetting >= 8) { + effectiveLineHeight = lineHeightSetting + } else { + effectiveLineHeight = Math.round(1.5 * fontSize) + } + + const themeName = vscode.workspace.getConfiguration('workbench').get('colorTheme', 'Default') + const themeColors = this.getThemeColors(themeName) + + return { + fontSize: fontSize, + lingHeight: effectiveLineHeight, + ...themeColors, + } + } + + private getThemeColors(themeName: string): { + foreground: string + background: string + diffAdded: string + diffRemoved: string + } { + // Define default dark theme colors + const darkThemeColors = { + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + diffAdded: 'rgba(231, 245, 231, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.2)', + } + + // Define default light theme colors + const lightThemeColors = { + foreground: 'rgba(0, 0, 0, 1)', + background: 'rgba(255, 255, 255, 1)', + diffAdded: 'rgba(198, 239, 206, 0.2)', + diffRemoved: 'rgba(255, 199, 206, 0.5)', + } + + // For dark and light modes + const themeNameLower = themeName.toLowerCase() + + if (themeNameLower.includes('dark')) { + return darkThemeColors + } else if (themeNameLower.includes('light')) { + return lightThemeColors + } + + // Define colors for specific themes, add more if needed. + const themeColorMap: { + [key: string]: { foreground: string; background: string; diffAdded: string; diffRemoved: string } + } = { + Abyss: { + foreground: 'rgba(255, 255, 255, 1)', + background: 'rgba(0, 12, 24, 1)', + diffAdded: 'rgba(0, 255, 0, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.3)', + }, + Red: { + foreground: 'rgba(255, 0, 0, 1)', + background: 'rgba(51, 0, 0, 1)', + diffAdded: 'rgba(255, 100, 100, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.5)', + }, + } + + // Return colors for the specific theme or default to light theme + return themeColorMap[themeName] || lightThemeColors + } + + private calculatePosition( + originalLines: string[], + newLines: string[], + diffLines: string[], + theme: editorThemeInfo + ): { offset: number; editStartLine: number } { + // Determine the starting line of the edit in the original file + let editStartLineInOldFile = 0 + const maxLength = Math.min(originalLines.length, newLines.length) + + for (let i = 0; i <= maxLength; i++) { + if (originalLines[i] !== newLines[i] || i === maxLength) { + editStartLineInOldFile = i + break + } + } + const shiftedStartLine = Math.max(0, editStartLineInOldFile - imageVerticalOffset) + + // Determine the range to consider + const startLine = shiftedStartLine + const endLine = Math.min(editStartLineInOldFile + diffLines.length, originalLines.length) + + // Find the longest line within the specified range + let maxLineLength = 0 + for (let i = startLine; i <= endLine; i++) { + const lineLength = originalLines[i]?.length || 0 + if (lineLength > maxLineLength) { + maxLineLength = lineLength + } + } + + // Calculate the offset based on the longest line and the starting line length + const startLineLength = originalLines[startLine]?.length || 0 + const offset = (maxLineLength - startLineLength) * theme.fontSize * 0.7 + 10 // padding + + return { offset, editStartLine: editStartLineInOldFile } + } + + private escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } + + /** + * Generates character-level highlight ranges for both original and modified code. + * @param originalCode Array of original code lines + * @param afterCode Array of code lines after modification + * @param modifiedLines Map of original lines to modified lines + * @returns Object containing ranges for original and after code character level highlighting + */ + private generateHighlightRanges( + originalCode: string[], + afterCode: string[], + modifiedLines: Map + ): { removedRanges: Range[]; addedRanges: Range[] } { + const originalRanges: Range[] = [] + const afterRanges: Range[] = [] + + /** + * Merges ranges on the same line that are separated by only one character + */ + const mergeAdjacentRanges = (ranges: Range[]): Range[] => { + const sortedRanges = [...ranges].sort((a, b) => { + if (a.line !== b.line) { + return a.line - b.line + } + return a.start - b.start + }) + + const result: Range[] = [] + + // Process all ranges + for (let i = 0; i < sortedRanges.length; i++) { + const current = sortedRanges[i] + + // If this is the last range or ranges are on different lines, add it directly + if (i === sortedRanges.length - 1 || current.line !== sortedRanges[i + 1].line) { + result.push(current) + continue + } + + // Check if current range and next range can be merged + const next = sortedRanges[i + 1] + if (current.line === next.line && next.start - current.end <= 1) { + sortedRanges[i + 1] = { + line: current.line, + start: current.start, + end: Math.max(current.end, next.end), + } + } else { + result.push(current) + } + } + + return result + } + + // Create reverse mapping for quicker lookups + const reverseMap = new Map() + for (const [original, modified] of modifiedLines.entries()) { + reverseMap.set(modified, original) + } + + // Process original code lines - produces highlight ranges in current editor text + for (let lineIndex = 0; lineIndex < originalCode.length; lineIndex++) { + const line = originalCode[lineIndex] + + // If line exists in modifiedLines as a key, process character diffs + if (Array.from(modifiedLines.keys()).includes(line)) { + const modifiedLine = modifiedLines.get(line)! + const changes = diffChars(line, modifiedLine) + + let charPos = 0 + for (const part of changes) { + if (part.removed) { + originalRanges.push({ + line: lineIndex, + start: charPos, + end: charPos + part.value.length, + }) + } + + if (!part.added) { + charPos += part.value.length + } + } + } else { + // Line doesn't exist in modifiedLines values, highlight entire line + originalRanges.push({ + line: lineIndex, + start: 0, + end: line.length, + }) + } + } + + // Process after code lines - used for highlight in SVG image + for (let lineIndex = 0; lineIndex < afterCode.length; lineIndex++) { + const line = afterCode[lineIndex] + + if (reverseMap.has(line)) { + const originalLine = reverseMap.get(line)! + const changes = diffChars(originalLine, line) + + let charPos = 0 + for (const part of changes) { + if (part.added) { + afterRanges.push({ + line: lineIndex, + start: charPos, + end: charPos + part.value.length, + }) + } + + if (!part.removed) { + charPos += part.value.length + } + } + } else { + afterRanges.push({ + line: lineIndex, + start: 0, + end: line.length, + }) + } + } + + const mergedOriginalRanges = mergeAdjacentRanges(originalRanges) + const mergedAfterRanges = mergeAdjacentRanges(afterRanges) + + return { + removedRanges: mergedOriginalRanges, + addedRanges: mergedAfterRanges, + } + } +} + +interface editorThemeInfo { + fontSize: number + lingHeight: number + foreground?: string + background?: string + diffAdded?: string + diffRemoved?: string +} diff --git a/packages/amazonq/src/app/inline/activation.ts b/packages/amazonq/src/app/inline/activation.ts index 69515127441..867ae95d9b5 100644 --- a/packages/amazonq/src/app/inline/activation.ts +++ b/packages/amazonq/src/app/inline/activation.ts @@ -17,6 +17,9 @@ import { globals, sleep } from 'aws-core-vscode/shared' export async function activate() { if (isInlineCompletionEnabled()) { + // Debugging purpose: only initialize NextEditPredictionPanel when development + // NextEditPredictionPanel.getInstance() + await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() } diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 1e0716097bb..069a6bc5128 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -24,7 +24,7 @@ import { LogInlineCompletionSessionResultsParams, } from '@aws/language-server-runtimes/protocol' import { SessionManager } from './sessionManager' -import { RecommendationService } from './recommendationService' +import { GetAllRecommendationsOptions, RecommendationService } from './recommendationService' import { CodeWhispererConstants, ReferenceHoverProvider, @@ -40,8 +40,10 @@ import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' import { TelemetryHelper } from './telemetryHelper' -import { getLogger } from 'aws-core-vscode/shared' +import { Experiments, getLogger } from 'aws-core-vscode/shared' import { debounce, messageUtils } from 'aws-core-vscode/utils' +import { showEdits } from './EditRendering/imageRenderer' +import { ICursorUpdateRecorder } from './cursorUpdateManager' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -58,13 +60,18 @@ export class InlineCompletionManager implements Disposable { languageClient: LanguageClient, sessionManager: SessionManager, lineTracker: LineTracker, - inlineTutorialAnnotation: InlineTutorialAnnotation + inlineTutorialAnnotation: InlineTutorialAnnotation, + cursorUpdateRecorder?: ICursorUpdateRecorder ) { this.languageClient = languageClient this.sessionManager = sessionManager this.lineTracker = lineTracker this.incomingGeneratingMessage = new InlineGeneratingMessage(this.lineTracker) - this.recommendationService = new RecommendationService(this.sessionManager, this.incomingGeneratingMessage) + this.recommendationService = new RecommendationService( + this.sessionManager, + this.incomingGeneratingMessage, + cursorUpdateRecorder + ) this.inlineTutorialAnnotation = inlineTutorialAnnotation this.inlineCompletionProvider = new AmazonQInlineCompletionItemProvider( languageClient, @@ -80,6 +87,10 @@ export class InlineCompletionManager implements Disposable { this.lineTracker.ready() } + public getInlineCompletionProvider(): AmazonQInlineCompletionItemProvider { + return this.inlineCompletionProvider + } + public dispose(): void { if (this.disposable) { this.disposable.dispose() @@ -176,6 +187,7 @@ export class InlineCompletionManager implements Disposable { } export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider { + private logger = getLogger('nextEditPrediction') constructor( private readonly languageClient: LanguageClient, private readonly recommendationService: RecommendationService, @@ -194,13 +206,24 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem document: TextDocument, position: Position, context: InlineCompletionContext, - token: CancellationToken + token: CancellationToken, + getAllRecommendationsOptions?: GetAllRecommendationsOptions ): Promise { + getLogger().info('_provideInlineCompletionItems called with: %O', { + documentUri: document.uri.toString(), + position, + context, + triggerKind: context.triggerKind === InlineCompletionTriggerKind.Automatic ? 'Automatic' : 'Invoke', + }) + // prevent concurrent API calls and write to shared state variables if (vsCodeState.isRecommendationsActive) { + getLogger().info('Recommendations already active, returning empty') return [] } + let logstr = `GenerateCompletion metadata:\\n` try { + const t0 = performance.now() vsCodeState.isRecommendationsActive = true const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { @@ -257,21 +280,38 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem this.sessionManager.clear() } + // TODO: this line will take ~200ms each trigger, need to root cause and maybe better to disable it for now // tell the tutorial that completions has been triggered await this.inlineTutorialAnnotation.triggered(context.triggerKind) + TelemetryHelper.instance.setInvokeSuggestionStartTime() TelemetryHelper.instance.setTriggerType(context.triggerKind) + const t1 = performance.now() + await this.recommendationService.getAllRecommendations( this.languageClient, document, position, context, - token + token, + getAllRecommendationsOptions ) // get active item from session for displaying const items = this.sessionManager.getActiveRecommendation() const itemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId + + // eslint-disable-next-line @typescript-eslint/no-base-to-string + const itemLog = items[0] ? `${items[0].insertText.toString()}` : `no suggestion` + + const t2 = performance.now() + + logstr = logstr += `- number of suggestions: ${items.length} +- first suggestion content (next line): +${itemLog} +- duration since trigger to before sending Flare call: ${t1 - t0}ms +- duration since trigger to receiving responses from Flare: ${t2 - t0}ms +` const session = this.sessionManager.getActiveSession() // Show message to user when manual invoke fails to produce results. @@ -311,6 +351,18 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem const itemsMatchingTypeahead = [] for (const item of items) { + if (item.isInlineEdit) { + // Check if Next Edit Prediction feature flag is enabled + if (Experiments.instance.isExperimentEnabled('amazonqLSPNEP')) { + void showEdits(item, editor, session, this.languageClient).then(() => { + const t3 = performance.now() + logstr = logstr + `- duration since trigger to NEP suggestion is displayed: ${t3 - t0}ms` + this.logger.info(logstr) + }) + } + return [] + } + item.insertText = typeof item.insertText === 'string' ? item.insertText : item.insertText.value if (item.insertText.startsWith(typeahead)) { item.command = { diff --git a/packages/amazonq/src/app/inline/cursorUpdateManager.ts b/packages/amazonq/src/app/inline/cursorUpdateManager.ts new file mode 100644 index 00000000000..4d9b28a35d8 --- /dev/null +++ b/packages/amazonq/src/app/inline/cursorUpdateManager.ts @@ -0,0 +1,190 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { LanguageClient } from 'vscode-languageclient' +import { getLogger } from 'aws-core-vscode/shared' +import { globals } from 'aws-core-vscode/shared' +import { AmazonQInlineCompletionItemProvider } from './completion' + +// Configuration section for cursor updates +export const cursorUpdateConfigurationSection = 'aws.q.cursorUpdate' + +/** + * Interface for recording completion requests + */ +export interface ICursorUpdateRecorder { + recordCompletionRequest(): void +} + +/** + * Manages periodic cursor position updates for Next Edit Prediction + */ +export class CursorUpdateManager implements vscode.Disposable, ICursorUpdateRecorder { + private readonly logger = getLogger('amazonqLsp') + private updateIntervalMs = 250 + private updateTimer?: NodeJS.Timeout + private lastPosition?: vscode.Position + private lastDocumentUri?: string + private lastSentPosition?: vscode.Position + private lastSentDocumentUri?: string + private isActive = false + private lastRequestTime = 0 + + constructor( + private readonly languageClient: LanguageClient, + private readonly inlineCompletionProvider?: AmazonQInlineCompletionItemProvider + ) {} + + /** + * Start tracking cursor positions and sending periodic updates + */ + public async start(): Promise { + if (this.isActive) { + return + } + + // Request configuration from server + try { + const config = await this.languageClient.sendRequest('aws/getConfigurationFromServer', { + section: cursorUpdateConfigurationSection, + }) + + if ( + config && + typeof config === 'object' && + 'intervalMs' in config && + typeof config.intervalMs === 'number' && + config.intervalMs > 0 + ) { + this.updateIntervalMs = config.intervalMs + } + } catch (error) { + this.logger.warn(`Failed to get cursor update configuration from server: ${error}`) + } + + this.isActive = true + this.setupUpdateTimer() + } + + /** + * Stop tracking cursor positions and sending updates + */ + public stop(): void { + this.isActive = false + this.clearUpdateTimer() + } + + /** + * Update the current cursor position + */ + public updatePosition(position: vscode.Position, documentUri: string): void { + // If the document changed, set the last sent position to the current position + // This prevents triggering an immediate recommendation when switching tabs + if (this.lastDocumentUri !== documentUri) { + this.lastSentPosition = position.with() // Create a copy + this.lastSentDocumentUri = documentUri + } + + this.lastPosition = position.with() // Create a copy + this.lastDocumentUri = documentUri + } + + /** + * Record that a regular InlineCompletionWithReferences request was made + * This will prevent cursor updates from being sent for the update interval + */ + public recordCompletionRequest(): void { + this.lastRequestTime = globals.clock.Date.now() + } + + /** + * Set up the timer for periodic cursor position updates + */ + private setupUpdateTimer(): void { + this.clearUpdateTimer() + + this.updateTimer = globals.clock.setInterval(async () => { + await this.sendCursorUpdate() + }, this.updateIntervalMs) + } + + /** + * Clear the update timer + */ + private clearUpdateTimer(): void { + if (this.updateTimer) { + globals.clock.clearInterval(this.updateTimer) + this.updateTimer = undefined + } + } + + /** + * Creates a cancellation token source + * This method exists to make testing easier by allowing it to be stubbed + */ + private createCancellationTokenSource(): vscode.CancellationTokenSource { + return new vscode.CancellationTokenSource() + } + + /** + * Send a cursor position update to the language server + */ + private async sendCursorUpdate(): Promise { + // Don't send an update if a regular request was made recently + const now = globals.clock.Date.now() + if (now - this.lastRequestTime < this.updateIntervalMs) { + return + } + + const editor = vscode.window.activeTextEditor + if (!editor || editor.document.uri.toString() !== this.lastDocumentUri) { + return + } + + // Don't send an update if the position hasn't changed since the last update + if ( + this.lastSentPosition && + this.lastPosition && + this.lastSentDocumentUri === this.lastDocumentUri && + this.lastSentPosition.line === this.lastPosition.line && + this.lastSentPosition.character === this.lastPosition.character + ) { + return + } + + // Only proceed if we have a valid position and provider + if (this.lastPosition && this.inlineCompletionProvider) { + const position = this.lastPosition.with() // Create a copy + + // Call the inline completion provider instead of directly calling getAllRecommendations + try { + await this.inlineCompletionProvider.provideInlineCompletionItems( + editor.document, + position, + { + triggerKind: vscode.InlineCompletionTriggerKind.Automatic, + selectedCompletionInfo: undefined, + }, + this.createCancellationTokenSource().token, + { emitTelemetry: false, showUi: false } + ) + + // Only update the last sent position after successfully sending the request + this.lastSentPosition = position + this.lastSentDocumentUri = this.lastDocumentUri + } catch (error) { + this.logger.error(`Error sending cursor update: ${error}`) + } + } + } + + /** + * Dispose of resources + */ + public dispose(): void { + this.stop() + } +} diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index eab2fc874b8..1b121da9047 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -14,20 +14,39 @@ import { SessionManager } from './sessionManager' import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' import { TelemetryHelper } from './telemetryHelper' +import { ICursorUpdateRecorder } from './cursorUpdateManager' +import { globals, getLogger } from 'aws-core-vscode/shared' + +export interface GetAllRecommendationsOptions { + emitTelemetry?: boolean + showUi?: boolean +} export class RecommendationService { constructor( private readonly sessionManager: SessionManager, - private readonly inlineGeneratingMessage: InlineGeneratingMessage + private readonly inlineGeneratingMessage: InlineGeneratingMessage, + private cursorUpdateRecorder?: ICursorUpdateRecorder ) {} + /** + * Set the recommendation service + */ + public setCursorUpdateRecorder(recorder: ICursorUpdateRecorder): void { + this.cursorUpdateRecorder = recorder + } + async getAllRecommendations( languageClient: LanguageClient, document: TextDocument, position: Position, context: InlineCompletionContext, - token: CancellationToken + token: CancellationToken, + options: GetAllRecommendationsOptions = { emitTelemetry: true, showUi: true } ) { + // Record that a regular request is being made + this.cursorUpdateRecorder?.recordCompletionRequest() + const request: InlineCompletionWithReferencesParams = { textDocument: { uri: document.uri.toString(), @@ -35,82 +54,92 @@ export class RecommendationService { position, context, } - const requestStartTime = Date.now() + const requestStartTime = globals.clock.Date.now() const statusBar = CodeWhispererStatusBarManager.instance + + // Only track telemetry if enabled TelemetryHelper.instance.setInvokeSuggestionStartTime() TelemetryHelper.instance.setPreprocessEndTime() TelemetryHelper.instance.setSdkApiCallStartTime() try { - // Show UI indicators that we are generating suggestions - await this.inlineGeneratingMessage.showGenerating(context.triggerKind) - await statusBar.setLoading() + // Show UI indicators only if UI is enabled + if (options.showUi) { + await this.inlineGeneratingMessage.showGenerating(context.triggerKind) + await statusBar.setLoading() + } // Handle first request - const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( + getLogger().info('Sending inline completion request: %O', { + method: inlineCompletionWithReferencesRequestType.method, + request: { + textDocument: request.textDocument, + position: request.position, + context: request.context, + }, + }) + let result: InlineCompletionListWithReferences = await languageClient.sendRequest( inlineCompletionWithReferencesRequestType.method, request, token ) + getLogger().info('Received inline completion response: %O', { + sessionId: result.sessionId, + itemCount: result.items?.length || 0, + items: result.items?.map((item) => ({ + itemId: item.itemId, + insertText: + (typeof item.insertText === 'string' ? item.insertText : String(item.insertText))?.substring( + 0, + 50 + ) + '...', + })), + }) - // Set telemetry data for the first response TelemetryHelper.instance.setSdkApiCallEndTime() - TelemetryHelper.instance.setSessionId(firstResult.sessionId) - if (firstResult.items.length > 0) { - TelemetryHelper.instance.setFirstResponseRequestId(firstResult.items[0].itemId) + TelemetryHelper.instance.setSessionId(result.sessionId) + if (result.items.length > 0 && result.items[0].itemId !== undefined) { + TelemetryHelper.instance.setFirstResponseRequestId(result.items[0].itemId as string) } TelemetryHelper.instance.setFirstSuggestionShowTime() - const firstCompletionDisplayLatency = Date.now() - requestStartTime + const firstCompletionDisplayLatency = globals.clock.Date.now() - requestStartTime this.sessionManager.startSession( - firstResult.sessionId, - firstResult.items, + result.sessionId, + result.items, requestStartTime, position, firstCompletionDisplayLatency ) - if (firstResult.partialResultToken) { - // If there are more results to fetch, handle them in the background - this.processRemainingRequests(languageClient, request, firstResult, token).catch((error) => { - languageClient.warn(`Error when getting suggestions: ${error}`) - }) - } else { - this.sessionManager.closeSession() - - // No more results to fetch, mark pagination as complete - TelemetryHelper.instance.setAllPaginationEndTime() - TelemetryHelper.instance.tryRecordClientComponentLatency() + // If there are more results to fetch, handle them in the background + try { + while (result.partialResultToken) { + const paginatedRequest = { ...request, partialResultToken: result.partialResultToken } + result = await languageClient.sendRequest( + inlineCompletionWithReferencesRequestType.method, + paginatedRequest, + token + ) + this.sessionManager.updateSessionSuggestions(result.items) + } + } catch (error) { + languageClient.warn(`Error when getting suggestions: ${error}`) } - } finally { - // Remove all UI indicators of message generation since we are done - this.inlineGeneratingMessage.hideGenerating() - void statusBar.refreshStatusBar() // effectively "stop loading" - } - } - private async processRemainingRequests( - languageClient: LanguageClient, - initialRequest: InlineCompletionWithReferencesParams, - firstResult: InlineCompletionListWithReferences, - token: CancellationToken - ): Promise { - let nextToken = firstResult.partialResultToken - while (nextToken) { - const request = { ...initialRequest, partialResultToken: nextToken } - const result: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType.method, - request, - token - ) - this.sessionManager.updateSessionSuggestions(result.items) - nextToken = result.partialResultToken + // Close session and finalize telemetry regardless of pagination path + this.sessionManager.closeSession() + TelemetryHelper.instance.setAllPaginationEndTime() + options.emitTelemetry && TelemetryHelper.instance.tryRecordClientComponentLatency() + } catch (error) { + getLogger().error('Error getting recommendations: %O', error) + return [] + } finally { + // Remove all UI indicators if UI is enabled + if (options.showUi) { + this.inlineGeneratingMessage.hideGenerating() + void statusBar.refreshStatusBar() // effectively "stop loading" + } } - - this.sessionManager.closeSession() - - // All pagination requests completed - TelemetryHelper.instance.setAllPaginationEndTime() - TelemetryHelper.instance.tryRecordClientComponentLatency() } } diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 6e052ddbfbe..7b3971ae2c1 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode' import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes-types' // TODO: add more needed data to the session interface -interface CodeWhispererSession { +export interface CodeWhispererSession { sessionId: string suggestions: InlineCompletionItemWithReferences[] // TODO: might need to convert to enum states diff --git a/packages/amazonq/src/app/inline/webViewPanel.ts b/packages/amazonq/src/app/inline/webViewPanel.ts new file mode 100644 index 00000000000..2effa94429c --- /dev/null +++ b/packages/amazonq/src/app/inline/webViewPanel.ts @@ -0,0 +1,450 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +/* eslint-disable no-restricted-imports */ +import fs from 'fs' +import { getLogger } from 'aws-core-vscode/shared' + +/** + * Interface for JSON request log data + */ +interface RequestLogEntry { + timestamp: string + request: string + response: string + endpoint: string + error: string + requestId: string + responseCode: number + applicationLogs?: { + rts?: string[] + ceo?: string[] + [key: string]: string[] | undefined + } + latency?: number + latencyBreakdown?: { + rts?: number + ceo?: number + [key: string]: number | undefined + } + miscellaneous?: any +} + +/** + * Manages the webview panel for displaying insert text content and request logs + */ +export class NextEditPredictionPanel implements vscode.Disposable { + public static readonly viewType = 'nextEditPrediction' + + private static instance: NextEditPredictionPanel | undefined + private panel: vscode.WebviewPanel | undefined + private disposables: vscode.Disposable[] = [] + private statusBarItem: vscode.StatusBarItem + private isVisible = false + private fileWatcher: vscode.FileSystemWatcher | undefined + private requestLogs: RequestLogEntry[] = [] + private logFilePath = '/tmp/request_log.jsonl' + private fileReadTimeout: NodeJS.Timeout | undefined + + private constructor() { + // Create status bar item - higher priority (1) to ensure visibility + this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1) + this.statusBarItem.text = '$(eye) NEP' // Add icon for better visibility + this.statusBarItem.tooltip = 'Toggle Next Edit Prediction Panel' + this.statusBarItem.command = 'aws.amazonq.toggleNextEditPredictionPanel' + this.statusBarItem.show() + + // Register command for toggling the panel + this.disposables.push( + vscode.commands.registerCommand('aws.amazonq.toggleNextEditPredictionPanel', () => { + this.toggle() + }) + ) + } + + /** + * Get or create the NextEditPredictionPanel instance + */ + public static getInstance(): NextEditPredictionPanel { + if (!NextEditPredictionPanel.instance) { + NextEditPredictionPanel.instance = new NextEditPredictionPanel() + } + return NextEditPredictionPanel.instance + } + + /** + * Setup file watcher to monitor the request log file + */ + private setupFileWatcher(): void { + if (this.fileWatcher) { + return + } + + try { + // Create the watcher for the specific file + this.fileWatcher = vscode.workspace.createFileSystemWatcher(this.logFilePath) + + // When file is changed, read it after a delay + this.fileWatcher.onDidChange(() => { + this.scheduleFileRead() + }) + + // When file is created, read it after a delay + this.fileWatcher.onDidCreate(() => { + this.scheduleFileRead() + }) + + this.disposables.push(this.fileWatcher) + + // Initial read of the file if it exists + if (fs.existsSync(this.logFilePath)) { + this.scheduleFileRead() + } + + getLogger('nextEditPrediction').info(`File watcher set up for ${this.logFilePath}`) + } catch (error) { + getLogger('nextEditPrediction').error(`Error setting up file watcher: ${error}`) + } + } + + /** + * Schedule file read with a delay to ensure file is fully written + */ + private scheduleFileRead(): void { + // Clear any existing timeout + if (this.fileReadTimeout) { + clearTimeout(this.fileReadTimeout) + } + + // Schedule new read after 1 second delay + this.fileReadTimeout = setTimeout(() => { + this.readRequestLogFile() + }, 1000) + } + + /** + * Read the request log file and update the panel content + */ + private readRequestLogFile(): void { + getLogger('nextEditPrediction').info(`Attempting to read log file: ${this.logFilePath}`) + try { + if (!fs.existsSync(this.logFilePath)) { + getLogger('nextEditPrediction').info(`Log file does not exist: ${this.logFilePath}`) + return + } + + const content = fs.readFileSync(this.logFilePath, 'utf8') + this.requestLogs = [] + + // Process JSONL format (one JSON object per line) + const lines = content.split('\n').filter((line: string) => line.trim() !== '') + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + try { + // Try to parse the JSON, handling potential trailing characters + let jsonString = line + + // Find the last valid JSON by looking for the last closing brace/bracket + const lastClosingBrace = line.lastIndexOf('}') + const lastClosingBracket = line.lastIndexOf(']') + const lastValidChar = Math.max(lastClosingBrace, lastClosingBracket) + + if (lastValidChar > 0 && lastValidChar < line.length - 1) { + // If there are characters after the last valid JSON ending, trim them + jsonString = line.substring(0, lastValidChar + 1) + getLogger('nextEditPrediction').info(`Trimmed extra characters from line ${i + 1}`) + } + + // Step 1: Parse the JSON string to get an object + const parsed = JSON.parse(jsonString) + // Step 2: Stringify the object to normalize it + const normalized = JSON.stringify(parsed) + // Step 3: Parse the normalized string back to an object + const logEntry = JSON.parse(normalized) as RequestLogEntry + + // Parse request and response fields if they're JSON stringss + if (typeof logEntry.request === 'string') { + try { + // Apply the same double-parse technique to nested JSON + const requestObj = JSON.parse(logEntry.request) + const requestNormalized = JSON.stringify(requestObj) + logEntry.request = JSON.parse(requestNormalized) + } catch (e) { + // Keep as string if it's not valid JSON + getLogger('nextEditPrediction').info(`Could not parse request as JSON: ${e}`) + } + } + + if (typeof logEntry.response === 'string') { + try { + // Apply the same double-parse technique to nested JSON + const responseObj = JSON.parse(logEntry.response) + const responseNormalized = JSON.stringify(responseObj) + logEntry.response = JSON.parse(responseNormalized) + } catch (e) { + // Keep as string if it's not valid JSON + getLogger('nextEditPrediction').info(`Could not parse response as JSON: ${e}`) + } + } + + this.requestLogs.push(logEntry) + } catch (e) { + getLogger('nextEditPrediction').error(`Error parsing log entry ${i + 1}: ${e}`) + getLogger('nextEditPrediction').error( + `Problematic line: ${line.length > 100 ? line.substring(0, 100) + '...' : line}` + ) + } + } + + if (this.isVisible && this.panel) { + this.updateRequestLogsView() + } + + getLogger('nextEditPrediction').info(`Read ${this.requestLogs.length} log entries`) + } catch (error) { + getLogger('nextEditPrediction').error(`Error reading log file: ${error}`) + } + } + + /** + * Update the panel with request logs data + */ + private updateRequestLogsView(): void { + if (this.panel) { + this.panel.webview.html = this.getWebviewContent() + getLogger('nextEditPrediction').info('Webview panel updated with request logs') + } + } + + /** + * Toggle the panel visibility + */ + public toggle(): void { + if (this.isVisible) { + this.hide() + } else { + this.show() + } + } + + /** + * Show the panel + */ + public show(): void { + if (!this.panel) { + // Create the webview panel + this.panel = vscode.window.createWebviewPanel( + NextEditPredictionPanel.viewType, + 'Next Edit Prediction', + vscode.ViewColumn.Beside, + { + enableScripts: true, + retainContextWhenHidden: true, + } + ) + + // Set initial content + this.panel.webview.html = this.getWebviewContent() + + // Handle panel disposal + this.panel.onDidDispose( + () => { + this.panel = undefined + this.isVisible = false + this.updateStatusBarItem() + }, + undefined, + this.disposables + ) + + // Handle webview messages + this.panel.webview.onDidReceiveMessage( + (message) => { + switch (message.command) { + case 'refresh': + getLogger('nextEditPrediction').info(`Refresh button clicked`) + this.readRequestLogFile() + break + case 'clear': + getLogger('nextEditPrediction').info(`Clear logs button clicked`) + this.clearLogFile() + break + } + }, + undefined, + this.disposables + ) + } else { + this.panel.reveal() + } + + this.isVisible = true + this.updateStatusBarItem() + + // Setup file watcher when panel is shown + this.setupFileWatcher() + + // If we already have logs, update the view + if (this.requestLogs.length > 0) { + this.updateRequestLogsView() + } else { + // Try to read the log file + this.scheduleFileRead() + } + } + + /** + * Hide the panel + */ + private hide(): void { + if (this.panel) { + this.panel.dispose() + this.panel = undefined + this.isVisible = false + this.updateStatusBarItem() + } + } + + /** + * Update the status bar item appearance based on panel state + */ + private updateStatusBarItem(): void { + if (this.isVisible) { + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground') + } else { + this.statusBarItem.backgroundColor = undefined + } + } + + /** + * Update the panel content with new text + */ + public updateContent(text: string): void { + if (this.panel) { + try { + // Store the text for display in a separate section + const customContent = text + + // Update the panel with both the custom content and the request logs + this.panel.webview.html = this.getWebviewContent(customContent) + getLogger('nextEditPrediction').info('Webview panel content updated') + } catch (error) { + getLogger('nextEditPrediction').error(`Error updating webview: ${error}`) + } + } + } + + /** + * Generate HTML content for the webview + */ + private getWebviewContent(customContent?: string): string { + // Path to the debug.html file + const debugHtmlPath = vscode.Uri.file( + vscode.Uri.joinPath( + vscode.Uri.file(__dirname), + '..', + '..', + '..', + 'app', + 'inline', + 'EditRendering', + 'debug.html' + ).fsPath + ) + + // Read the HTML file content + try { + const htmlContent = fs.readFileSync(debugHtmlPath.fsPath, 'utf8') + getLogger('nextEditPrediction').info(`Successfully loaded debug.html from ${debugHtmlPath.fsPath}`) + + // Modify the HTML to add vscode API initialization + return htmlContent.replace( + '', + ` + + ` + ) + } catch (error) { + getLogger('nextEditPrediction').error(`Error loading debug.html: ${error}`) + return ` + + +

Error loading visualization

+

Failed to load debug.html file: ${error}

+ + + ` + } + } + + /** + * Clear the log file and update the panel + */ + private clearLogFile(): void { + try { + getLogger('nextEditPrediction').info(`Clearing log file: ${this.logFilePath}`) + + // Write an empty string to clear the file + fs.writeFileSync(this.logFilePath, '') + + // Clear the in-memory logs + this.requestLogs = [] + + // Update the view + if (this.isVisible && this.panel) { + this.updateRequestLogsView() + } + + getLogger('nextEditPrediction').info(`Log file cleared successfully`) + } catch (error) { + getLogger('nextEditPrediction').error(`Error clearing log file: ${error}`) + } + } + + /** + * Dispose of resources + */ + public dispose(): void { + if (this.panel) { + this.panel.dispose() + } + + if (this.fileWatcher) { + this.fileWatcher.dispose() + } + + if (this.fileReadTimeout) { + clearTimeout(this.fileReadTimeout) + } + + this.statusBarItem.dispose() + + for (const d of this.disposables) { + d.dispose() + } + this.disposables = [] + + NextEditPredictionPanel.instance = undefined + } +} diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 6cbb05dd582..8fcfef0d397 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -169,6 +169,11 @@ export async function startLanguageServer( notifications: true, showSaveFileDialog: true, }, + textDocument: { + inlineCompletionWithReferences: { + inlineEditSupport: Experiments.instance.isExperimentEnabled('amazonqLSPNEP'), + }, + }, }, contextConfiguration: { workspaceIdentifier: extensionContext.storageUri?.path, diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index 57eca77b147..c143020d74d 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -5,21 +5,32 @@ import sinon from 'sinon' import { LanguageClient } from 'vscode-languageclient' -import { Position, CancellationToken, InlineCompletionItem } from 'vscode' +import { Position, CancellationToken, InlineCompletionItem, InlineCompletionTriggerKind } from 'vscode' import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' import { createMockDocument } from 'aws-core-vscode/test' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' +// Import CursorUpdateManager directly instead of the interface +import { CursorUpdateManager } from '../../../../../src/app/inline/cursorUpdateManager' +import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' +import { globals } from 'aws-core-vscode/shared' describe('RecommendationService', () => { let languageClient: LanguageClient let sendRequestStub: sinon.SinonStub let sandbox: sinon.SinonSandbox + let sessionManager: SessionManager + let lineTracker: LineTracker + let activeStateController: InlineGeneratingMessage + let service: RecommendationService + let cursorUpdateManager: CursorUpdateManager + let statusBarStub: any + let clockStub: sinon.SinonFakeTimers const mockDocument = createMockDocument() const mockPosition = { line: 0, character: 0 } as Position - const mockContext = { triggerKind: 1, selectedCompletionInfo: undefined } + const mockContext = { triggerKind: InlineCompletionTriggerKind.Automatic, selectedCompletionInfo: undefined } const mockToken = { isCancellationRequested: false } as CancellationToken const mockInlineCompletionItemOne = { insertText: 'ItemOne', @@ -29,19 +40,61 @@ describe('RecommendationService', () => { insertText: 'ItemTwo', } as InlineCompletionItem const mockPartialResultToken = 'some-random-token' - const sessionManager = new SessionManager() - const lineTracker = new LineTracker() - const activeStateController = new InlineGeneratingMessage(lineTracker) - const service = new RecommendationService(sessionManager, activeStateController) - beforeEach(() => { + beforeEach(async () => { sandbox = sinon.createSandbox() + // Create a fake clock for testing time-based functionality + clockStub = sandbox.useFakeTimers({ + now: 1000, + shouldAdvanceTime: true, + }) + + // Stub globals.clock + sandbox.stub(globals, 'clock').value({ + Date: { + now: () => clockStub.now, + }, + setTimeout: clockStub.setTimeout.bind(clockStub), + clearTimeout: clockStub.clearTimeout.bind(clockStub), + setInterval: clockStub.setInterval.bind(clockStub), + clearInterval: clockStub.clearInterval.bind(clockStub), + }) + sendRequestStub = sandbox.stub() languageClient = { sendRequest: sendRequestStub, + warn: sandbox.stub(), } as unknown as LanguageClient + + sessionManager = new SessionManager() + lineTracker = new LineTracker() + activeStateController = new InlineGeneratingMessage(lineTracker) + + // Create cursor update manager mock + cursorUpdateManager = { + recordCompletionRequest: sandbox.stub(), + logger: { debug: sandbox.stub(), warn: sandbox.stub(), error: sandbox.stub() }, + updateIntervalMs: 250, + isActive: false, + lastRequestTime: 0, + dispose: sandbox.stub(), + start: sandbox.stub(), + stop: sandbox.stub(), + updatePosition: sandbox.stub(), + } as unknown as CursorUpdateManager + + // Create status bar stub + statusBarStub = { + setLoading: sandbox.stub().resolves(), + refreshStatusBar: sandbox.stub().resolves(), + } + + sandbox.stub(CodeWhispererStatusBarManager, 'instance').get(() => statusBarStub) + + // Create the service without cursor update recorder initially + service = new RecommendationService(sessionManager, activeStateController) }) afterEach(() => { @@ -49,6 +102,32 @@ describe('RecommendationService', () => { sessionManager.clear() }) + describe('constructor', () => { + it('should initialize with optional cursorUpdateRecorder', () => { + const serviceWithRecorder = new RecommendationService( + sessionManager, + activeStateController, + cursorUpdateManager + ) + + // Verify the service was created with the recorder + assert.strictEqual(serviceWithRecorder['cursorUpdateRecorder'], cursorUpdateManager) + }) + }) + + describe('setCursorUpdateRecorder', () => { + it('should set the cursor update recorder', () => { + // Initially the recorder should be undefined + assert.strictEqual(service['cursorUpdateRecorder'], undefined) + + // Set the recorder + service.setCursorUpdateRecorder(cursorUpdateManager) + + // Verify it was set correctly + assert.strictEqual(service['cursorUpdateRecorder'], cursorUpdateManager) + }) + }) + describe('getAllRecommendations', () => { it('should handle single request with no partial result token', async () => { const mockFirstResult = { @@ -112,5 +191,112 @@ describe('RecommendationService', () => { partialResultToken: mockPartialResultToken, }) }) + + it('should record completion request when cursorUpdateRecorder is set', async () => { + // Set the cursor update recorder + service.setCursorUpdateRecorder(cursorUpdateManager) + + const mockFirstResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockFirstResult) + + await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken) + + // Verify recordCompletionRequest was called + // eslint-disable-next-line @typescript-eslint/unbound-method + sinon.assert.calledOnce(cursorUpdateManager.recordCompletionRequest as sinon.SinonStub) + }) + + // Helper function to setup UI test + function setupUITest() { + const mockFirstResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockFirstResult) + + // Spy on the UI methods + const showGeneratingStub = sandbox.stub(activeStateController, 'showGenerating').resolves() + const hideGeneratingStub = sandbox.stub(activeStateController, 'hideGenerating') + + return { showGeneratingStub, hideGeneratingStub } + } + + it('should not show UI indicators when showUi option is false', async () => { + const { showGeneratingStub, hideGeneratingStub } = setupUITest() + + // Call with showUi: false option + await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken, { + showUi: false, + emitTelemetry: true, + }) + + // Verify UI methods were not called + sinon.assert.notCalled(showGeneratingStub) + sinon.assert.notCalled(hideGeneratingStub) + sinon.assert.notCalled(statusBarStub.setLoading) + sinon.assert.notCalled(statusBarStub.refreshStatusBar) + }) + + it('should show UI indicators when showUi option is true (default)', async () => { + const { showGeneratingStub, hideGeneratingStub } = setupUITest() + + // Call with default options (showUi: true) + await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken) + + // Verify UI methods were called + sinon.assert.calledOnce(showGeneratingStub) + sinon.assert.calledOnce(hideGeneratingStub) + sinon.assert.calledOnce(statusBarStub.setLoading) + sinon.assert.calledOnce(statusBarStub.refreshStatusBar) + }) + + it('should handle errors gracefully', async () => { + // Set the cursor update recorder + service.setCursorUpdateRecorder(cursorUpdateManager) + + // Make the request throw an error + const testError = new Error('Test error') + sendRequestStub.rejects(testError) + + // Set up UI options + const options = { showUi: true } + + // Stub the UI methods to avoid errors + // const showGeneratingStub = sandbox.stub(activeStateController, 'showGenerating').resolves() + const hideGeneratingStub = sandbox.stub(activeStateController, 'hideGenerating') + + // Temporarily replace console.error with a no-op function to prevent test failure + const originalConsoleError = console.error + console.error = () => {} + + try { + // Call the method and expect it to handle the error + const result = await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + options + ) + + // Assert that error handling was done correctly + assert.deepStrictEqual(result, []) + + // Verify the UI indicators were hidden even when an error occurs + sinon.assert.calledOnce(hideGeneratingStub) + sinon.assert.calledOnce(statusBarStub.refreshStatusBar) + } finally { + // Restore the original console.error function + console.error = originalConsoleError + } + }) }) }) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts new file mode 100644 index 00000000000..512092d53d3 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts @@ -0,0 +1,105 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { applyUnifiedDiff, getAddedAndDeletedCharCount } from '../../../../../src/app/inline/EditRendering/diffUtils' + +describe('diffUtils', function () { + describe('applyUnifiedDiff', function () { + it('should correctly apply a unified diff to original text', function () { + // Original code + const originalCode = 'function add(a, b) {\n return a + b;\n}' + + // Unified diff that adds a comment and modifies the return statement + const unifiedDiff = + '--- a/file.js\n' + + '+++ b/file.js\n' + + '@@ -1,3 +1,4 @@\n' + + ' function add(a, b) {\n' + + '+ // Add two numbers\n' + + '- return a + b;\n' + + '+ return a + b; // Return the sum\n' + + ' }' + + // Expected result after applying the diff + const expectedResult = 'function add(a, b) {\n // Add two numbers\n return a + b; // Return the sum\n}' + + // Apply the diff + const { appliedCode } = applyUnifiedDiff(originalCode, unifiedDiff) + + // Verify the result + assert.strictEqual(appliedCode, expectedResult) + }) + }) + + describe('getAddedAndDeletedCharCount', function () { + it('should correctly calculate added and deleted character counts', function () { + // Unified diff with additions and deletions + const unifiedDiff = + '--- a/file.js\n' + + '+++ b/file.js\n' + + '@@ -1,3 +1,4 @@\n' + + ' function add(a, b) {\n' + + '+ // Add two numbers\n' + + '- return a + b;\n' + + '+ return a + b; // Return the sum\n' + + ' }' + + // Calculate character counts + const { addedCharacterCount, deletedCharacterCount } = getAddedAndDeletedCharCount(unifiedDiff) + + // Verify the counts with the actual values from the implementation + assert.strictEqual(addedCharacterCount, 20) + assert.strictEqual(deletedCharacterCount, 15) + }) + }) + + describe('applyUnifiedDiff with complex changes', function () { + it('should handle multiple hunks in a diff', function () { + // Original code with multiple functions + const originalCode = + 'function add(a, b) {\n' + + ' return a + b;\n' + + '}\n' + + '\n' + + 'function subtract(a, b) {\n' + + ' return a - b;\n' + + '}' + + // Unified diff that modifies both functions + const unifiedDiff = + '--- a/file.js\n' + + '+++ b/file.js\n' + + '@@ -1,3 +1,4 @@\n' + + ' function add(a, b) {\n' + + '+ // Addition function\n' + + ' return a + b;\n' + + ' }\n' + + '@@ -5,3 +6,4 @@\n' + + ' function subtract(a, b) {\n' + + '+ // Subtraction function\n' + + ' return a - b;\n' + + ' }' + + // Expected result after applying the diff + const expectedResult = + 'function add(a, b) {\n' + + ' // Addition function\n' + + ' return a + b;\n' + + '}\n' + + '\n' + + 'function subtract(a, b) {\n' + + ' // Subtraction function\n' + + ' return a - b;\n' + + '}' + + // Apply the diff + const { appliedCode } = applyUnifiedDiff(originalCode, unifiedDiff) + + // Verify the result + assert.strictEqual(appliedCode, expectedResult) + }) + }) +}) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts new file mode 100644 index 00000000000..df4fac09c28 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts @@ -0,0 +1,176 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import assert from 'assert' +import { EditDecorationManager } from '../../../../../src/app/inline/EditRendering/displayImage' + +describe('EditDecorationManager', function () { + let sandbox: sinon.SinonSandbox + let editorStub: sinon.SinonStubbedInstance + let documentStub: sinon.SinonStubbedInstance + let windowStub: sinon.SinonStubbedInstance + let commandsStub: sinon.SinonStubbedInstance + let decorationTypeStub: sinon.SinonStubbedInstance + let manager: EditDecorationManager + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create stubs for vscode objects + decorationTypeStub = { + dispose: sandbox.stub(), + } as unknown as sinon.SinonStubbedInstance + + documentStub = { + getText: sandbox.stub().returns('Original code content'), + lineCount: 5, + lineAt: sandbox.stub().returns({ + text: 'Line text content', + range: new vscode.Range(0, 0, 0, 18), + rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 19), + firstNonWhitespaceCharacterIndex: 0, + isEmptyOrWhitespace: false, + }), + } as unknown as sinon.SinonStubbedInstance + + editorStub = { + document: documentStub, + setDecorations: sandbox.stub(), + edit: sandbox.stub().resolves(true), + } as unknown as sinon.SinonStubbedInstance + + windowStub = sandbox.stub(vscode.window) + windowStub.createTextEditorDecorationType.returns(decorationTypeStub as any) + + commandsStub = sandbox.stub(vscode.commands) + commandsStub.registerCommand.returns({ dispose: sandbox.stub() }) + + // Create a new instance of EditDecorationManager for each test + manager = new EditDecorationManager() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should display SVG decorations in the editor', function () { + // Create a fake SVG image URI + const svgUri = vscode.Uri.parse('file:///path/to/image.svg') + + // Create accept and reject handlers + const acceptHandler = sandbox.stub() + const rejectHandler = sandbox.stub() + + // Reset the setDecorations stub to clear any previous calls + editorStub.setDecorations.reset() + + // Call displayEditSuggestion + manager.displayEditSuggestion( + editorStub as unknown as vscode.TextEditor, + svgUri, + 0, + acceptHandler, + rejectHandler, + 'Original code', + 'New code', + [{ line: 0, start: 0, end: 0 }] + ) + + // Verify decorations were set (we expect 4 calls because clearDecorations is called first) + assert.strictEqual(editorStub.setDecorations.callCount, 4) + + // Verify the third call is for the image decoration (after clearDecorations) + const imageCall = editorStub.setDecorations.getCall(2) + assert.strictEqual(imageCall.args[0], manager['imageDecorationType']) + assert.strictEqual(imageCall.args[1].length, 1) + + // Verify the fourth call is for the removed code decoration + const removedCodeCall = editorStub.setDecorations.getCall(3) + assert.strictEqual(removedCodeCall.args[0], manager['removedCodeDecorationType']) + }) + + // Helper function to setup edit suggestion test + function setupEditSuggestionTest() { + // Create a fake SVG image URI + const svgUri = vscode.Uri.parse('file:///path/to/image.svg') + + // Create accept and reject handlers + const acceptHandler = sandbox.stub() + const rejectHandler = sandbox.stub() + + // Display the edit suggestion + manager.displayEditSuggestion( + editorStub as unknown as vscode.TextEditor, + svgUri, + 0, + acceptHandler, + rejectHandler, + 'Original code', + 'New code', + [{ line: 0, start: 0, end: 0 }] + ) + + return { acceptHandler, rejectHandler } + } + + it('should trigger accept handler when command is executed', function () { + const { acceptHandler, rejectHandler } = setupEditSuggestionTest() + + // Find the command handler that was registered for accept + const acceptCommandArgs = commandsStub.registerCommand.args.find( + (args) => args[0] === 'aws.amazonq.inline.acceptEdit' + ) + + // Execute the accept command handler if found + if (acceptCommandArgs && acceptCommandArgs[1]) { + const acceptCommandHandler = acceptCommandArgs[1] + acceptCommandHandler() + + // Verify the accept handler was called + sinon.assert.calledOnce(acceptHandler) + sinon.assert.notCalled(rejectHandler) + } else { + assert.fail('Accept command handler not found') + } + }) + + it('should trigger reject handler when command is executed', function () { + const { acceptHandler, rejectHandler } = setupEditSuggestionTest() + + // Find the command handler that was registered for reject + const rejectCommandArgs = commandsStub.registerCommand.args.find( + (args) => args[0] === 'aws.amazonq.inline.rejectEdit' + ) + + // Execute the reject command handler if found + if (rejectCommandArgs && rejectCommandArgs[1]) { + const rejectCommandHandler = rejectCommandArgs[1] + rejectCommandHandler() + + // Verify the reject handler was called + sinon.assert.calledOnce(rejectHandler) + sinon.assert.notCalled(acceptHandler) + } else { + assert.fail('Reject command handler not found') + } + }) + + it('should clear decorations when requested', function () { + // Reset the setDecorations stub to clear any previous calls + editorStub.setDecorations.reset() + + // Call clearDecorations + manager.clearDecorations(editorStub as unknown as vscode.TextEditor) + + // Verify decorations were cleared + assert.strictEqual(editorStub.setDecorations.callCount, 2) + + // Verify both decoration types were cleared + sinon.assert.calledWith(editorStub.setDecorations.firstCall, manager['imageDecorationType'], []) + sinon.assert.calledWith(editorStub.setDecorations.secondCall, manager['removedCodeDecorationType'], []) + }) +}) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts new file mode 100644 index 00000000000..2a3db2af650 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts @@ -0,0 +1,271 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import assert from 'assert' +// Remove static import - we'll use dynamic import instead +// import { showEdits } from '../../../../../src/app/inline/EditRendering/imageRenderer' +import { SvgGenerationService } from '../../../../../src/app/inline/EditRendering/svgGenerator' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' + +describe('showEdits', function () { + let sandbox: sinon.SinonSandbox + let editorStub: sinon.SinonStubbedInstance + let documentStub: sinon.SinonStubbedInstance + let svgGenerationServiceStub: sinon.SinonStubbedInstance + let displaySvgDecorationStub: sinon.SinonStub + let loggerStub: sinon.SinonStubbedInstance + let getLoggerStub: sinon.SinonStub + let showEdits: any // Will be dynamically imported + let languageClientStub: any + let sessionStub: any + let itemStub: InlineCompletionItemWithReferences + + // Helper function to create mock SVG result + function createMockSvgResult(overrides: Partial = {}) { + return { + svgImage: vscode.Uri.file('/path/to/generated.svg'), + startLine: 5, + newCode: 'console.log("Hello World");', + origionalCodeHighlightRange: [{ line: 5, start: 0, end: 10 }], + addedCharacterCount: 25, + deletedCharacterCount: 0, + ...overrides, + } + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create logger stub + loggerStub = { + error: sandbox.stub(), + info: sandbox.stub(), + debug: sandbox.stub(), + warn: sandbox.stub(), + } + + // Clear all relevant module caches + const moduleId = require.resolve('../../../../../src/app/inline/EditRendering/imageRenderer') + const sharedModuleId = require.resolve('aws-core-vscode/shared') + delete require.cache[moduleId] + delete require.cache[sharedModuleId] + + // Create getLogger stub and store reference for test verification + getLoggerStub = sandbox.stub().returns(loggerStub) + + // Create a mock shared module with stubbed getLogger + const mockSharedModule = { + getLogger: getLoggerStub, + } + + // Override the require cache with our mock + require.cache[sharedModuleId] = { + id: sharedModuleId, + filename: sharedModuleId, + loaded: true, + parent: undefined, + children: [], + exports: mockSharedModule, + paths: [], + } as any + + // Now require the module - it should use our mocked getLogger + const imageRendererModule = require('../../../../../src/app/inline/EditRendering/imageRenderer') + showEdits = imageRendererModule.showEdits + + // Create document stub + documentStub = { + uri: { + fsPath: '/path/to/test/file.ts', + }, + getText: sandbox.stub().returns('Original code content'), + lineCount: 5, + } as unknown as sinon.SinonStubbedInstance + + // Create editor stub + editorStub = { + document: documentStub, + setDecorations: sandbox.stub(), + edit: sandbox.stub().resolves(true), + } as unknown as sinon.SinonStubbedInstance + + // Create SVG generation service stub + svgGenerationServiceStub = { + generateDiffSvg: sandbox.stub(), + } as unknown as sinon.SinonStubbedInstance + + // Stub the SvgGenerationService constructor + sandbox + .stub(SvgGenerationService.prototype, 'generateDiffSvg') + .callsFake(svgGenerationServiceStub.generateDiffSvg) + + // Create display SVG decoration stub + displaySvgDecorationStub = sandbox.stub() + sandbox.replace( + require('../../../../../src/app/inline/EditRendering/displayImage'), + 'displaySvgDecoration', + displaySvgDecorationStub + ) + + // Create language client stub + languageClientStub = {} as any + + // Create session stub + sessionStub = { + sessionId: 'test-session-id', + suggestions: [], + isRequestInProgress: false, + requestStartTime: Date.now(), + startPosition: new vscode.Position(0, 0), + } as any + + // Create item stub + itemStub = { + insertText: 'console.log("Hello World");', + range: new vscode.Range(0, 0, 0, 0), + itemId: 'test-item-id', + } as any + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should return early when editor is undefined', async function () { + await showEdits(itemStub, undefined, sessionStub, languageClientStub) + + // Verify that no SVG generation or display methods were called + sinon.assert.notCalled(svgGenerationServiceStub.generateDiffSvg) + sinon.assert.notCalled(displaySvgDecorationStub) + sinon.assert.notCalled(loggerStub.error) + }) + + it('should successfully generate and display SVG when all parameters are valid', async function () { + // Setup successful SVG generation + const mockSvgResult = createMockSvgResult() + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called with correct parameters + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + sinon.assert.calledWith( + svgGenerationServiceStub.generateDiffSvg, + '/path/to/test/file.ts', + 'console.log("Hello World");' + ) + + // Verify display decoration was called with correct parameters + sinon.assert.calledOnce(displaySvgDecorationStub) + sinon.assert.calledWith( + displaySvgDecorationStub, + editorStub, + mockSvgResult.svgImage, + mockSvgResult.startLine, + mockSvgResult.newCode, + mockSvgResult.origionalCodeHighlightRange, + sessionStub, + languageClientStub, + itemStub, + mockSvgResult.addedCharacterCount, + mockSvgResult.deletedCharacterCount + ) + + // Verify no errors were logged + sinon.assert.notCalled(loggerStub.error) + }) + + it('should log error when SVG generation returns empty result', async function () { + // Setup SVG generation to return undefined svgImage + const mockSvgResult = createMockSvgResult({ svgImage: undefined as any }) + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + + // Verify display decoration was NOT called + sinon.assert.notCalled(displaySvgDecorationStub) + + // Verify error was logged + sinon.assert.calledOnce(loggerStub.error) + sinon.assert.calledWith(loggerStub.error, 'SVG image generation returned an empty result.') + }) + + it('should catch and log error when SVG generation throws exception', async function () { + // Setup SVG generation to throw an error + const testError = new Error('SVG generation failed') + svgGenerationServiceStub.generateDiffSvg.rejects(testError) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + + // Verify display decoration was NOT called + sinon.assert.notCalled(displaySvgDecorationStub) + + // Verify error was logged with correct message + sinon.assert.calledOnce(loggerStub.error) + const errorCall = loggerStub.error.getCall(0) + assert.strictEqual(errorCall.args[0], `Error generating SVG image: ${testError}`) + }) + + it('should catch and log error when displaySvgDecoration throws exception', async function () { + // Setup successful SVG generation + const mockSvgResult = createMockSvgResult() + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + // Setup displaySvgDecoration to throw an error + const testError = new Error('Display decoration failed') + displaySvgDecorationStub.rejects(testError) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + + // Verify display decoration was called + sinon.assert.calledOnce(displaySvgDecorationStub) + + // Verify error was logged with correct message + sinon.assert.calledOnce(loggerStub.error) + const errorCall = loggerStub.error.getCall(0) + assert.strictEqual(errorCall.args[0], `Error generating SVG image: ${testError}`) + }) + + it('should use correct logger name', async function () { + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify getLogger was called with correct name + sinon.assert.calledWith(getLoggerStub, 'nextEditPrediction') + }) + + it('should handle item with undefined insertText', async function () { + // Create item with undefined insertText + const itemWithUndefinedText = { + ...itemStub, + insertText: undefined, + } as any + + // Setup successful SVG generation + const mockSvgResult = createMockSvgResult() + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + await showEdits( + itemWithUndefinedText, + editorStub as unknown as vscode.TextEditor, + sessionStub, + languageClientStub + ) + + // Verify SVG generation was called with undefined as string + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + sinon.assert.calledWith(svgGenerationServiceStub.generateDiffSvg, '/path/to/test/file.ts', undefined) + }) +}) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts new file mode 100644 index 00000000000..81ba05251e2 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts @@ -0,0 +1,278 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import assert from 'assert' +import { SvgGenerationService } from '../../../../../src/app/inline/EditRendering/svgGenerator' + +describe('SvgGenerationService', function () { + let sandbox: sinon.SinonSandbox + let service: SvgGenerationService + let documentStub: sinon.SinonStubbedInstance + let workspaceStub: sinon.SinonStubbedInstance + let editorConfigStub: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create stubs for vscode objects and utilities + documentStub = { + getText: sandbox.stub().returns('function example() {\n return 42;\n}'), + lineCount: 3, + lineAt: sandbox.stub().returns({ + text: 'Line content', + range: new vscode.Range(0, 0, 0, 12), + }), + } as unknown as sinon.SinonStubbedInstance + + workspaceStub = sandbox.stub(vscode.workspace) + workspaceStub.openTextDocument.resolves(documentStub as unknown as vscode.TextDocument) + workspaceStub.getConfiguration = sandbox.stub() + + editorConfigStub = { + get: sandbox.stub(), + } + editorConfigStub.get.withArgs('fontSize').returns(14) + editorConfigStub.get.withArgs('lineHeight').returns(0) + + // Create the service instance + service = new SvgGenerationService() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('generateDiffSvg', function () { + it('should handle empty original code', async function () { + // Create a new document stub for this test with empty content + const emptyDocStub = { + getText: sandbox.stub().returns(''), + lineCount: 0, + lineAt: sandbox.stub().returns({ + text: '', + range: new vscode.Range(0, 0, 0, 0), + }), + } as unknown as vscode.TextDocument + + // Make openTextDocument return our empty document + workspaceStub.openTextDocument.resolves(emptyDocStub as unknown as vscode.TextDocument) + + // A simple unified diff + const udiff = '--- a/example.js\n+++ b/example.js\n@@ -0,0 +1,1 @@\n+function example() {}\n' + + // Expect an error to be thrown + try { + await service.generateDiffSvg('example.js', udiff) + assert.fail('Expected an error to be thrown') + } catch (error) { + assert.ok(error) + assert.strictEqual((error as Error).message, 'udiff format error') + } + }) + }) + + describe('theme handling', function () { + it('should generate correct styles for dark theme', function () { + // Configure for dark theme + workspaceStub.getConfiguration.withArgs('editor').returns(editorConfigStub) + workspaceStub.getConfiguration.withArgs('workbench').returns({ + get: sandbox.stub().withArgs('colorTheme', 'Default').returns('Dark+ (default dark)'), + } as any) + + const getEditorTheme = (service as any).getEditorTheme.bind(service) + const theme = getEditorTheme() + + assert.strictEqual(theme.fontSize, 14) + assert.strictEqual(theme.lingHeight, 21) // 1.5 * 14 + assert.strictEqual(theme.foreground, 'rgba(212, 212, 212, 1)') + assert.strictEqual(theme.background, 'rgba(30, 30, 30, 1)') + }) + + it('should generate correct styles for light theme', function () { + // Reconfigure for light theme + editorConfigStub.get.withArgs('fontSize', 12).returns(12) + + workspaceStub.getConfiguration.withArgs('editor').returns(editorConfigStub) + workspaceStub.getConfiguration.withArgs('workbench').returns({ + get: sandbox.stub().withArgs('colorTheme', 'Default').returns('Light+ (default light)'), + } as any) + + const getEditorTheme = (service as any).getEditorTheme.bind(service) + const theme = getEditorTheme() + + assert.strictEqual(theme.fontSize, 12) + assert.strictEqual(theme.lingHeight, 18) // 1.5 * 12 + assert.strictEqual(theme.foreground, 'rgba(0, 0, 0, 1)') + assert.strictEqual(theme.background, 'rgba(255, 255, 255, 1)') + }) + + it('should handle custom line height settings', function () { + // Reconfigure for custom line height + editorConfigStub.get.withArgs('fontSize').returns(16) + editorConfigStub.get.withArgs('lineHeight').returns(2.5) + + workspaceStub.getConfiguration.withArgs('editor').returns(editorConfigStub) + workspaceStub.getConfiguration.withArgs('workbench').returns({ + get: sandbox.stub().withArgs('colorTheme', 'Default').returns('Dark+ (default dark)'), + } as any) + + const getEditorTheme = (service as any).getEditorTheme.bind(service) + const theme = getEditorTheme() + + assert.strictEqual(theme.fontSize, 16) + assert.strictEqual(theme.lingHeight, 40) // 2.5 * 16 + }) + + it('should generate CSS styles correctly', function () { + const theme = { + fontSize: 14, + lingHeight: 21, + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + diffAdded: 'rgba(231, 245, 231, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.2)', + } + + const generateStyles = (service as any).generateStyles.bind(service) + const styles = generateStyles(theme) + + assert.ok(styles.includes('font-size: 14px')) + assert.ok(styles.includes('line-height: 21px')) + assert.ok(styles.includes('color: rgba(212, 212, 212, 1)')) + assert.ok(styles.includes('background-color: rgba(30, 30, 30, 1)')) + assert.ok(styles.includes('.diff-changed')) + assert.ok(styles.includes('.diff-removed')) + }) + }) + + describe('highlight ranges', function () { + it('should generate highlight ranges for character-level changes', function () { + const originalCode = ['function test() {', ' return 42;', '}'] + const afterCode = ['function test() {', ' return 100;', '}'] + const modifiedLines = new Map([[' return 42;', ' return 100;']]) + + const generateHighlightRanges = (service as any).generateHighlightRanges.bind(service) + const result = generateHighlightRanges(originalCode, afterCode, modifiedLines) + + // Should have ranges for the changed characters + assert.ok(result.removedRanges.length > 0) + assert.ok(result.addedRanges.length > 0) + + // Check that ranges are properly formatted + const removedRange = result.removedRanges[0] + assert.ok(removedRange.line >= 0) + assert.ok(removedRange.start >= 0) + assert.ok(removedRange.end > removedRange.start) + + const addedRange = result.addedRanges[0] + assert.ok(addedRange.line >= 0) + assert.ok(addedRange.start >= 0) + assert.ok(addedRange.end > addedRange.start) + }) + + it('should merge adjacent highlight ranges', function () { + const originalCode = ['function test() {', ' return 42;', '}'] + const afterCode = ['function test() {', ' return 100;', '}'] + const modifiedLines = new Map([[' return 42;', ' return 100;']]) + + const generateHighlightRanges = (service as any).generateHighlightRanges.bind(service) + const result = generateHighlightRanges(originalCode, afterCode, modifiedLines) + + // Adjacent ranges should be merged + const sortedRanges = [...result.addedRanges].sort((a, b) => { + if (a.line !== b.line) { + return a.line - b.line + } + return a.start - b.start + }) + + // Check that no adjacent ranges exist + for (let i = 0; i < sortedRanges.length - 1; i++) { + const current = sortedRanges[i] + const next = sortedRanges[i + 1] + if (current.line === next.line) { + assert.ok(next.start - current.end > 1, 'Adjacent ranges should be merged') + } + } + }) + + it('should handle HTML escaping in highlight edits', function () { + const newLines = ['function test() {', ' return "";', '}'] + const highlightRanges = [{ line: 1, start: 10, end: 35 }] + + const getHighlightEdit = (service as any).getHighlightEdit.bind(service) + const result = getHighlightEdit(newLines, highlightRanges) + + assert.ok(result[1].includes('<script>')) + assert.ok(result[1].includes('</script>')) + assert.ok(result[1].includes('diff-changed')) + }) + }) + + describe('dimensions and positioning', function () { + it('should calculate dimensions correctly', function () { + const newLines = ['function test() {', ' return 42;', '}'] + const theme = { + fontSize: 14, + lingHeight: 21, + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + } + + const calculateDimensions = (service as any).calculateDimensions.bind(service) + const result = calculateDimensions(newLines, theme) + + assert.strictEqual(result.width, 287) + assert.strictEqual(result.height, 109) + assert.ok(result.height >= (newLines.length + 1) * theme.lingHeight) + }) + + it('should calculate position offset correctly', function () { + const originalLines = ['function test() {', ' return 42;', '}'] + const newLines = ['function test() {', ' return 100;', '}'] + const diffLines = [' return 100;'] + const theme = { + fontSize: 14, + lingHeight: 21, + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + } + + const calculatePosition = (service as any).calculatePosition.bind(service) + const result = calculatePosition(originalLines, newLines, diffLines, theme) + + assert.strictEqual(result.offset, 10) + assert.strictEqual(result.editStartLine, 1) + }) + }) + + describe('HTML content generation', function () { + it('should generate HTML content with proper structure', function () { + const diffLines = ['function test() {', ' return 42;', '}'] + const styles = '.code-container { color: white; }' + const offset = 20 + + const generateHtmlContent = (service as any).generateHtmlContent.bind(service) + const result = generateHtmlContent(diffLines, styles, offset) + + assert.ok(result.includes('
')) + assert.ok(result.includes('