Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(amazonq): client-side build support #6771

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions packages/amazonq/test/e2e/amazonq/transformByQ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ describe('Amazon Q Code Transformation', function () {
},
])

transformByQState.setSourceJDKVersion(JDKVersion.JDK8)
transformByQState.setTargetJDKVersion(JDKVersion.JDK17)

tab.addChatMessage({ command: '/transform' })

// wait for /transform to respond with some intro messages and the first user input form
Expand Down Expand Up @@ -141,20 +144,34 @@ describe('Amazon Q Code Transformation', function () {
formItemValues: oneOrMultipleDiffsFormValues,
})

// 2 additional chat messages (including message with 4th form) get sent after 3rd form submitted; wait for both of them
// 2 additional chat messages get sent after 3rd form submitted; wait for both of them
await tab.waitForEvent(() => tab.getChatItems().length > 11, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
const jdkPathPrompt = tab.getChatItems().pop()
assert.strictEqual(jdkPathPrompt?.body?.includes('Enter the path to JDK'), true)

// 2 additional chat messages get sent after 4th form submitted; wait for both of them
tab.addChatMessage({ prompt: '/dummy/path/to/jdk8' })
const customDependencyVersionPrompt = tab.getChatItems().pop()
assert.strictEqual(
customDependencyVersionPrompt?.body?.includes('You can optionally upload a YAML file'),
true
)
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, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})

const sourceJdkPathPrompt = tab.getChatItems().pop()
assert.strictEqual(sourceJdkPathPrompt?.body?.includes('Enter the path to JDK 8'), true)

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 > 15, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
const jdkPathResponse = tab.getChatItems().pop()
// this 'Sorry' message is OK - just making sure that the UI components are working correctly
assert.strictEqual(jdkPathResponse?.body?.includes("Sorry, I couldn't locate your Java installation"), true)
Expand All @@ -173,7 +190,7 @@ describe('Amazon Q Code Transformation', function () {
text: 'View summary',
})

await tab.waitForEvent(() => tab.getChatItems().length > 14, {
await tab.waitForEvent(() => tab.getChatItems().length > 16, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import assert from 'assert'
import {
TransformationProgressUpdate,
TransformationStep,
findDownloadArtifactProgressUpdate,
findDownloadArtifactStep,
getArtifactsFromProgressUpdate,
} from 'aws-core-vscode/codewhisperer/node'
Expand Down Expand Up @@ -95,4 +96,66 @@ describe('Amazon Q Transform - transformApiHandler tests', function () {
assert.strictEqual(progressUpdate, undefined)
})
})

describe('findDownloadArtifactProgressUpdate', function () {
it('will return correct progress update from transformationStep', function () {
const transformationStepsFixture: TransformationStep[] = [
{
id: 'dummy-id',
name: 'Step name',
description: 'Step description',
status: 'TRANSFORMING',
progressUpdates: [
{
name: 'Progress update name',
status: 'AWAITING_CLIENT_ACTION',
description: 'Client-side build happening now',
startTime: new Date(),
endTime: new Date(),
downloadArtifacts: [
{
downloadArtifactId: 'some-download-artifact-id',
downloadArtifactType: 'some-download-artifact-type',
},
],
},
],
startTime: new Date(),
endTime: new Date(),
},
]
const progressUpdate = findDownloadArtifactProgressUpdate(transformationStepsFixture)
assert.strictEqual(progressUpdate, transformationStepsFixture[0].progressUpdates?.[0])
})

it('will return undefined if step status is NOT AWAITING_CLIENT_ACTION', function () {
const transformationStepsFixture: TransformationStep[] = [
{
id: 'dummy-id',
name: 'Step name',
description: 'Step description',
status: 'TRANSFORMING',
progressUpdates: [
{
name: 'Progress update name',
status: 'SOMETHING-BESIDES-AWAITING_CLIENT_ACTION',
description: 'Progress update description',
startTime: new Date(),
endTime: new Date(),
downloadArtifacts: [
{
downloadArtifactId: 'some-download-artifact-id',
downloadArtifactType: 'some-download-artifact-type',
},
],
},
],
startTime: new Date(),
endTime: new Date(),
},
]
const progressUpdate = findDownloadArtifactProgressUpdate(transformationStepsFixture)
assert.strictEqual(progressUpdate, undefined)
})
})
})
106 changes: 68 additions & 38 deletions packages/core/src/amazonqGumby/chat/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@ import {
processSQLConversionTransformFormInput,
startTransformByQ,
stopTransformByQ,
validateCanCompileProject,
getValidSQLConversionCandidateProjects,
openHilPomFile,
} from '../../../codewhisperer/commands/startTransformByQ'
import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model'
import {
AbsolutePathDetectedError,
AlternateDependencyVersionsNotFoundError,
JavaHomeNotSetError,
JobStartError,
ModuleUploadError,
NoJavaProjectsFoundError,
Expand All @@ -59,8 +57,10 @@ import {
openBuildLogFile,
parseBuildFile,
validateSQLMetadataFile,
validateYamlFile,
} from '../../../codewhisperer/service/transformByQ/transformFileHandler'
import { getAuthType } from '../../../auth/utils'
import fs from '../../../shared/fs/fs'

// These events can be interactions within the chat,
// or elsewhere in the IDE
Expand Down Expand Up @@ -239,7 +239,7 @@ export class GumbyController {
CodeTransformTelemetryState.instance.setSessionId()

this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_TRANSFORMATION_OBJECTIVE
this.messenger.sendStaticTextResponse('choose-transformation-objective', message.tabID)
this.messenger.sendMessage(CodeWhispererConstants.chooseTransformationObjective, message.tabID, 'ai-prompt')
this.messenger.sendChatInputEnabled(message.tabID, true)
this.messenger.sendUpdatePlaceholder(
message.tabID,
Expand Down Expand Up @@ -295,7 +295,7 @@ export class GumbyController {
const validProjects = await this.validateSQLConversionProjects(message)
if (validProjects.length > 0) {
this.sessionStorage.getSession().updateCandidateProjects(validProjects)
await this.messenger.sendSelectSQLMetadataFileMessage(message.tabID)
this.messenger.sendSelectSQLMetadataFileMessage(message.tabID)
}
})
.catch((err) => {
Expand Down Expand Up @@ -385,6 +385,17 @@ export class GumbyController {
case ButtonActions.SELECT_SQL_CONVERSION_METADATA_FILE:
await this.processMetadataFile(message)
break
case ButtonActions.SELECT_CUSTOM_DEPENDENCY_VERSION_FILE:
await this.processCustomDependencyVersionFile(message)
break
case ButtonActions.CONTINUE_TRANSFORMATION_FORM:
this.messenger.sendMessage('Ok, I will continue without this information.', message.tabID, 'ai-prompt')
transformByQState.setCustomDependencyVersionFilePath('')
this.promptJavaHome('source', message.tabID)
break
case ButtonActions.AGREE_TO_LOCAL_BUILD:
await this.prepareLanguageUpgradeProject(message) // build project locally right after user agrees to do so
break
case ButtonActions.VIEW_TRANSFORMATION_HUB:
await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB, CancelActionPositions.Chat)
break
Expand All @@ -405,7 +416,7 @@ export class GumbyController {
await this.continueJobWithSelectedDependency(message)
break
case ButtonActions.CANCEL_DEPENDENCY_FORM:
this.messenger.sendUserPrompt('Cancel', message.tabID)
this.messenger.sendMessage('Cancel', message.tabID, 'prompt')
await this.continueTransformationWithoutHIL(message)
break
case ButtonActions.OPEN_FILE:
Expand Down Expand Up @@ -450,11 +461,25 @@ export class GumbyController {
})

this.messenger.sendOneOrMultipleDiffsMessage(oneOrMultipleDiffsSelection, message.tabID)
// perform local build
await this.validateBuildWithPromptOnError(message)
await this.messenger.sendCustomDependencyVersionMessage(message.tabID)
})
}

private promptJavaHome(type: 'source' | 'target', tabID: any) {
let jdkVersion = undefined
if (type === 'source') {
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_SOURCE_JAVA_HOME
jdkVersion = transformByQState.getSourceJDKVersion()
} else if (type === 'target') {
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_TARGET_JAVA_HOME
jdkVersion = transformByQState.getTargetJDKVersion()
}
const message = MessengerUtils.createJavaHomePrompt(jdkVersion)
this.messenger.sendMessage(message, tabID, 'ai-prompt')
this.messenger.sendChatInputEnabled(tabID, true)
this.messenger.sendUpdatePlaceholder(tabID, CodeWhispererConstants.enterJavaHomePlaceholder)
}

private async handleUserLanguageUpgradeProjectChoice(message: any) {
await telemetry.codeTransform_submitSelection.run(async () => {
const pathToProject: string = message.formSelectedValues['GumbyTransformLanguageUpgradeProjectForm']
Expand Down Expand Up @@ -523,15 +548,8 @@ export class GumbyController {
})
}

private async prepareLanguageUpgradeProject(message: { pathToJavaHome: string; tabID: string }) {
if (message.pathToJavaHome) {
transformByQState.setJavaHome(message.pathToJavaHome)
getLogger().info(
`CodeTransformation: using JAVA_HOME = ${transformByQState.getJavaHome()} since source JDK does not match Maven JDK`
)
}

// Pre-build project locally
private async prepareLanguageUpgradeProject(message: any) {
// build project locally
try {
this.sessionStorage.getSession().conversationState = ConversationState.COMPILING
this.messenger.sendCompilationInProgress(message.tabID)
Expand Down Expand Up @@ -567,22 +585,25 @@ export class GumbyController {
await startTransformByQ()
}

// only for Language Upgrades
private async validateBuildWithPromptOnError(message: any | undefined = undefined): Promise<void> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is not needed because we can just ask the user for their source and target JDK paths each time, rather than running a shell command just to decide whether or not to prompt them.

try {
// Check Java Home is set (not yet prebuilding)
await validateCanCompileProject()
} catch (err: any) {
if (err instanceof JavaHomeNotSetError) {
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_JAVA_HOME
this.messenger.sendStaticTextResponse('java-home-not-set', message.tabID)
this.messenger.sendChatInputEnabled(message.tabID, true)
this.messenger.sendUpdatePlaceholder(message.tabID, 'Enter the path to your Java installation.')
}
private async processCustomDependencyVersionFile(message: any) {
const fileUri = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: 'Select',
filters: {
'.YAML file': ['yaml'], // Restrict user to only pick a .yaml file
},
})
if (!fileUri || fileUri.length === 0) {
return
}

await this.prepareLanguageUpgradeProject(message)
const fileContents = await fs.readFileText(fileUri[0].fsPath)
const isValidYaml = await validateYamlFile(fileContents, message)
if (!isValidYaml) {
return
}
this.messenger.sendMessage('Received custom dependency version YAML file.', message.tabID, 'ai-prompt')
transformByQState.setCustomDependencyVersionFilePath(fileUri[0].fsPath)
this.promptJavaHome('source', message.tabID)
}

private async processMetadataFile(message: any) {
Expand Down Expand Up @@ -659,19 +680,28 @@ export class GumbyController {
}

private async processHumanChatMessage(data: { message: string; tabID: string }) {
this.messenger.sendUserPrompt(data.message, data.tabID)
this.messenger.sendMessage(data.message, data.tabID, 'prompt')
this.messenger.sendChatInputEnabled(data.tabID, false)
this.messenger.sendUpdatePlaceholder(data.tabID, 'Open a new tab to chat with Q')
this.messenger.sendUpdatePlaceholder(data.tabID, CodeWhispererConstants.openNewTabPlaceholder)

const session = this.sessionStorage.getSession()
switch (session.conversationState) {
case ConversationState.PROMPT_JAVA_HOME: {
case ConversationState.PROMPT_SOURCE_JAVA_HOME: {
const pathToJavaHome = extractPath(data.message)
if (pathToJavaHome) {
await this.prepareLanguageUpgradeProject({
pathToJavaHome,
tabID: data.tabID,
})
transformByQState.setSourceJavaHome(pathToJavaHome)
this.promptJavaHome('target', data.tabID) // get target JDK path right after saving source JDK path
} else {
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
}
break
}

case ConversationState.PROMPT_TARGET_JAVA_HOME: {
const pathToJavaHome = extractPath(data.message)
if (pathToJavaHome) {
transformByQState.setTargetJavaHome(pathToJavaHome)
this.messenger.sendPermissionToBuildMessage(data.tabID)
} else {
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
}
Expand Down Expand Up @@ -749,7 +779,7 @@ export class GumbyController {
})
}

this.messenger.sendStaticTextResponse('end-HIL-early', message.tabID)
this.messenger.sendMessage(CodeWhispererConstants.continueWithoutHilMessage, message.tabID, 'ai-prompt')
}
}

Expand Down
Loading