diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 324616a445a..f946b4acb69 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -3,7 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' +import { + ChatItem, + ChatItemButton, + ChatItemFormItem, + ChatItemType, + MynahUIDataModel, + QuickActionCommand, +} from '@aws/mynah-ui' import { TabType } from '../storages/tabsStorage' import { CWCChatItem } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' @@ -18,12 +25,14 @@ export interface ConnectorProps extends BaseConnectorProps { title?: string, description?: string ) => void + onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void } export class Connector extends BaseConnector { private readonly onCWCContextCommandMessage private readonly onContextCommandDataReceived private readonly onShowCustomForm + private readonly onChatAnswerUpdated override getTabType(): TabType { return 'cwc' @@ -34,6 +43,7 @@ export class Connector extends BaseConnector { this.onCWCContextCommandMessage = props.onCWCContextCommandMessage this.onContextCommandDataReceived = props.onContextCommandDataReceived this.onShowCustomForm = props.onShowCustomForm + this.onChatAnswerUpdated = props.onChatAnswerUpdated } onSourceLinkClick = (tabID: string, messageId: string, link: string): void => { @@ -91,16 +101,14 @@ export class Connector extends BaseConnector { messageId: messageData.messageID ?? messageData.triggerID, body: messageData.message, followUp: followUps, - canBeVoted: true, + canBeVoted: messageData.canBeVoted ?? false, codeReference: messageData.codeReference, userIntent: messageData.userIntent, codeBlockLanguage: messageData.codeBlockLanguage, contextList: messageData.contextList, - } - - // If it is not there we will not set it - if (messageData.messageType === 'answer-part' || messageData.messageType === 'answer') { - answer.canBeVoted = true + title: messageData.title, + buttons: messageData.buttons ?? undefined, + fileList: messageData.fileList ?? undefined, } if (messageData.relatedSuggestions !== undefined) { @@ -137,6 +145,8 @@ export class Connector extends BaseConnector { options: messageData.followUps, } : undefined, + buttons: messageData.buttons ?? undefined, + canBeVoted: messageData.canBeVoted ?? false, } this.onChatAnswerReceived(messageData.tabID, answer, messageData) @@ -204,7 +214,7 @@ export class Connector extends BaseConnector { } if (messageData.type === 'customFormActionMessage') { - this.onCustomFormAction(messageData.tabID, messageData.action) + this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action) return } // For other message types, call the base class handleMessageReceive @@ -235,6 +245,7 @@ export class Connector extends BaseConnector { onCustomFormAction( tabId: string, + messageId: string, action: { id: string text?: string | undefined @@ -248,14 +259,53 @@ export class Connector extends BaseConnector { this.sendMessageToExtension({ command: 'form-action-click', action: action, + formSelectedValues: action.formItemValues, tabType: this.getTabType(), tabID: tabId, }) + + if (!this.onChatAnswerUpdated || !['accept-code-diff', 'reject-code-diff'].includes(action.id)) { + return + } + const answer: ChatItem = { + type: ChatItemType.ANSWER, + messageId: messageId, + buttons: [], + } + switch (action.id) { + case 'accept-code-diff': + answer.buttons = [ + { + keepCardAfterClick: true, + text: 'Accepted code', + id: 'accepted-code-diff', + status: 'success', + position: 'outside', + disabled: true, + }, + ] + break + case 'reject-code-diff': + answer.buttons = [ + { + keepCardAfterClick: true, + text: 'Rejected code', + id: 'rejected-code-diff', + status: 'error', + position: 'outside', + disabled: true, + }, + ] + break + default: + break + } + this.onChatAnswerUpdated(tabId, answer) } onFileClick = (tabID: string, filePath: string, messageId?: string) => { this.sendMessageToExtension({ - command: 'file-click', + command: messageId === '' ? 'file-click' : 'open-diff', tabID, messageId, filePath, diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 8f1cde9e565..6968761d8ac 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -63,6 +63,7 @@ export interface CWCChatItem extends ChatItem { userIntent?: UserIntent codeBlockLanguage?: string contextList?: Context[] + title?: string } export interface Context { @@ -711,7 +712,7 @@ export class Connector { tabType: 'cwc', }) } else { - this.cwChatConnector.onCustomFormAction(tabId, action) + this.cwChatConnector.onCustomFormAction(tabId, messageId ?? '', action) } break case 'agentWalkthrough': { diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index d7285d81ba5..561ee55e5cd 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -352,6 +352,7 @@ export const createMynahUI = ( ...(item.followUp !== undefined ? { followUp: item.followUp } : {}), ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), ...(item.header !== undefined ? { header: item.header } : { header: undefined }), + ...(item.buttons !== undefined ? { buttons: item.buttons } : { buttons: undefined }), }) if ( item.messageId !== undefined && @@ -374,7 +375,7 @@ export const createMynahUI = ( fileList: { fileTreeTitle: '', filePaths: item.contextList.map((file) => file.relativeFilePath), - rootFolderTitle: 'Context', + rootFolderTitle: item.title, flatList: true, collapsed: true, hideFileCount: true, diff --git a/packages/core/src/codewhispererChat/app.ts b/packages/core/src/codewhispererChat/app.ts index 3ffa51b75eb..1085b040c79 100644 --- a/packages/core/src/codewhispererChat/app.ts +++ b/packages/core/src/codewhispererChat/app.ts @@ -28,6 +28,7 @@ import { AcceptDiff, QuickCommandGroupActionClick, FileClick, + OpenDiff, } from './controllers/chat/model' import { EditorContextCommand, registerCommands } from './commands/registerCommands' import { ContextSelectedMessage, CustomFormActionMessage } from './view/connector/connector' @@ -41,6 +42,7 @@ export function init(appContext: AmazonQAppInitContext) { processInsertCodeAtCursorPosition: new EventEmitter(), processAcceptDiff: new EventEmitter(), processViewDiff: new EventEmitter(), + processOpenDiff: new EventEmitter(), processCopyCodeToClipboard: new EventEmitter(), processContextMenuCommand: new EventEmitter(), processTriggerTabIDReceived: new EventEmitter(), @@ -76,6 +78,7 @@ export function init(appContext: AmazonQAppInitContext) { ), processAcceptDiff: new MessageListener(cwChatControllerEventEmitters.processAcceptDiff), processViewDiff: new MessageListener(cwChatControllerEventEmitters.processViewDiff), + processOpenDiff: new MessageListener(cwChatControllerEventEmitters.processOpenDiff), processCopyCodeToClipboard: new MessageListener( cwChatControllerEventEmitters.processCopyCodeToClipboard ), @@ -137,6 +140,7 @@ export function init(appContext: AmazonQAppInitContext) { ), processAcceptDiff: new MessagePublisher(cwChatControllerEventEmitters.processAcceptDiff), processViewDiff: new MessagePublisher(cwChatControllerEventEmitters.processViewDiff), + processOpenDiff: new MessagePublisher(cwChatControllerEventEmitters.processOpenDiff), processCopyCodeToClipboard: new MessagePublisher( cwChatControllerEventEmitters.processCopyCodeToClipboard ), diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index a89a203430d..b1ffb09d909 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -17,6 +17,14 @@ import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWr export class ChatSession { private sessionId?: string + /** + * _readFiles = list of files read from the project to gather context before generating response. + * _filePath = The path helps the system locate exactly where to make the necessary changes in the project structure + * _tempFilePath = Used to show the code diff view in the editor including LLM changes. + */ + private _readFiles: string[] = [] + private _filePath: string | undefined + private _tempFilePath: string | undefined private _toolUse: ToolUse | undefined contexts: Map = new Map() @@ -48,6 +56,27 @@ export class ChatSession { public setSessionID(id?: string) { this.sessionId = id } + public get listOfReadFiles(): string[] { + return this._readFiles + } + public get filePath(): string | undefined { + return this._filePath + } + public get tempFilePath(): string | undefined { + return this._tempFilePath + } + public setFilePath(filePath: string | undefined) { + this._filePath = filePath + } + public setTempFilePath(tempFilePath: string | undefined) { + this._tempFilePath = tempFilePath + } + public pushToListOfReadFiles(filePath: string) { + this._readFiles.push(filePath) + } + public clearListOfReadFiles() { + this._readFiles = [] + } async chatIam(chatRequest: SendMessageRequest): Promise { const client = await createQDeveloperStreamingClient() diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index b7c67f1ecc1..282e0a15a3e 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -32,6 +32,7 @@ import { DocumentReference, FileClick, RelevantTextDocumentAddition, + OpenDiff, } from './model' import { AppToWebViewMessageDispatcher, @@ -81,6 +82,7 @@ import { } from '../../constants' import { ChatSession } from '../../clients/chat/v0/chat' import { ChatHistoryManager } from '../../storages/chatHistory' +import { amazonQTabSuffix } from '../../../shared/constants' import { FsRead, FsReadParams } from '../../tools/fsRead' export interface ChatControllerMessagePublishers { @@ -91,6 +93,7 @@ export interface ChatControllerMessagePublishers { readonly processInsertCodeAtCursorPosition: MessagePublisher readonly processAcceptDiff: MessagePublisher readonly processViewDiff: MessagePublisher + readonly processOpenDiff: MessagePublisher readonly processCopyCodeToClipboard: MessagePublisher readonly processContextMenuCommand: MessagePublisher readonly processTriggerTabIDReceived: MessagePublisher @@ -116,6 +119,7 @@ export interface ChatControllerMessageListeners { readonly processInsertCodeAtCursorPosition: MessageListener readonly processAcceptDiff: MessageListener readonly processViewDiff: MessageListener + readonly processOpenDiff: MessageListener readonly processCopyCodeToClipboard: MessageListener readonly processContextMenuCommand: MessageListener readonly processTriggerTabIDReceived: MessageListener @@ -214,6 +218,10 @@ export class ChatController { return this.processViewDiff(data) }) + this.chatControllerMessageListeners.processOpenDiff.onMessage((data) => { + return this.processOpenDiff(data) + }) + this.chatControllerMessageListeners.processCopyCodeToClipboard.onMessage((data) => { return this.processCopyCodeToClipboard(data) }) @@ -389,6 +397,45 @@ export class ChatController { }) } + private async processOpenDiff(message: OpenDiff) { + const session = this.sessionStorage.getSession(message.tabID) + const filePath = session.filePath ?? message.filePath + const fileExists = await fs.existsFile(filePath) + // Check if fileExists=false, If yes, return instead of showing broken diff experience. + if (!session.tempFilePath) { + return + } + const leftUri = fileExists ? vscode.Uri.file(filePath) : vscode.Uri.from({ scheme: 'untitled' }) + const rightUri = vscode.Uri.file(session.tempFilePath ?? filePath) + const fileName = path.basename(filePath) + await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, `${fileName} ${amazonQTabSuffix}`) + } + + private async processAcceptCodeDiff(message: CustomFormActionMessage) { + const session = this.sessionStorage.getSession(message.tabID ?? '') + const filePath = session.filePath ?? '' + const fileExists = await fs.existsFile(filePath) + const tempFilePath = session.tempFilePath + const tempFileExists = await fs.existsFile(tempFilePath ?? '') + if (fileExists && tempFileExists) { + const fileContent = await fs.readFileText(filePath) + const tempFileContent = await fs.readFileText(tempFilePath ?? '') + if (fileContent !== tempFileContent) { + await fs.writeFile(filePath, tempFileContent) + } + await fs.delete(tempFilePath ?? '') + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath)) + } else if (!fileExists && tempFileExists) { + const fileContent = await fs.readFileText(tempFilePath ?? '') + await fs.writeFile(filePath, fileContent) + await fs.delete(tempFilePath ?? '') + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath)) + } + // Reset the filePaths to undefined + session.setFilePath(undefined) + session.setTempFilePath(undefined) + } + private async processCopyCodeToClipboard(message: CopyCodeToClipboard) { this.telemetryHelper.recordInteractWithMessage(message) } @@ -578,6 +625,12 @@ export class ChatController { const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) await vscode.window.showTextDocument(newFileDoc) telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' }) + } else if (message.action.id === 'accept-code-diff') { + await this.processAcceptCodeDiff(message) + } else if (message.action.id === 'reject-code-diff') { + // Reset the filePaths to undefined + this.sessionStorage.getSession(message.tabID ?? '').setFilePath(undefined) + this.sessionStorage.getSession(message.tabID ?? '').setTempFilePath(undefined) } else if (message.action.id === 'confirm-tool-use') { await this.processToolUseMessage(message) } @@ -936,6 +989,8 @@ export class ChatController { } private async processPromptMessageAsNewThread(message: PromptMessage) { + const session = this.sessionStorage.getSession(message.tabID) + session.clearListOfReadFiles() this.editorContextExtractor .extractContextForTrigger('ChatMessage') .then(async (context) => { diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 24ca24e0d59..5e5f85c40cc 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -86,6 +86,10 @@ export class Messenger { userIntent: undefined, codeBlockLanguage: undefined, contextList: mergedRelevantDocuments, + title: 'Context', + buttons: undefined, + fileList: undefined, + canBeVoted: false, }, tabID ) @@ -486,6 +490,7 @@ export class Messenger { userIntent: undefined, codeBlockLanguage: undefined, contextList: undefined, + title: undefined, }, tabID ) diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 71837d10c76..3d322e9bc1e 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -96,6 +96,14 @@ export interface ViewDiff { totalCodeBlocks?: number } +export interface OpenDiff { + command: string | undefined + tabID: string + messageId: string + filePath: string + referenceTrackerInformation?: CodeReference[] +} + export type ChatPromptCommandType = | 'help' | 'clear' diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index b37f5610611..62176ccb1a1 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -7,7 +7,7 @@ import { Timestamp } from 'aws-sdk/clients/apigateway' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' -import { ChatItemButton, ChatItemFormItem, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' +import { ChatItemButton, ChatItemContent, ChatItemFormItem, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' import { DocumentReference } from '../../controllers/chat/model' class UiMessage { @@ -208,6 +208,10 @@ export interface ChatMessageProps { readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined readonly contextList: DocumentReference[] | undefined + readonly title?: string + readonly buttons?: ChatItemButton[] + readonly fileList?: ChatItemContent['fileList'] + readonly canBeVoted?: boolean } export class ChatMessage extends UiMessage { @@ -223,6 +227,10 @@ export class ChatMessage extends UiMessage { readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined readonly contextList: DocumentReference[] | undefined + readonly title?: string + readonly buttons?: ChatItemButton[] + readonly fileList?: ChatItemContent['fileList'] + readonly canBeVoted?: boolean = false override type = 'chatMessage' constructor(props: ChatMessageProps, tabID: string) { @@ -238,6 +246,10 @@ export class ChatMessage extends UiMessage { this.userIntent = props.userIntent this.codeBlockLanguage = props.codeBlockLanguage this.contextList = props.contextList + this.title = props.title + this.buttons = props.buttons + this.fileList = props.fileList + this.canBeVoted = props.canBeVoted } } diff --git a/packages/core/src/codewhispererChat/view/messages/messageListener.ts b/packages/core/src/codewhispererChat/view/messages/messageListener.ts index bb2871957c8..09bd69aea4b 100644 --- a/packages/core/src/codewhispererChat/view/messages/messageListener.ts +++ b/packages/core/src/codewhispererChat/view/messages/messageListener.ts @@ -69,6 +69,9 @@ export class UIMessageListener { case 'view_diff': this.processViewDiff(msg) break + case 'open-diff': + this.processOpenDiff(msg) + break case 'code_was_copied_to_clipboard': this.processCodeWasCopiedToClipboard(msg) break @@ -221,6 +224,14 @@ export class UIMessageListener { }) } + private processOpenDiff(msg: any) { + this.chatControllerMessagePublishers.processOpenDiff.publish({ + command: msg.command, + tabID: msg.tabID || msg.tabId, + ...msg, + }) + } + private processCodeWasCopiedToClipboard(msg: any) { this.chatControllerMessagePublishers.processCopyCodeToClipboard.publish({ command: msg.command,