Skip to content

Commit b2a0bd9

Browse files
jpinkney-awsDiana Abitova
andauthored
feat: Introduce file by file acceptance (#4523)
* feat: Introduce file by file acceptance Problem: - We don't have a way for users to individually select which files should be accepted after code generation Solution: - Allow users to reject files from being accepted after code generation Co-authored-by: Diana Abitova <[email protected]> Co-authored-by: Josh Pinkney <[email protected]>
1 parent 1b16303 commit b2a0bd9

File tree

17 files changed

+207
-28
lines changed

17 files changed

+207
-28
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4241,7 +4241,7 @@
42414241
},
42424242
"devDependencies": {
42434243
"@aws-sdk/types": "^3.13.1",
4244-
"@aws-toolkits/telemetry": "^1.0.192",
4244+
"@aws-toolkits/telemetry": "^1.0.193",
42454245
"@aws/fully-qualified-names": "^2.1.4",
42464246
"@cspotcode/source-map-support": "^0.8.1",
42474247
"@sinonjs/fake-timers": "^10.0.2",

packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ExtensionMessage } from '../commands'
88
import { TabType, TabsStorage } from '../storages/tabsStorage'
99
import { CodeReference } from './amazonqCommonsConnector'
1010
import { FollowUpGenerator } from '../followUps/generator'
11+
import { getActions } from '../diffTree/actions'
12+
import { DiffTreeFileInfo } from '../diffTree/types'
1113

1214
interface ChatPayload {
1315
chatMessage: string
@@ -21,6 +23,8 @@ export interface ConnectorProps {
2123
sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined
2224
onError: (tabID: string, message: string, title: string) => void
2325
onWarning: (tabID: string, message: string, title: string) => void
26+
onFileComponentUpdate: (tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[]) => void
27+
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void
2428
onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
2529
onChatInputEnabled: (tabID: string, enabled: boolean) => void
2630
onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void
@@ -32,6 +36,7 @@ export class Connector {
3236
private readonly sendMessageToExtension
3337
private readonly onError
3438
private readonly onWarning
39+
private readonly onFileComponentUpdate
3540
private readonly onChatAnswerReceived
3641
private readonly onAsyncEventProgress
3742
private readonly updatePlaceholder
@@ -44,6 +49,7 @@ export class Connector {
4449
this.sendMessageToExtension = props.sendMessageToExtension
4550
this.onChatAnswerReceived = props.onChatAnswerReceived
4651
this.onWarning = props.onWarning
52+
this.onFileComponentUpdate = props.onFileComponentUpdate
4753
this.onError = props.onError
4854
this.onAsyncEventProgress = props.onAsyncEventProgress
4955
this.updatePlaceholder = props.onUpdatePlaceholder
@@ -92,6 +98,16 @@ export class Connector {
9298
tabType: 'featuredev',
9399
})
94100
}
101+
onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => {
102+
this.sendMessageToExtension({
103+
command: 'file-click',
104+
tabID,
105+
messageId,
106+
filePath,
107+
actionName,
108+
tabType: 'featuredev',
109+
})
110+
}
95111

96112
followUpClicked = (tabID: string, followUp: ChatItemAction): void => {
97113
this.sendMessageToExtension({
@@ -137,6 +153,7 @@ export class Connector {
137153

138154
private processCodeResultMessage = async (messageData: any): Promise<void> => {
139155
if (this.onChatAnswerReceived !== undefined) {
156+
const actions = getActions([...messageData.filePaths, ...messageData.deletedFiles])
140157
const answer: ChatItem = {
141158
type: ChatItemType.CODE_RESULT,
142159
relatedContent: undefined,
@@ -146,8 +163,9 @@ export class Connector {
146163
// TODO get the backend to store a message id in addition to conversationID
147164
messageId: messageData.messageID ?? messageData.triggerID ?? messageData.conversationID,
148165
fileList: {
149-
filePaths: messageData.filePaths,
150-
deletedFiles: messageData.deletedFiles,
166+
filePaths: messageData.filePaths.map((f: DiffTreeFileInfo) => f.zipFilePath),
167+
deletedFiles: messageData.deletedFiles.map((f: DiffTreeFileInfo) => f.zipFilePath),
168+
actions,
151169
},
152170
body: '',
153171
}
@@ -178,6 +196,10 @@ export class Connector {
178196
}
179197

180198
handleMessageReceive = async (messageData: any): Promise<void> => {
199+
if (messageData.type === 'updateFileComponent') {
200+
this.onFileComponentUpdate(messageData.tabID, messageData.filePaths, messageData.deletedFiles)
201+
return
202+
}
181203
if (messageData.type === 'errorMessage') {
182204
this.onError(messageData.tabID, messageData.message, messageData.title)
183205
return

packages/core/src/amazonq/webview/ui/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ type MessageCommand =
2828
| 'response-body-link-click'
2929
| 'transform'
3030
| 'footer-info-link-click'
31+
| 'file-click'
3132

3233
export type ExtensionMessage = Record<string, any> & { command: MessageCommand }

packages/core/src/amazonq/webview/ui/connector.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ExtensionMessage } from './commands'
1111
import { TabType, TabsStorage } from './storages/tabsStorage'
1212
import { WelcomeFollowupType } from './apps/amazonqCommonsConnector'
1313
import { AuthFollowUpType } from './followUps/generator'
14+
import { DiffTreeFileInfo } from './diffTree/types'
1415

1516
export interface CodeReference {
1617
licenseName?: string
@@ -37,10 +38,12 @@ export interface ConnectorProps {
3738
onCWCOnboardingPageInteractionMessage: (message: ChatItem) => string | undefined
3839
onError: (tabID: string, message: string, title: string) => void
3940
onWarning: (tabID: string, message: string, title: string) => void
41+
onFileComponentUpdate: (tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[]) => void
4042
onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
4143
onChatInputEnabled: (tabID: string, enabled: boolean) => void
4244
onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void
4345
onNewTab: (tabType: TabType) => void
46+
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void
4447
tabsStorage: TabsStorage
4548
}
4649

@@ -297,6 +300,14 @@ export class Connector {
297300
}
298301
}
299302

303+
onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => {
304+
switch (this.tabsStorage.getTab(tabID)?.type) {
305+
case 'featuredev':
306+
this.featureDevChatConnector.onFileActionClick(tabID, messageId, filePath, actionName)
307+
break
308+
}
309+
}
310+
300311
onOpenDiff = (tabID: string, filePath: string, deleted: boolean): void => {
301312
switch (this.tabsStorage.getTab(tabID)?.type) {
302313
case 'featuredev':
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { MynahIcons } from '@aws/mynah-ui'
7+
import { FileNodeAction, TreeNodeDetails } from '@aws/mynah-ui/dist/static'
8+
import { DiffTreeFileInfo } from './types'
9+
10+
export function getDetails(filePaths: DiffTreeFileInfo[]): Record<string, TreeNodeDetails> {
11+
const details: Record<string, TreeNodeDetails> = {}
12+
for (const filePath of filePaths) {
13+
if (filePath.rejected) {
14+
details[filePath.relativePath] = {
15+
status: 'error',
16+
label: 'File rejected',
17+
icon: MynahIcons.CANCEL_CIRCLE,
18+
}
19+
}
20+
}
21+
return details
22+
}
23+
24+
export function getActions(filePaths: DiffTreeFileInfo[]): Record<string, FileNodeAction[]> {
25+
const actions: Record<string, FileNodeAction[]> = {}
26+
for (const filePath of filePaths) {
27+
switch (filePath.rejected) {
28+
case true:
29+
actions[filePath.relativePath] = [
30+
{
31+
icon: MynahIcons.REVERT,
32+
name: 'revert-rejection',
33+
description: 'Revert rejection',
34+
},
35+
]
36+
break
37+
case false:
38+
actions[filePath.relativePath] = [
39+
{
40+
icon: MynahIcons.CANCEL_CIRCLE,
41+
status: 'error',
42+
name: 'reject-change',
43+
description: 'Reject change',
44+
},
45+
]
46+
break
47+
}
48+
}
49+
return actions
50+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export type DiffTreeFileInfo = {
7+
zipFilePath: string
8+
relativePath: string
9+
rejected: boolean
10+
}

packages/core/src/amazonq/webview/ui/main.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { FollowUpInteractionHandler } from './followUps/handler'
1414
import { QuickActionHandler } from './quickActions/handler'
1515
import { TextMessageHandler } from './messages/handler'
1616
import { MessageController } from './messages/controller'
17+
import { getActions, getDetails } from './diffTree/actions'
18+
import { DiffTreeFileInfo } from './diffTree/types'
1719

1820
export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, gumbyInitEnabled: boolean) => {
1921
// eslint-disable-next-line prefer-const
@@ -98,6 +100,7 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, gumby
98100
}
99101
}
100102
},
103+
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string): void => {},
101104
onCWCOnboardingPageInteractionMessage: (message: ChatItem): string | undefined => {
102105
return messageController.sendMessageToTab(message, 'cwc')
103106
},
@@ -188,6 +191,18 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, gumby
188191
onMessageReceived: (tabID: string, messageData: MynahUIDataModel) => {
189192
mynahUI.updateStore(tabID, messageData)
190193
},
194+
onFileComponentUpdate: (tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[]) => {
195+
const updateWith: Partial<ChatItem> = {
196+
type: ChatItemType.CODE_RESULT,
197+
fileList: {
198+
filePaths: filePaths.map(i => i.relativePath),
199+
deletedFiles: deletedFiles.map(i => i.relativePath),
200+
details: getDetails(filePaths),
201+
actions: getActions([...filePaths, ...deletedFiles]),
202+
},
203+
}
204+
mynahUI.updateLastChatAnswer(tabID, updateWith)
205+
},
191206
onWarning: (tabID: string, message: string, title: string) => {
192207
mynahUI.notify({
193208
title: title,
@@ -330,6 +345,9 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, gumby
330345
onFollowUpClicked: (tabID, messageId, followUp) => {
331346
followUpsInteractionHandler.onFollowUpClicked(tabID, messageId, followUp)
332347
},
348+
onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => {
349+
connector.onFileActionClick(tabID, messageId, filePath, actionName)
350+
},
333351
onOpenDiff: connector.onOpenDiff,
334352
tabs: {
335353
'tab-1': {

packages/core/src/amazonqFeatureDev/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function init(appContext: AmazonQAppInitContext) {
3232
authClicked: new vscode.EventEmitter<any>(),
3333
processResponseBodyLinkClick: new vscode.EventEmitter<any>(),
3434
insertCodeAtPositionClicked: new vscode.EventEmitter<any>(),
35+
fileClicked: new vscode.EventEmitter<any>(),
3536
}
3637

3738
const messenger = new Messenger(new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher()))

packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface ChatControllerEventEmitters {
3939
readonly authClicked: EventEmitter<any>
4040
readonly processResponseBodyLinkClick: EventEmitter<any>
4141
readonly insertCodeAtPositionClicked: EventEmitter<any>
42+
readonly fileClicked: EventEmitter<any>
4243
}
4344

4445
type OpenDiffMessage = {
@@ -48,6 +49,13 @@ type OpenDiffMessage = {
4849
filePath: string
4950
deleted: boolean
5051
}
52+
53+
type fileClickedMessage = {
54+
tabID: string
55+
messageId: string
56+
filePath: string
57+
actionName: string
58+
}
5159
export class FeatureDevController {
5260
private readonly messenger: Messenger
5361
private readonly sessionStorage: ChatSessionStorage
@@ -131,6 +139,9 @@ export class FeatureDevController {
131139
this.chatControllerMessageListeners.insertCodeAtPositionClicked.event(data => {
132140
this.insertCodeAtPosition(data)
133141
})
142+
this.chatControllerMessageListeners.fileClicked.event(async data => {
143+
return await this.fileClicked(data)
144+
})
134145
}
135146

136147
private async processChatItemVotedMessage(tabId: string, messageId: string, vote: string) {
@@ -402,6 +413,7 @@ export class FeatureDevController {
402413
session = await this.sessionStorage.getSession(message.tabID)
403414
telemetry.amazonq_isAcceptedCodeChanges.emit({
404415
amazonqConversationId: session.conversationId,
416+
amazonqNumberOfFilesAccepted: session.state.filePaths?.filter(i => !i.rejected).length ?? -1,
405417
enabled: true,
406418
result: 'Succeeded',
407419
})
@@ -599,6 +611,27 @@ export class FeatureDevController {
599611
return { left, right }
600612
}
601613

614+
private async fileClicked(message: fileClickedMessage) {
615+
// TODO: add Telemetry here
616+
const tabId: string = message.tabID
617+
const filePathToUpdate: string = message.filePath
618+
619+
const session = await this.sessionStorage.getSession(tabId)
620+
const filePathIndex = (session.state.filePaths ?? []).findIndex(obj => obj.relativePath === filePathToUpdate)
621+
if (filePathIndex !== -1 && session.state.filePaths) {
622+
session.state.filePaths[filePathIndex].rejected = !session.state.filePaths[filePathIndex].rejected
623+
}
624+
const deletedFilePathIndex = (session.state.deletedFiles ?? []).findIndex(
625+
obj => obj.relativePath === filePathToUpdate
626+
)
627+
if (deletedFilePathIndex !== -1 && session.state.deletedFiles) {
628+
session.state.deletedFiles[deletedFilePathIndex].rejected =
629+
!session.state.deletedFiles[deletedFilePathIndex].rejected
630+
}
631+
632+
await session.updateFilesPaths(tabId, session.state.filePaths ?? [], session.state.deletedFiles ?? [])
633+
}
634+
602635
private async openDiff(message: OpenDiffMessage) {
603636
const tabId: string = message.tabID
604637
const zipFilePath: string = message.filePath

0 commit comments

Comments
 (0)