Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c53fdb9
introduce file level accept action button
kelvin-klchu Oct 28, 2024
1e8b744
open file after accept
kelvin-klchu Oct 28, 2024
79d0d11
disable file options
kelvin-klchu Oct 28, 2024
00a7d41
disable previous code result actions when providing feedback
kelvin-klchu Oct 29, 2024
746ad37
fix style for deleted files
kelvin-klchu Oct 29, 2024
79c6a2f
disable previous file list on new session
kelvin-klchu Oct 29, 2024
6f4a181
string updates on accept code buttons
kelvin-klchu Oct 31, 2024
d77ebf3
auto trigger next step if all files accepted
kelvin-klchu Oct 31, 2024
d7731cc
open deleted file in a diff
kelvin-klchu Oct 31, 2024
644830e
only shows revert-rejection action when a file is rejected
kelvin-klchu Oct 31, 2024
fa18f22
fix edge case for triggering next step
kelvin-klchu Oct 31, 2024
0647654
update openFile behaviour
kelvin-klchu Oct 31, 2024
312b460
minor string updates
kelvin-klchu Oct 31, 2024
9d53822
fix diff test
kelvin-klchu Nov 1, 2024
bccc542
fix diff test
kelvin-klchu Nov 4, 2024
e1befb0
replace disablePreviousFileList with session.disableFileList from con…
kelvin-klchu Nov 4, 2024
b022cfa
update controller tests
kelvin-klchu Nov 5, 2024
9e821bd
update controller tests
kelvin-klchu Nov 5, 2024
aa1b295
chore
kelvin-klchu Nov 5, 2024
2c89a97
update accept code telemetry for file-level accepts
kelvin-klchu Nov 6, 2024
4024dbe
update i18n and minor fixes
kelvin-klchu Nov 6, 2024
d24234a
fix rebase
kelvin-klchu Nov 7, 2024
77958b2
remove disableFileList test from controller
kelvin-klchu Nov 7, 2024
9f4786e
add e2e tests for file-level accepts
kelvin-klchu Nov 7, 2024
47e1eea
update e2e tests for file-level accepts
kelvin-klchu Nov 7, 2024
1084921
upgrade mynah-ui version
kelvin-klchu Nov 8, 2024
39e5a8f
address PR comment
kelvin-klchu Nov 8, 2024
40364af
update button text for the case to continue
kelvin-klchu Nov 8, 2024
c25a918
minor fix for CodeIterationLimitError
kelvin-klchu Nov 8, 2024
c01d330
refactor insert code pill text logic
kelvin-klchu Nov 8, 2024
494ae85
update e2e test clickActionButton wait time
kelvin-klchu Nov 8, 2024
ee844a4
update e2e test clickActionButton wait time
kelvin-klchu Nov 8, 2024
e16ce6e
add changelog
kelvin-klchu Nov 12, 2024
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
12 changes: 8 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "Amazon Q /dev: Add an action to accept individual files"
}
127 changes: 126 additions & 1 deletion packages/amazonq/test/e2e/amazonq/featureDev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ describe('Amazon Q Feature Dev', function () {
let tab: Messenger

const prompt = 'Add blank.txt file with empty content'
const codegenApproachPrompt = prompt + ' and add a readme that describes the changes'
const codegenApproachPrompt = `${prompt} and add a readme that describes the changes`
const fileLevelAcceptPrompt = `${prompt} and add a license, and a contributing file`
const tooManyRequestsWaitTime = 100000

function waitForButtons(buttons: FollowUpTypes[]) {
Expand Down Expand Up @@ -50,6 +51,14 @@ describe('Amazon Q Feature Dev', function () {
)
}

async function clickActionButton(filePath: string, actionName: string) {
tab.clickFileActionButton(filePath, actionName)
await tab.waitForEvent(() => !tab.hasAction(filePath, actionName), {
waitIntervalInMs: 500,
waitTimeoutInMs: 600000,
})
}

/**
* Wait for the original request to finish.
* If the response has a retry button or encountered a guardrails error, continue retrying
Expand Down Expand Up @@ -216,4 +225,120 @@ describe('Amazon Q Feature Dev', function () {
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])
})
})

describe('file-level accepts', async () => {
beforeEach(async function () {
tab.addChatMessage({ command: '/dev', prompt: fileLevelAcceptPrompt })
await retryIfRequired(
async () => {
await tab.waitForChatFinishesLoading()
},
() => {
tab.addChatMessage({ prompt })
}
)
await retryIfRequired(async () => {
await Promise.any([
waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]),
waitForButtons([FollowUpTypes.Retry]),
])
})
})

describe('fileList', async () => {
it('has both accept-change and reject-change action buttons for file', async () => {
const filePath = tab.getFilePaths()[0]
assert.ok(tab.getActionsByFilePath(filePath).length === 2)
assert.ok(tab.hasAction(filePath, 'accept-change'))
assert.ok(tab.hasAction(filePath, 'reject-change'))
})

it('has only revert-rejection action button for rejected file', async () => {
const filePath = tab.getFilePaths()[0]
await clickActionButton(filePath, 'reject-change')

assert.ok(tab.getActionsByFilePath(filePath).length === 1)
assert.ok(tab.hasAction(filePath, 'revert-rejection'))
})

it('does not have any of the action buttons for accepted file', async () => {
const filePath = tab.getFilePaths()[0]
await clickActionButton(filePath, 'accept-change')

assert.ok(tab.getActionsByFilePath(filePath).length === 0)
})

it('disables all action buttons when new task is clicked', async () => {
tab.clickButton(FollowUpTypes.InsertCode)
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])
tab.clickButton(FollowUpTypes.NewTask)
await waitForText('What new task would you like to work on?')

const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
assert.ok(tab.getActionsByFilePath(filePath).length === 0)
}
})

it('disables all action buttons when close session is clicked', async () => {
tab.clickButton(FollowUpTypes.InsertCode)
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])
tab.clickButton(FollowUpTypes.CloseSession)
await waitForText(
"Okay, I've ended this chat session. You can open a new tab to chat or start another workflow."
)

const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
assert.ok(tab.getActionsByFilePath(filePath).length === 0)
}
})
})

describe('accept button', async () => {
describe('button text', async () => {
it('shows "Accept all changes" when no files are accepted or rejected, and "Accept remaining changes" otherwise', async () => {
let insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept all changes')

const filePath = tab.getFilePaths()[0]
await clickActionButton(filePath, 'reject-change')

insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept remaining changes')

await clickActionButton(filePath, 'revert-rejection')

insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept all changes')

await clickActionButton(filePath, 'accept-change')

insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Accept remaining changes')
})

it('shows "Continue" when all files are either accepted or rejected, with at least one of them rejected', async () => {
const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
await clickActionButton(filePath, 'reject-change')
}

const insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode)
assert.ok(insertCodeButton.pillText === 'Continue')
})
})

it('disappears and automatically moves on to the next step when all changes are accepted', async () => {
const filePaths = tab.getFilePaths()
for (const filePath of filePaths) {
await clickActionButton(filePath, 'accept-change')
}
await waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession])

assert.ok(tab.hasButton(FollowUpTypes.InsertCode) === false)
assert.ok(tab.hasButton(FollowUpTypes.ProvideFeedbackAndRegenerateCode) === false)
})
})
})
})
58 changes: 58 additions & 0 deletions packages/amazonq/test/e2e/amazonq/framework/messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export class Messenger {
this.mynahUIProps.onFollowUpClicked(this.tabID, lastChatItem?.messageId ?? '', option[0])
}

clickFileActionButton(filePath: string, actionName: string) {
if (!this.mynahUIProps.onFileActionClick) {
assert.fail('onFileActionClick must be defined to use it in the tests')
}

this.mynahUIProps.onFileActionClick(this.tabID, this.getFileListMessageId(), filePath, actionName)
}

findCommand(command: string) {
return this.getCommands()
.map((groups) => groups.commands)
Expand All @@ -78,6 +86,52 @@ export class Messenger {
return this.getStore().promptInputPlaceholder
}

getFollowUpButton(type: FollowUpTypes) {
const followUpButton = this.getChatItems()
.pop()
?.followUp?.options?.find((action) => action.type === type)
if (!followUpButton) {
assert.fail(`Could not find follow up button with type ${type}`)
}
return followUpButton
}

getFileList() {
const chatItems = this.getChatItems()
const fileList = chatItems.find((item) => 'fileList' in item)
if (!fileList) {
assert.fail('Could not find file list')
}
return fileList
}

getFileListMessageId() {
const fileList = this.getFileList()
const messageId = fileList?.messageId
if (!messageId) {
assert.fail('Could not find file list message id')
}
return messageId
}

getFilePaths() {
const fileList = this.getFileList()
const filePaths = fileList?.fileList?.filePaths
if (!filePaths) {
assert.fail('Could not find file paths')
}
if (filePaths.length === 0) {
assert.fail('File paths list is empty')
}
return filePaths
}

getActionsByFilePath(filePath: string) {
const fileList = this.getFileList()
const actions = fileList?.fileList?.actions
return actions?.[filePath] ?? []
}

hasButton(type: FollowUpTypes) {
return (
this.getChatItems()
Expand All @@ -87,6 +141,10 @@ export class Messenger {
)
}

hasAction(filePath: string, actionName: string) {
return this.getActionsByFilePath(filePath).some((action) => action.name === actionName)
}

async waitForChatFinishesLoading() {
return this.waitForEvent(() => this.getStore().loadingChat === false || this.hasButton(FollowUpTypes.Retry))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe('session', () => {
rejected: false,
virtualMemoryUri: uri,
workspaceFolder: controllerSetup.workspaceFolder,
changeApplied: false,
},
{
zipFilePath: 'rejectedFile.js',
Expand All @@ -91,6 +92,7 @@ describe('session', () => {
rejected: true,
virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'rejectedFile.js'),
workspaceFolder: controllerSetup.workspaceFolder,
changeApplied: false,
},
],
[],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@
"@aws-sdk/property-provider": "3.46.0",
"@aws-sdk/smithy-client": "^3.46.0",
"@aws-sdk/util-arn-parser": "^3.46.0",
"@aws/mynah-ui": "^4.15.11",
"@aws/mynah-ui": "^4.18.0",
"@gerhobbelt/gitignore-parser": "^0.2.0-9",
"@iarna/toml": "^2.2.5",
"@smithy/middleware-retry": "^2.3.1",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@
"AWS.amazonq.featureDev.pillText.generatingCode": "Generating code...",
"AWS.amazonq.featureDev.pillText.requestingChanges": "Requesting changes ...",
"AWS.amazonq.featureDev.pillText.insertCode": "Accept code",
"AWS.amazonq.featureDev.pillText.continue": "Continue",
"AWS.amazonq.featureDev.pillText.acceptAllChanges": "Accept all changes",
"AWS.amazonq.featureDev.pillText.acceptRemainingChanges": "Accept remaining changes",
"AWS.amazonq.featureDev.pillText.stoppingCodeGeneration": "Stopping code generation...",
"AWS.amazonq.featureDev.pillText.sendFeedback": "Send feedback",
"AWS.amazonq.featureDev.pillText.selectFiles": "Select files for context",
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/amazonq/commons/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export async function openDiff(leftPath: string, rightPath: string, tabId: strin
}

export async function openDeletedDiff(filePath: string, name: string, tabId: string) {
const fileUri = await getOriginalFileUri(filePath, tabId)
await vscode.commands.executeCommand('vscode.open', fileUri, {}, `${name} (Deleted)`)
const left = await getOriginalFileUri(filePath, tabId)
const right = createAmazonQUri('empty', tabId)
await vscode.commands.executeCommand('vscode.diff', left, right, `${name} (Deleted)`)
}

export async function getOriginalFileUri(fullPath: string, tabId: string) {
Expand All @@ -32,3 +33,11 @@ export function createAmazonQUri(path: string, tabId: string) {
// TODO change the featureDevScheme to a more general amazon q scheme
return vscode.Uri.from({ scheme: featureDevScheme, path, query: `tabID=${tabId}` })
}

export async function openFile(path: string) {
if (!(await fs.exists(path))) {
return
}
const fileUri = vscode.Uri.file(path)
await vscode.commands.executeCommand('vscode.diff', fileUri, fileUri)
}
9 changes: 8 additions & 1 deletion packages/core/src/amazonq/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ export { amazonQHelpUrl } from '../shared/constants'
export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/statusBarMenu'
export { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands'
export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens'
export { createAmazonQUri, openDiff, openDeletedDiff, getOriginalFileUri, getFileDiffUris } from './commons/diff'
export {
createAmazonQUri,
openDiff,
openDeletedDiff,
getOriginalFileUri,
getFileDiffUris,
openFile,
} from './commons/diff'
export { CodeReference } from '../codewhispererChat/view/connector/connector'
export { AuthMessageDataMap, AuthFollowUpType } from './auth/model'
export { extractAuthFollowUp } from './util/authUtils'
Expand Down
Loading
Loading