Skip to content

Commit 8392a4c

Browse files
authored
Merge pull request aws#6846 from laileni-aws/part1
feat(amazonq): Adding openDiff and acceptDiff UX to the agentic chat.
2 parents 1fc3fdc + 02478e3 commit 8392a4c

File tree

10 files changed

+188
-12
lines changed

10 files changed

+188
-12
lines changed

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

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui'
6+
import {
7+
ChatItem,
8+
ChatItemButton,
9+
ChatItemFormItem,
10+
ChatItemType,
11+
MynahUIDataModel,
12+
QuickActionCommand,
13+
} from '@aws/mynah-ui'
714
import { TabType } from '../storages/tabsStorage'
815
import { CWCChatItem } from '../connector'
916
import { BaseConnector, BaseConnectorProps } from './baseConnector'
@@ -18,12 +25,14 @@ export interface ConnectorProps extends BaseConnectorProps {
1825
title?: string,
1926
description?: string
2027
) => void
28+
onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
2129
}
2230

2331
export class Connector extends BaseConnector {
2432
private readonly onCWCContextCommandMessage
2533
private readonly onContextCommandDataReceived
2634
private readonly onShowCustomForm
35+
private readonly onChatAnswerUpdated
2736

2837
override getTabType(): TabType {
2938
return 'cwc'
@@ -34,6 +43,7 @@ export class Connector extends BaseConnector {
3443
this.onCWCContextCommandMessage = props.onCWCContextCommandMessage
3544
this.onContextCommandDataReceived = props.onContextCommandDataReceived
3645
this.onShowCustomForm = props.onShowCustomForm
46+
this.onChatAnswerUpdated = props.onChatAnswerUpdated
3747
}
3848

3949
onSourceLinkClick = (tabID: string, messageId: string, link: string): void => {
@@ -91,16 +101,14 @@ export class Connector extends BaseConnector {
91101
messageId: messageData.messageID ?? messageData.triggerID,
92102
body: messageData.message,
93103
followUp: followUps,
94-
canBeVoted: true,
104+
canBeVoted: messageData.canBeVoted ?? false,
95105
codeReference: messageData.codeReference,
96106
userIntent: messageData.userIntent,
97107
codeBlockLanguage: messageData.codeBlockLanguage,
98108
contextList: messageData.contextList,
99-
}
100-
101-
// If it is not there we will not set it
102-
if (messageData.messageType === 'answer-part' || messageData.messageType === 'answer') {
103-
answer.canBeVoted = true
109+
title: messageData.title,
110+
buttons: messageData.buttons ?? undefined,
111+
fileList: messageData.fileList ?? undefined,
104112
}
105113

106114
if (messageData.relatedSuggestions !== undefined) {
@@ -137,6 +145,8 @@ export class Connector extends BaseConnector {
137145
options: messageData.followUps,
138146
}
139147
: undefined,
148+
buttons: messageData.buttons ?? undefined,
149+
canBeVoted: messageData.canBeVoted ?? false,
140150
}
141151
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
142152

@@ -204,7 +214,7 @@ export class Connector extends BaseConnector {
204214
}
205215

206216
if (messageData.type === 'customFormActionMessage') {
207-
this.onCustomFormAction(messageData.tabID, messageData.action)
217+
this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action)
208218
return
209219
}
210220
// For other message types, call the base class handleMessageReceive
@@ -235,6 +245,7 @@ export class Connector extends BaseConnector {
235245

236246
onCustomFormAction(
237247
tabId: string,
248+
messageId: string,
238249
action: {
239250
id: string
240251
text?: string | undefined
@@ -248,14 +259,53 @@ export class Connector extends BaseConnector {
248259
this.sendMessageToExtension({
249260
command: 'form-action-click',
250261
action: action,
262+
formSelectedValues: action.formItemValues,
251263
tabType: this.getTabType(),
252264
tabID: tabId,
253265
})
266+
267+
if (!this.onChatAnswerUpdated || !['accept-code-diff', 'reject-code-diff'].includes(action.id)) {
268+
return
269+
}
270+
const answer: ChatItem = {
271+
type: ChatItemType.ANSWER,
272+
messageId: messageId,
273+
buttons: [],
274+
}
275+
switch (action.id) {
276+
case 'accept-code-diff':
277+
answer.buttons = [
278+
{
279+
keepCardAfterClick: true,
280+
text: 'Accepted code',
281+
id: 'accepted-code-diff',
282+
status: 'success',
283+
position: 'outside',
284+
disabled: true,
285+
},
286+
]
287+
break
288+
case 'reject-code-diff':
289+
answer.buttons = [
290+
{
291+
keepCardAfterClick: true,
292+
text: 'Rejected code',
293+
id: 'rejected-code-diff',
294+
status: 'error',
295+
position: 'outside',
296+
disabled: true,
297+
},
298+
]
299+
break
300+
default:
301+
break
302+
}
303+
this.onChatAnswerUpdated(tabId, answer)
254304
}
255305

256306
onFileClick = (tabID: string, filePath: string, messageId?: string) => {
257307
this.sendMessageToExtension({
258-
command: 'file-click',
308+
command: messageId === '' ? 'file-click' : 'open-diff',
259309
tabID,
260310
messageId,
261311
filePath,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface CWCChatItem extends ChatItem {
6363
userIntent?: UserIntent
6464
codeBlockLanguage?: string
6565
contextList?: Context[]
66+
title?: string
6667
}
6768

6869
export interface Context {
@@ -711,7 +712,7 @@ export class Connector {
711712
tabType: 'cwc',
712713
})
713714
} else {
714-
this.cwChatConnector.onCustomFormAction(tabId, action)
715+
this.cwChatConnector.onCustomFormAction(tabId, messageId ?? '', action)
715716
}
716717
break
717718
case 'agentWalkthrough': {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ export const createMynahUI = (
352352
...(item.followUp !== undefined ? { followUp: item.followUp } : {}),
353353
...(item.fileList !== undefined ? { fileList: item.fileList } : {}),
354354
...(item.header !== undefined ? { header: item.header } : { header: undefined }),
355+
...(item.buttons !== undefined ? { buttons: item.buttons } : { buttons: undefined }),
355356
})
356357
if (
357358
item.messageId !== undefined &&
@@ -374,7 +375,7 @@ export const createMynahUI = (
374375
fileList: {
375376
fileTreeTitle: '',
376377
filePaths: item.contextList.map((file) => file.relativeFilePath),
377-
rootFolderTitle: 'Context',
378+
rootFolderTitle: item.title,
378379
flatList: true,
379380
collapsed: true,
380381
hideFileCount: true,

packages/core/src/codewhispererChat/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
AcceptDiff,
2929
QuickCommandGroupActionClick,
3030
FileClick,
31+
OpenDiff,
3132
} from './controllers/chat/model'
3233
import { EditorContextCommand, registerCommands } from './commands/registerCommands'
3334
import { ContextSelectedMessage, CustomFormActionMessage } from './view/connector/connector'
@@ -41,6 +42,7 @@ export function init(appContext: AmazonQAppInitContext) {
4142
processInsertCodeAtCursorPosition: new EventEmitter<InsertCodeAtCursorPosition>(),
4243
processAcceptDiff: new EventEmitter<AcceptDiff>(),
4344
processViewDiff: new EventEmitter<ViewDiff>(),
45+
processOpenDiff: new EventEmitter<OpenDiff>(),
4446
processCopyCodeToClipboard: new EventEmitter<CopyCodeToClipboard>(),
4547
processContextMenuCommand: new EventEmitter<EditorContextCommand>(),
4648
processTriggerTabIDReceived: new EventEmitter<TriggerTabIDReceived>(),
@@ -76,6 +78,7 @@ export function init(appContext: AmazonQAppInitContext) {
7678
),
7779
processAcceptDiff: new MessageListener<AcceptDiff>(cwChatControllerEventEmitters.processAcceptDiff),
7880
processViewDiff: new MessageListener<ViewDiff>(cwChatControllerEventEmitters.processViewDiff),
81+
processOpenDiff: new MessageListener<OpenDiff>(cwChatControllerEventEmitters.processOpenDiff),
7982
processCopyCodeToClipboard: new MessageListener<CopyCodeToClipboard>(
8083
cwChatControllerEventEmitters.processCopyCodeToClipboard
8184
),
@@ -137,6 +140,7 @@ export function init(appContext: AmazonQAppInitContext) {
137140
),
138141
processAcceptDiff: new MessagePublisher<AcceptDiff>(cwChatControllerEventEmitters.processAcceptDiff),
139142
processViewDiff: new MessagePublisher<ViewDiff>(cwChatControllerEventEmitters.processViewDiff),
143+
processOpenDiff: new MessagePublisher<OpenDiff>(cwChatControllerEventEmitters.processOpenDiff),
140144
processCopyCodeToClipboard: new MessagePublisher<CopyCodeToClipboard>(
141145
cwChatControllerEventEmitters.processCopyCodeToClipboard
142146
),

packages/core/src/codewhispererChat/clients/chat/v0/chat.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWr
1717

1818
export class ChatSession {
1919
private sessionId?: string
20+
/**
21+
* _readFiles = list of files read from the project to gather context before generating response.
22+
* _filePath = The path helps the system locate exactly where to make the necessary changes in the project structure
23+
* _tempFilePath = Used to show the code diff view in the editor including LLM changes.
24+
*/
25+
private _readFiles: string[] = []
26+
private _filePath: string | undefined
27+
private _tempFilePath: string | undefined
2028
private _toolUse: ToolUse | undefined
2129

2230
contexts: Map<string, { first: number; second: number }[]> = new Map()
@@ -48,6 +56,27 @@ export class ChatSession {
4856
public setSessionID(id?: string) {
4957
this.sessionId = id
5058
}
59+
public get listOfReadFiles(): string[] {
60+
return this._readFiles
61+
}
62+
public get filePath(): string | undefined {
63+
return this._filePath
64+
}
65+
public get tempFilePath(): string | undefined {
66+
return this._tempFilePath
67+
}
68+
public setFilePath(filePath: string | undefined) {
69+
this._filePath = filePath
70+
}
71+
public setTempFilePath(tempFilePath: string | undefined) {
72+
this._tempFilePath = tempFilePath
73+
}
74+
public pushToListOfReadFiles(filePath: string) {
75+
this._readFiles.push(filePath)
76+
}
77+
public clearListOfReadFiles() {
78+
this._readFiles = []
79+
}
5180
async chatIam(chatRequest: SendMessageRequest): Promise<SendMessageCommandOutput> {
5281
const client = await createQDeveloperStreamingClient()
5382

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
DocumentReference,
3333
FileClick,
3434
RelevantTextDocumentAddition,
35+
OpenDiff,
3536
} from './model'
3637
import {
3738
AppToWebViewMessageDispatcher,
@@ -81,6 +82,7 @@ import {
8182
} from '../../constants'
8283
import { ChatSession } from '../../clients/chat/v0/chat'
8384
import { ChatHistoryManager } from '../../storages/chatHistory'
85+
import { amazonQTabSuffix } from '../../../shared/constants'
8486
import { FsRead, FsReadParams } from '../../tools/fsRead'
8587

8688
export interface ChatControllerMessagePublishers {
@@ -91,6 +93,7 @@ export interface ChatControllerMessagePublishers {
9193
readonly processInsertCodeAtCursorPosition: MessagePublisher<InsertCodeAtCursorPosition>
9294
readonly processAcceptDiff: MessagePublisher<AcceptDiff>
9395
readonly processViewDiff: MessagePublisher<ViewDiff>
96+
readonly processOpenDiff: MessagePublisher<OpenDiff>
9497
readonly processCopyCodeToClipboard: MessagePublisher<CopyCodeToClipboard>
9598
readonly processContextMenuCommand: MessagePublisher<EditorContextCommand>
9699
readonly processTriggerTabIDReceived: MessagePublisher<TriggerTabIDReceived>
@@ -116,6 +119,7 @@ export interface ChatControllerMessageListeners {
116119
readonly processInsertCodeAtCursorPosition: MessageListener<InsertCodeAtCursorPosition>
117120
readonly processAcceptDiff: MessageListener<AcceptDiff>
118121
readonly processViewDiff: MessageListener<ViewDiff>
122+
readonly processOpenDiff: MessageListener<OpenDiff>
119123
readonly processCopyCodeToClipboard: MessageListener<CopyCodeToClipboard>
120124
readonly processContextMenuCommand: MessageListener<EditorContextCommand>
121125
readonly processTriggerTabIDReceived: MessageListener<TriggerTabIDReceived>
@@ -214,6 +218,10 @@ export class ChatController {
214218
return this.processViewDiff(data)
215219
})
216220

221+
this.chatControllerMessageListeners.processOpenDiff.onMessage((data) => {
222+
return this.processOpenDiff(data)
223+
})
224+
217225
this.chatControllerMessageListeners.processCopyCodeToClipboard.onMessage((data) => {
218226
return this.processCopyCodeToClipboard(data)
219227
})
@@ -389,6 +397,45 @@ export class ChatController {
389397
})
390398
}
391399

400+
private async processOpenDiff(message: OpenDiff) {
401+
const session = this.sessionStorage.getSession(message.tabID)
402+
const filePath = session.filePath ?? message.filePath
403+
const fileExists = await fs.existsFile(filePath)
404+
// Check if fileExists=false, If yes, return instead of showing broken diff experience.
405+
if (!session.tempFilePath) {
406+
return
407+
}
408+
const leftUri = fileExists ? vscode.Uri.file(filePath) : vscode.Uri.from({ scheme: 'untitled' })
409+
const rightUri = vscode.Uri.file(session.tempFilePath ?? filePath)
410+
const fileName = path.basename(filePath)
411+
await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, `${fileName} ${amazonQTabSuffix}`)
412+
}
413+
414+
private async processAcceptCodeDiff(message: CustomFormActionMessage) {
415+
const session = this.sessionStorage.getSession(message.tabID ?? '')
416+
const filePath = session.filePath ?? ''
417+
const fileExists = await fs.existsFile(filePath)
418+
const tempFilePath = session.tempFilePath
419+
const tempFileExists = await fs.existsFile(tempFilePath ?? '')
420+
if (fileExists && tempFileExists) {
421+
const fileContent = await fs.readFileText(filePath)
422+
const tempFileContent = await fs.readFileText(tempFilePath ?? '')
423+
if (fileContent !== tempFileContent) {
424+
await fs.writeFile(filePath, tempFileContent)
425+
}
426+
await fs.delete(tempFilePath ?? '')
427+
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath))
428+
} else if (!fileExists && tempFileExists) {
429+
const fileContent = await fs.readFileText(tempFilePath ?? '')
430+
await fs.writeFile(filePath, fileContent)
431+
await fs.delete(tempFilePath ?? '')
432+
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath))
433+
}
434+
// Reset the filePaths to undefined
435+
session.setFilePath(undefined)
436+
session.setTempFilePath(undefined)
437+
}
438+
392439
private async processCopyCodeToClipboard(message: CopyCodeToClipboard) {
393440
this.telemetryHelper.recordInteractWithMessage(message)
394441
}
@@ -578,6 +625,12 @@ export class ChatController {
578625
const newFileDoc = await vscode.workspace.openTextDocument(newFilePath)
579626
await vscode.window.showTextDocument(newFileDoc)
580627
telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' })
628+
} else if (message.action.id === 'accept-code-diff') {
629+
await this.processAcceptCodeDiff(message)
630+
} else if (message.action.id === 'reject-code-diff') {
631+
// Reset the filePaths to undefined
632+
this.sessionStorage.getSession(message.tabID ?? '').setFilePath(undefined)
633+
this.sessionStorage.getSession(message.tabID ?? '').setTempFilePath(undefined)
581634
} else if (message.action.id === 'confirm-tool-use') {
582635
await this.processToolUseMessage(message)
583636
}
@@ -936,6 +989,8 @@ export class ChatController {
936989
}
937990

938991
private async processPromptMessageAsNewThread(message: PromptMessage) {
992+
const session = this.sessionStorage.getSession(message.tabID)
993+
session.clearListOfReadFiles()
939994
this.editorContextExtractor
940995
.extractContextForTrigger('ChatMessage')
941996
.then(async (context) => {

packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export class Messenger {
8686
userIntent: undefined,
8787
codeBlockLanguage: undefined,
8888
contextList: mergedRelevantDocuments,
89+
title: 'Context',
90+
buttons: undefined,
91+
fileList: undefined,
92+
canBeVoted: false,
8993
},
9094
tabID
9195
)
@@ -486,6 +490,7 @@ export class Messenger {
486490
userIntent: undefined,
487491
codeBlockLanguage: undefined,
488492
contextList: undefined,
493+
title: undefined,
489494
},
490495
tabID
491496
)

packages/core/src/codewhispererChat/controllers/chat/model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ export interface ViewDiff {
9696
totalCodeBlocks?: number
9797
}
9898

99+
export interface OpenDiff {
100+
command: string | undefined
101+
tabID: string
102+
messageId: string
103+
filePath: string
104+
referenceTrackerInformation?: CodeReference[]
105+
}
106+
99107
export type ChatPromptCommandType =
100108
| 'help'
101109
| 'clear'

0 commit comments

Comments
 (0)