in packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts [325:613]
constructor(context: vscode.ExtensionContext) {
const diffModel = new DiffModel()
const transformDataProvider = new TransformationResultsProvider(diffModel)
this.changeViewer = vscode.window.createTreeView(TransformationResultsProvider.viewType, {
treeDataProvider: transformDataProvider,
})
let patchFiles: string[] = []
let singlePatchFile: string = ''
let patchFilesDescriptions: DescriptionContent | undefined = undefined
const reset = async () => {
await setContext('gumby.transformationProposalReviewInProgress', false)
await setContext('gumby.reviewState', TransformByQReviewStatus.NotStarted)
// delete result archive after changes cleared; summary is under ResultArchiveFilePath
if (fs.existsSync(transformByQState.getResultArchiveFilePath())) {
fs.rmSync(transformByQState.getResultArchiveFilePath(), { recursive: true, force: true })
}
if (fs.existsSync(transformByQState.getProjectCopyFilePath())) {
fs.rmSync(transformByQState.getProjectCopyFilePath(), { recursive: true, force: true })
}
diffModel.clearChanges()
// update summary path to where it is locally after user accepts changes, so that View Summary button works
transformByQState.setSummaryFilePath(
path.join(transformByQState.getProjectPath(), ExportResultArchiveStructure.PathToSummary)
)
transformByQState.setProjectCopyFilePath('')
transformByQState.setResultArchiveFilePath('')
transformDataProvider.refresh()
}
vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.refresh', () =>
transformDataProvider.refresh()
)
vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.reset', async () => await reset())
vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.reveal', async () => {
await setContext('gumby.transformationProposalReviewInProgress', true)
const root = diffModel.getRoot()
if (root) {
await this.changeViewer.reveal(root, {
expand: true,
})
}
})
vscode.commands.registerCommand('aws.amazonq.transformationHub.summary.reveal', async () => {
if (fs.existsSync(transformByQState.getSummaryFilePath())) {
await vscode.commands.executeCommand(
'markdown.showPreview',
vscode.Uri.file(transformByQState.getSummaryFilePath())
)
}
})
vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.startReview', async () => {
await setContext('gumby.reviewState', TransformByQReviewStatus.PreparingReview)
const pathToArchive = path.join(
ProposedTransformationExplorer.TmpDir,
transformByQState.getJobId(),
'ExportResultsArchive.zip'
)
let exportResultsArchiveSize = 0
let downloadErrorMessage = undefined
const cwStreamingClient = await createCodeWhispererChatStreamingClient()
try {
await telemetry.codeTransform_downloadArtifact.run(async () => {
telemetry.record({
codeTransformArtifactType: 'ClientInstructions',
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
codeTransformJobId: transformByQState.getJobId(),
})
await downloadExportResultArchive(
cwStreamingClient,
{
exportId: transformByQState.getJobId(),
exportIntent: ExportIntent.TRANSFORMATION,
},
pathToArchive,
AuthUtil.instance.regionProfileManager.activeRegionProfile
)
getLogger().info('CodeTransformation: downloaded results successfully')
// Update downloaded artifact size
exportResultsArchiveSize = (await fs.promises.stat(pathToArchive)).size
telemetry.record({ codeTransformTotalByteSize: exportResultsArchiveSize })
})
} catch (e: any) {
// user can retry the download
downloadErrorMessage = (e as Error).message
if (downloadErrorMessage.includes('Encountered an unexpected error when processing the request')) {
downloadErrorMessage = CodeWhispererConstants.errorDownloadingExpiredDiff
}
void vscode.window.showErrorMessage(
`${CodeWhispererConstants.errorDownloadingDiffNotification} The download failed due to: ${downloadErrorMessage}`
)
transformByQState.getChatControllers()?.transformationFinished.fire({
message: `${CodeWhispererConstants.errorDownloadingDiffChatMessage} The download failed due to: ${downloadErrorMessage}`,
tabID: ChatSessionManager.Instance.getSession().tabID,
})
await setContext('gumby.reviewState', TransformByQReviewStatus.NotStarted)
getLogger().error(`CodeTransformation: ExportResultArchive error = ${downloadErrorMessage}`)
throw new Error('Error downloading diff')
} finally {
cwStreamingClient.destroy()
UserWrittenCodeTracker.instance.onQFeatureInvoked()
}
let deserializeErrorMessage = undefined
let pathContainingArchive = ''
patchFiles = [] // reset patchFiles if there was a previous transformation
try {
// Download and deserialize the zip
pathContainingArchive = path.dirname(pathToArchive)
const zip = new AdmZip(pathToArchive)
zip.extractAllTo(pathContainingArchive)
const files = fs.readdirSync(path.join(pathContainingArchive, ExportResultArchiveStructure.PathToPatch))
if (files.length === 1) {
singlePatchFile = path.join(
pathContainingArchive,
ExportResultArchiveStructure.PathToPatch,
files[0]
)
} else {
const jsonFile = files.find((file) => file.endsWith('.json'))
if (!jsonFile) {
throw new Error('Expected JSON file not found')
}
const filePath = path.join(
pathContainingArchive,
ExportResultArchiveStructure.PathToPatch,
jsonFile
)
const jsonData = fs.readFileSync(filePath, 'utf-8')
patchFilesDescriptions = JSON.parse(jsonData)
}
if (patchFilesDescriptions !== undefined) {
for (const patchInfo of patchFilesDescriptions.content) {
patchFiles.push(
path.join(
pathContainingArchive,
ExportResultArchiveStructure.PathToPatch,
patchInfo.filename
)
)
}
} else {
patchFiles.push(singlePatchFile)
}
// Because multiple patches are returned once the ZIP is downloaded, we want to show the first one to start
diffModel.parseDiff(
patchFiles[0],
transformByQState.getProjectPath(),
patchFilesDescriptions ? patchFilesDescriptions.content[0] : undefined,
patchFiles.length
)
await setContext('gumby.reviewState', TransformByQReviewStatus.InReview)
transformDataProvider.refresh()
transformByQState.setSummaryFilePath(
path.join(pathContainingArchive, ExportResultArchiveStructure.PathToSummary)
)
transformByQState.setResultArchiveFilePath(pathContainingArchive)
await setContext('gumby.isSummaryAvailable', true)
// Do not await this so that the summary reveals without user needing to close this notification
void vscode.window.showInformationMessage(CodeWhispererConstants.viewProposedChangesNotification)
transformByQState.getChatControllers()?.transformationFinished.fire({
message: CodeWhispererConstants.viewProposedChangesChatMessage,
tabID: ChatSessionManager.Instance.getSession().tabID,
})
await vscode.commands.executeCommand('aws.amazonq.transformationHub.summary.reveal')
} catch (e: any) {
deserializeErrorMessage = (e as Error).message
getLogger().error(`CodeTransformation: ParseDiff error = ${deserializeErrorMessage}`)
transformByQState.getChatControllers()?.transformationFinished.fire({
message: `${CodeWhispererConstants.errorDeserializingDiffChatMessage} ${deserializeErrorMessage}`,
tabID: ChatSessionManager.Instance.getSession().tabID,
})
void vscode.window.showErrorMessage(
`${CodeWhispererConstants.errorDeserializingDiffNotification} ${deserializeErrorMessage}`
)
}
try {
const metricsPath = path.join(pathContainingArchive, ExportResultArchiveStructure.PathToMetrics)
const metricsData = JSON.parse(fs.readFileSync(metricsPath, 'utf8'))
await codeWhisperer.codeWhispererClient.sendTelemetryEvent({
telemetryEvent: {
transformEvent: {
jobId: transformByQState.getJobId(),
timestamp: new Date(),
ideCategory: 'VSCODE',
programmingLanguage: {
languageName:
transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE
? 'java'
: 'sql',
},
linesOfCodeChanged: metricsData.linesOfCodeChanged,
charsOfCodeChanged: metricsData.charactersOfCodeChanged,
linesOfCodeSubmitted: transformByQState.getLinesOfCodeSubmitted(), // currently unavailable for SQL conversions
},
},
})
} catch (err: any) {
// log error, but continue to show user diff.patch with results
getLogger().error(`CodeTransformation: SendTelemetryEvent error = ${err.message}`)
}
})
vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.acceptChanges', async () => {
telemetry.codeTransform_submitSelection.run(() => {
getLogger().info('CodeTransformation: accepted changes')
diffModel.saveChanges()
telemetry.record({
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
codeTransformJobId: transformByQState.getJobId(),
userChoice: `acceptChanges-${patchFilesDescriptions?.content[diffModel.currentPatchIndex].name}`,
})
})
if (transformByQState.getMultipleDiffs()) {
void vscode.window.showInformationMessage(
CodeWhispererConstants.changesAppliedNotificationMultipleDiffs(
diffModel.currentPatchIndex,
patchFiles.length
)
)
} else {
void vscode.window.showInformationMessage(CodeWhispererConstants.changesAppliedNotificationOneDiff)
}
// We do this to ensure that the changesAppliedChatMessage is only sent to user when they accept the first diff.patch
transformByQState.getChatControllers()?.transformationFinished.fire({
message: CodeWhispererConstants.changesAppliedChatMessageMultipleDiffs(
diffModel.currentPatchIndex,
patchFiles.length,
patchFilesDescriptions
? patchFilesDescriptions.content[diffModel.currentPatchIndex].name
: undefined
),
tabID: ChatSessionManager.Instance.getSession().tabID,
includeStartNewTransformationButton: diffModel.currentPatchIndex === patchFiles.length - 1,
})
// Load the next patch file
diffModel.currentPatchIndex++
if (diffModel.currentPatchIndex < patchFiles.length) {
const nextPatchFile = patchFiles[diffModel.currentPatchIndex]
const nextPatchFileDescription = patchFilesDescriptions
? patchFilesDescriptions.content[diffModel.currentPatchIndex]
: undefined
diffModel.parseDiff(
nextPatchFile,
transformByQState.getProjectPath(),
nextPatchFileDescription,
patchFiles.length
)
transformDataProvider.refresh()
} else {
// All patches have been applied, reset the state
await reset()
}
})
vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.rejectChanges', async () => {
await telemetry.codeTransform_submitSelection.run(async () => {
getLogger().info('CodeTransformation: rejected changes')
diffModel.rejectChanges()
await reset()
telemetry.record({
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
codeTransformJobId: transformByQState.getJobId(),
userChoice: 'rejectChanges',
})
})
transformByQState.getChatControllers()?.transformationFinished.fire({
tabID: ChatSessionManager.Instance.getSession().tabID,
})
})
}