Skip to content

Commit ac72bca

Browse files
authored
feat(amazon q): display multiple diff patches (#5812)
adding functionality for selective transformation to allow for java 17 as source and display multiple diff patches for the user to accept/reject
1 parent 817ebac commit ac72bca

File tree

12 files changed

+435
-66
lines changed

12 files changed

+435
-66
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"content": [
3+
{
4+
"name": "Added file",
5+
"fileName": "resources/files/addedFile.diff",
6+
"isSuccessful": true
7+
}
8+
]
9+
}

packages/amazonq/test/unit/amazonqGumby/transformationResultsHandler.test.ts

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
*/
55
import assert from 'assert'
66
import sinon from 'sinon'
7-
import os from 'os'
87
import { DiffModel, AddedChangeNode, ModifiedChangeNode } from 'aws-core-vscode/codewhisperer/node'
8+
import { DescriptionContent } from 'aws-core-vscode/codewhisperer'
99
import path from 'path'
1010
import { getTestResourceFilePath } from './amazonQGumbyUtil'
1111
import { fs } from 'aws-core-vscode/shared'
12+
import { createTestWorkspace } from 'aws-core-vscode/test'
1213

1314
describe('DiffModel', function () {
15+
let parsedTestDescriptions: DescriptionContent
16+
beforeEach(async () => {
17+
parsedTestDescriptions = JSON.parse(await fs.readFileText(getTestResourceFilePath('resources/files/diff.json')))
18+
})
19+
1420
afterEach(() => {
1521
sinon.restore()
1622
})
@@ -28,34 +34,76 @@ describe('DiffModel', function () {
2834

2935
return true
3036
})
37+
testDiffModel.parseDiff(
38+
getTestResourceFilePath('resources/files/addedFile.diff'),
39+
workspacePath,
40+
parsedTestDescriptions.content[0],
41+
1
42+
)
3143

32-
testDiffModel.parseDiff(getTestResourceFilePath('resources/files/addedFile.diff'), workspacePath)
33-
34-
assert.strictEqual(testDiffModel.changes.length, 1)
35-
const change = testDiffModel.changes[0]
44+
assert.strictEqual(
45+
testDiffModel.patchFileNodes[0].patchFilePath,
46+
getTestResourceFilePath('resources/files/addedFile.diff')
47+
)
48+
assert(testDiffModel.patchFileNodes[0].label.includes(parsedTestDescriptions.content[0].name))
49+
const change = testDiffModel.patchFileNodes[0].children[0]
3650

3751
assert.strictEqual(change instanceof AddedChangeNode, true)
3852
})
3953

4054
it('WHEN parsing a diff patch where a file was modified THEN returns an array representing the modified file', async function () {
4155
const testDiffModel = new DiffModel()
4256

43-
const workspacePath = os.tmpdir()
44-
45-
sinon.replace(fs, 'exists', async (path) => true)
57+
const fileAmount = 1
58+
const workspaceFolder = await createTestWorkspace(fileAmount, { fileContent: '' })
4659

4760
await fs.writeFile(
48-
path.join(workspacePath, 'README.md'),
61+
path.join(workspaceFolder.uri.fsPath, 'README.md'),
4962
'This guide walks you through using Gradle to build a simple Java project.'
5063
)
5164

52-
testDiffModel.parseDiff(getTestResourceFilePath('resources/files/modifiedFile.diff'), workspacePath)
65+
testDiffModel.parseDiff(
66+
getTestResourceFilePath('resources/files/modifiedFile.diff'),
67+
workspaceFolder.uri.fsPath,
68+
parsedTestDescriptions.content[0],
69+
1
70+
)
5371

54-
assert.strictEqual(testDiffModel.changes.length, 1)
55-
const change = testDiffModel.changes[0]
72+
assert.strictEqual(
73+
testDiffModel.patchFileNodes[0].patchFilePath,
74+
getTestResourceFilePath('resources/files/modifiedFile.diff')
75+
)
76+
assert(testDiffModel.patchFileNodes[0].label.includes(parsedTestDescriptions.content[0].name))
77+
const change = testDiffModel.patchFileNodes[0].children[0]
5678

5779
assert.strictEqual(change instanceof ModifiedChangeNode, true)
80+
})
81+
82+
it('WHEN parsing a diff patch where diff.json is not present and a file was modified THEN returns an array representing the modified file', async function () {
83+
const testDiffModel = new DiffModel()
5884

59-
await fs.delete(path.join(workspacePath, 'README.md'), { recursive: true })
85+
const fileAmount = 1
86+
const workspaceFolder = await createTestWorkspace(fileAmount, { fileContent: '' })
87+
88+
await fs.writeFile(
89+
path.join(workspaceFolder.uri.fsPath, 'README.md'),
90+
'This guide walks you through using Gradle to build a simple Java project.'
91+
)
92+
93+
testDiffModel.parseDiff(
94+
getTestResourceFilePath('resources/files/modifiedFile.diff'),
95+
workspaceFolder.uri.fsPath,
96+
undefined,
97+
1
98+
)
99+
100+
assert.strictEqual(
101+
testDiffModel.patchFileNodes[0].patchFilePath,
102+
getTestResourceFilePath('resources/files/modifiedFile.diff')
103+
)
104+
assert(testDiffModel.patchFileNodes[0].label.endsWith('modifiedFile.diff'))
105+
const change = testDiffModel.patchFileNodes[0].children[0]
106+
107+
assert.strictEqual(change instanceof ModifiedChangeNode, true)
60108
})
61109
})

packages/core/src/amazonqGumby/chat/controller/controller.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { getStringHash } from '../../../shared/utilities/textUtilities'
6262
import { getVersionData } from '../../../codewhisperer/service/transformByQ/transformMavenHandler'
6363
import AdmZip from 'adm-zip'
6464
import { AuthError } from '../../../auth/sso/server'
65+
import { isSelectiveTransformationReady } from '../../../dev/config'
6566

6667
// These events can be interactions within the chat,
6768
// or elsewhere in the IDE
@@ -358,6 +359,7 @@ export class GumbyController {
358359
this.transformationFinished({
359360
message: CodeWhispererConstants.jobCancelledChatMessage,
360361
tabID: message.tabID,
362+
includeStartNewTransformationButton: true,
361363
})
362364
break
363365
case ButtonActions.CONFIRM_SKIP_TESTS_FORM:
@@ -366,6 +368,12 @@ export class GumbyController {
366368
case ButtonActions.CANCEL_SKIP_TESTS_FORM:
367369
this.messenger.sendJobFinishedMessage(message.tabID, CodeWhispererConstants.jobCancelledChatMessage)
368370
break
371+
case ButtonActions.CONFIRM_SELECTIVE_TRANSFORMATION_FORM:
372+
await this.handleOneOrMultipleDiffs(message)
373+
break
374+
case ButtonActions.CANCEL_SELECTIVE_TRANSFORMATION_FORM:
375+
this.messenger.sendJobFinishedMessage(message.tabID, CodeWhispererConstants.jobCancelledChatMessage)
376+
break
369377
case ButtonActions.CONFIRM_SQL_CONVERSION_TRANSFORMATION_FORM:
370378
await this.handleUserSQLConversionProjectSelection(message)
371379
break
@@ -416,6 +424,20 @@ export class GumbyController {
416424
result: MetadataResult.Pass,
417425
})
418426
this.messenger.sendSkipTestsSelectionMessage(skipTestsSelection, message.tabID)
427+
if (!isSelectiveTransformationReady) {
428+
// perform local build
429+
await this.validateBuildWithPromptOnError(message)
430+
} else {
431+
await this.messenger.sendOneOrMultipleDiffsPrompt(message.tabID)
432+
}
433+
}
434+
435+
private async handleOneOrMultipleDiffs(message: any) {
436+
const oneOrMultipleDiffsSelection = message.formSelectedValues['GumbyTransformOneOrMultipleDiffsForm']
437+
if (oneOrMultipleDiffsSelection === CodeWhispererConstants.multipleDiffsMessage) {
438+
transformByQState.setMultipleDiffs(true)
439+
}
440+
this.messenger.sendOneOrMultipleDiffsMessage(oneOrMultipleDiffsSelection, message.tabID)
419441
// perform local build
420442
await this.validateBuildWithPromptOnError(message)
421443
}
@@ -452,7 +474,6 @@ export class GumbyController {
452474
this.messenger.sendUnrecoverableErrorResponse('unsupported-source-jdk-version', message.tabID)
453475
return
454476
}
455-
456477
await processLanguageUpgradeTransformFormInput(pathToProject, fromJDKVersion, toJDKVersion)
457478
await this.messenger.sendSkipTestsPrompt(message.tabID)
458479
})
@@ -563,6 +584,7 @@ export class GumbyController {
563584
this.transformationFinished({
564585
message: CodeWhispererConstants.jobCancelledChatMessage,
565586
tabID: message.tabID,
587+
includeStartNewTransformationButton: true,
566588
})
567589
return
568590
}
@@ -591,11 +613,15 @@ export class GumbyController {
591613
)
592614
}
593615

594-
private transformationFinished(data: { message: string | undefined; tabID: string }) {
616+
private transformationFinished(data: {
617+
message: string | undefined
618+
tabID: string
619+
includeStartNewTransformationButton: boolean
620+
}) {
595621
this.resetTransformationChatFlow()
596622
// at this point job is either completed, partially_completed, cancelled, or failed
597623
if (data.message) {
598-
this.messenger.sendJobFinishedMessage(data.tabID, data.message)
624+
this.messenger.sendJobFinishedMessage(data.tabID, data.message, data.includeStartNewTransformationButton)
599625
}
600626
}
601627

@@ -701,7 +727,11 @@ export class GumbyController {
701727
try {
702728
await finishHumanInTheLoop()
703729
} catch (err: any) {
704-
this.transformationFinished({ tabID: message.tabID, message: (err as Error).message })
730+
this.transformationFinished({
731+
tabID: message.tabID,
732+
message: (err as Error).message,
733+
includeStartNewTransformationButton: true,
734+
})
705735
}
706736

707737
this.messenger.sendStaticTextResponse('end-HIL-early', message.tabID)

packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,45 @@ export class Messenger {
155155
)
156156
}
157157

158+
public async sendOneOrMultipleDiffsPrompt(tabID: string) {
159+
const formItems: ChatItemFormItem[] = []
160+
formItems.push({
161+
id: 'GumbyTransformOneOrMultipleDiffsForm',
162+
type: 'select',
163+
title: CodeWhispererConstants.selectiveTransformationFormTitle,
164+
mandatory: true,
165+
options: [
166+
{
167+
value: CodeWhispererConstants.oneDiffMessage,
168+
label: CodeWhispererConstants.oneDiffMessage,
169+
},
170+
{
171+
value: CodeWhispererConstants.multipleDiffsMessage,
172+
label: CodeWhispererConstants.multipleDiffsMessage,
173+
},
174+
],
175+
})
176+
177+
this.dispatcher.sendAsyncEventProgress(
178+
new AsyncEventProgressMessage(tabID, {
179+
inProgress: true,
180+
message: CodeWhispererConstants.userPatchDescriptionChatMessage,
181+
})
182+
)
183+
184+
this.dispatcher.sendChatPrompt(
185+
new ChatPrompt(
186+
{
187+
message: 'Q Code Transformation',
188+
formItems: formItems,
189+
},
190+
'TransformOneOrMultipleDiffsForm',
191+
tabID,
192+
false
193+
)
194+
)
195+
}
196+
158197
public async sendLanguageUpgradeProjectPrompt(projects: TransformationCandidateProject[], tabID: string) {
159198
const projectFormOptions: { value: any; label: string }[] = []
160199
const detectedJavaVersions = new Array<JDKVersion | undefined>()
@@ -367,7 +406,6 @@ export class Messenger {
367406
},
368407
tabID
369408
)
370-
371409
this.dispatcher.sendChatMessage(jobSubmittedMessage)
372410
}
373411

@@ -477,13 +515,15 @@ export class Messenger {
477515
this.dispatcher.sendCommandMessage(new SendCommandMessage(message.command, message.tabID, message.eventId))
478516
}
479517

480-
public sendJobFinishedMessage(tabID: string, message: string) {
518+
public sendJobFinishedMessage(tabID: string, message: string, includeStartNewTransformationButton: boolean = true) {
481519
const buttons: ChatItemButton[] = []
482-
buttons.push({
483-
keepCardAfterClick: false,
484-
text: CodeWhispererConstants.startTransformationButtonText,
485-
id: ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW,
486-
})
520+
if (includeStartNewTransformationButton) {
521+
buttons.push({
522+
keepCardAfterClick: false,
523+
text: CodeWhispererConstants.startTransformationButtonText,
524+
id: ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW,
525+
})
526+
}
487527

488528
this.dispatcher.sendChatMessage(
489529
new ChatMessage(
@@ -562,6 +602,11 @@ export class Messenger {
562602
this.dispatcher.sendChatMessage(new ChatMessage({ message, messageType: 'ai-prompt' }, tabID))
563603
}
564604

605+
public sendOneOrMultipleDiffsMessage(selectiveTransformationSelection: string, tabID: string) {
606+
const message = `Okay, I will create ${selectiveTransformationSelection.toLowerCase()} when providing the proposed changes.`
607+
this.dispatcher.sendChatMessage(new ChatMessage({ message, messageType: 'ai-prompt' }, tabID))
608+
}
609+
565610
public sendHumanInTheLoopInitialMessage(tabID: string, codeSnippet: string) {
566611
let message = `I was not able to upgrade all dependencies. To resolve it, I will try to find an updated depedency in your local Maven repository. I will need additional information from you to continue.`
567612

packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export enum ButtonActions {
1818
CANCEL_TRANSFORMATION_FORM = 'gumbyTransformFormCancel', // shared between Language Upgrade & SQL Conversion
1919
CONFIRM_SKIP_TESTS_FORM = 'gumbyTransformSkipTestsFormConfirm',
2020
CANCEL_SKIP_TESTS_FORM = 'gumbyTransformSkipTestsFormCancel',
21+
CONFIRM_SELECTIVE_TRANSFORMATION_FORM = 'gumbyTransformOneOrMultipleDiffsFormConfirm',
22+
CANCEL_SELECTIVE_TRANSFORMATION_FORM = 'gumbyTransformOneOrMultipleDiffsFormCancel',
2123
SELECT_SQL_CONVERSION_METADATA_FILE = 'gumbySQLConversionMetadataTransformFormConfirm',
2224
CONFIRM_DEPENDENCY_FORM = 'gumbyTransformDependencyFormConfirm',
2325
CANCEL_DEPENDENCY_FORM = 'gumbyTransformDependencyFormCancel',

packages/core/src/codewhisperer/commands/startTransformByQ.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -811,15 +811,17 @@ export async function postTransformationJob() {
811811
}
812812

813813
let chatMessage = transformByQState.getJobFailureErrorChatMessage()
814+
const diffMessage = CodeWhispererConstants.diffMessage(transformByQState.getMultipleDiffs())
814815
if (transformByQState.isSucceeded()) {
815-
chatMessage = CodeWhispererConstants.jobCompletedChatMessage
816+
chatMessage = CodeWhispererConstants.jobCompletedChatMessage(diffMessage)
816817
} else if (transformByQState.isPartiallySucceeded()) {
817-
chatMessage = CodeWhispererConstants.jobPartiallyCompletedChatMessage
818+
chatMessage = CodeWhispererConstants.jobPartiallyCompletedChatMessage(diffMessage)
818819
}
819820

820-
transformByQState
821-
.getChatControllers()
822-
?.transformationFinished.fire({ message: chatMessage, tabID: ChatSessionManager.Instance.getSession().tabID })
821+
transformByQState.getChatControllers()?.transformationFinished.fire({
822+
message: chatMessage,
823+
tabID: ChatSessionManager.Instance.getSession().tabID,
824+
})
823825
const durationInMs = calculateTotalLatency(CodeTransformTelemetryState.instance.getStartTime())
824826
const resultStatusMessage = transformByQState.getStatus()
825827

@@ -842,11 +844,11 @@ export async function postTransformationJob() {
842844
}
843845

844846
if (transformByQState.isSucceeded()) {
845-
void vscode.window.showInformationMessage(CodeWhispererConstants.jobCompletedNotification)
847+
void vscode.window.showInformationMessage(CodeWhispererConstants.jobCompletedNotification(diffMessage))
846848
} else if (transformByQState.isPartiallySucceeded()) {
847849
void vscode.window
848850
.showInformationMessage(
849-
CodeWhispererConstants.jobPartiallyCompletedNotification,
851+
CodeWhispererConstants.jobPartiallyCompletedNotification(diffMessage),
850852
CodeWhispererConstants.amazonQFeedbackText
851853
)
852854
.then((choice) => {

0 commit comments

Comments
 (0)