Skip to content

Commit 06b03a1

Browse files
committed
Implementation of code diff for File Write along with Accept and Reject buttons
1 parent aa76262 commit 06b03a1

File tree

5 files changed

+201
-174
lines changed

5 files changed

+201
-174
lines changed

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ export class ChatSession {
2424
* _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user
2525
*/
2626
private _readFiles: string[] = []
27-
private _filePath: string | undefined
28-
private _tempFilePath: string | undefined
2927
private _toolUse: ToolUse | undefined
3028
private _showDiffOnFileWrite: boolean = false
3129

@@ -61,24 +59,12 @@ export class ChatSession {
6159
public get readFiles(): string[] {
6260
return this._readFiles
6361
}
64-
public get filePath(): string | undefined {
65-
return this._filePath
66-
}
67-
public get tempFilePath(): string | undefined {
68-
return this._tempFilePath
69-
}
7062
public get showDiffOnFileWrite(): boolean {
7163
return this._showDiffOnFileWrite
7264
}
7365
public setShowDiffOnFileWrite(value: boolean) {
7466
this._showDiffOnFileWrite = value
7567
}
76-
public setFilePath(filePath: string | undefined) {
77-
this._filePath = filePath
78-
}
79-
public setTempFilePath(tempFilePath: string | undefined) {
80-
this._tempFilePath = tempFilePath
81-
}
8268
public addToReadFiles(filePath: string) {
8369
this._readFiles.push(filePath)
8470
}

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

Lines changed: 160 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ import { amazonQTabSuffix } from '../../../shared/constants'
9292
import { OutputKind } from '../../tools/toolShared'
9393
import { ToolUtils, Tool } from '../../tools/toolUtils'
9494
import { ChatStream } from '../../tools/chatStream'
95+
import { FsWriteParams } from '../../tools/fsWrite'
96+
import { tempDirPath } from '../../../shared/filesystemUtilities'
9597

9698
export interface ChatControllerMessagePublishers {
9799
readonly processPromptChatMessage: MessagePublisher<PromptMessage>
@@ -399,31 +401,6 @@ export class ChatController {
399401
})
400402
}
401403

402-
private async processAcceptCodeDiff(message: CustomFormActionMessage) {
403-
const session = this.sessionStorage.getSession(message.tabID ?? '')
404-
const filePath = session.filePath ?? ''
405-
const fileExists = await fs.existsFile(filePath)
406-
const tempFilePath = session.tempFilePath
407-
const tempFileExists = await fs.existsFile(tempFilePath ?? '')
408-
if (fileExists && tempFileExists) {
409-
const fileContent = await fs.readFileText(filePath)
410-
const tempFileContent = await fs.readFileText(tempFilePath ?? '')
411-
if (fileContent !== tempFileContent) {
412-
await fs.writeFile(filePath, tempFileContent)
413-
}
414-
await fs.delete(tempFilePath ?? '')
415-
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath))
416-
} else if (!fileExists && tempFileExists) {
417-
const fileContent = await fs.readFileText(tempFilePath ?? '')
418-
await fs.writeFile(filePath, fileContent)
419-
await fs.delete(tempFilePath ?? '')
420-
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath))
421-
}
422-
// Reset the filePaths to undefined
423-
session.setFilePath(undefined)
424-
session.setTempFilePath(undefined)
425-
}
426-
427404
private async processCopyCodeToClipboard(message: CopyCodeToClipboard) {
428405
this.telemetryHelper.recordInteractWithMessage(message)
429406
}
@@ -628,29 +605,126 @@ export class ChatController {
628605
this.handlePromptCreate(message.tabID)
629606
}
630607
}
608+
private async handleCreatePrompt(message: CustomFormActionMessage) {
609+
const userPromptsDirectory = getUserPromptsDirectory()
610+
const title = message.action.formItemValues?.['prompt-name'] ?? 'default'
611+
const newFilePath = path.join(userPromptsDirectory, `${title}${promptFileExtension}`)
631612

632-
private async processCustomFormAction(message: CustomFormActionMessage) {
633-
if (message.action.id === 'submit-create-prompt') {
634-
const userPromptsDirectory = getUserPromptsDirectory()
613+
await fs.writeFile(newFilePath, new Uint8Array(Buffer.from('')))
614+
const newFileDoc = await vscode.workspace.openTextDocument(newFilePath)
615+
await vscode.window.showTextDocument(newFileDoc)
635616

636-
const title = message.action.formItemValues?.['prompt-name']
637-
const newFilePath = path.join(
638-
userPromptsDirectory,
639-
title ? `${title}${promptFileExtension}` : `default${promptFileExtension}`
640-
)
641-
const newFileContent = new Uint8Array(Buffer.from(''))
642-
await fs.writeFile(newFilePath, newFileContent)
643-
const newFileDoc = await vscode.workspace.openTextDocument(newFilePath)
644-
await vscode.window.showTextDocument(newFileDoc)
645-
telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' })
646-
} else if (message.action.id === 'accept-code-diff') {
647-
await this.processAcceptCodeDiff(message)
648-
} else if (message.action.id === 'reject-code-diff') {
649-
// Reset the filePaths to undefined
650-
this.sessionStorage.getSession(message.tabID ?? '').setFilePath(undefined)
651-
this.sessionStorage.getSession(message.tabID ?? '').setTempFilePath(undefined)
652-
} else if (message.action.id === 'confirm-tool-use') {
653-
await this.processToolUseMessage(message)
617+
telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' })
618+
}
619+
620+
private async processToolUseMessage(message: CustomFormActionMessage) {
621+
const tabID = message.tabID
622+
if (!tabID) {
623+
return
624+
}
625+
this.editorContextExtractor
626+
.extractContextForTrigger('ChatMessage')
627+
.then(async (context) => {
628+
const triggerID = randomUUID()
629+
this.triggerEventsStorage.addTriggerEvent({
630+
id: triggerID,
631+
tabID: message.tabID,
632+
message: undefined,
633+
type: 'chat_message',
634+
context,
635+
})
636+
const session = this.sessionStorage.getSession(tabID)
637+
const toolUse = session.toolUse
638+
if (!toolUse || !toolUse.input) {
639+
return
640+
}
641+
session.setToolUse(undefined)
642+
643+
const toolResults: ToolResult[] = []
644+
645+
const result = ToolUtils.tryFromToolUse(toolUse)
646+
if ('type' in result) {
647+
const tool: Tool = result
648+
649+
try {
650+
await ToolUtils.validate(tool)
651+
652+
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse)
653+
const output = await ToolUtils.invoke(tool, chatStream)
654+
655+
toolResults.push({
656+
content: [
657+
output.output.kind === OutputKind.Text
658+
? { text: output.output.content }
659+
: { json: output.output.content },
660+
],
661+
toolUseId: toolUse.toolUseId,
662+
status: ToolResultStatus.SUCCESS,
663+
})
664+
// Close the diff view if User accept the generated code changes.
665+
if (vscode.window.tabGroups.activeTabGroup.activeTab?.label.includes(amazonQTabSuffix)) {
666+
await vscode.commands.executeCommand('workbench.action.closeActiveEditor')
667+
}
668+
} catch (e: any) {
669+
toolResults.push({
670+
content: [{ text: e.message }],
671+
toolUseId: toolUse.toolUseId,
672+
status: ToolResultStatus.ERROR,
673+
})
674+
}
675+
} else {
676+
const toolResult: ToolResult = result
677+
toolResults.push(toolResult)
678+
}
679+
680+
await this.generateResponse(
681+
{
682+
message: '',
683+
trigger: ChatTriggerType.ChatMessage,
684+
query: undefined,
685+
codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock,
686+
fileText: context?.focusAreaContext?.extendedCodeBlock ?? '',
687+
fileLanguage: context?.activeFileContext?.fileLanguage,
688+
filePath: context?.activeFileContext?.filePath,
689+
matchPolicy: context?.activeFileContext?.matchPolicy,
690+
codeQuery: context?.focusAreaContext?.names,
691+
userIntent: undefined,
692+
customization: getSelectedCustomization(),
693+
toolResults: toolResults,
694+
origin: Origin.IDE,
695+
chatHistory: this.chatHistoryManager.getHistory(),
696+
context: [],
697+
relevantTextDocuments: [],
698+
additionalContents: [],
699+
documentReferences: [],
700+
useRelevantDocuments: false,
701+
contextLengths: {
702+
...defaultContextLengths,
703+
},
704+
},
705+
triggerID
706+
)
707+
})
708+
.catch((e) => {
709+
this.processException(e, tabID)
710+
})
711+
}
712+
713+
private async processCustomFormAction(message: CustomFormActionMessage) {
714+
switch (message.action.id) {
715+
case 'submit-create-prompt':
716+
await this.handleCreatePrompt(message)
717+
break
718+
case 'accept-code-diff':
719+
case 'confirm-tool-use':
720+
await this.processToolUseMessage(message)
721+
break
722+
case 'reject-code-diff':
723+
// TODO: Do session cleanUp.
724+
getLogger().info('Generated response is rejected')
725+
break
726+
default:
727+
getLogger().warn(`Unhandled action: ${message.action.id}`)
654728
}
655729
}
656730

@@ -663,15 +737,52 @@ export class ChatController {
663737
const session = this.sessionStorage.getSession(message.tabID)
664738
// Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly.
665739
if (session.showDiffOnFileWrite) {
666-
const filePath = session.filePath ?? message.filePath
740+
// Create a temporary file path to show the diff view
741+
const pathToArchiveDir = path.join(tempDirPath, 'q-chat')
742+
const archivePathExists = await fs.existsDir(pathToArchiveDir)
743+
if (archivePathExists) {
744+
await fs.delete(pathToArchiveDir, { recursive: true })
745+
}
746+
await fs.mkdir(pathToArchiveDir)
747+
const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts')
748+
await fs.mkdir(resultArtifactsDir)
749+
const tempFilePath = path.join(
750+
resultArtifactsDir,
751+
`temp-${path.basename((session.toolUse?.input as any).path)}`
752+
)
753+
754+
// If we have existing filePath copy file content from existing file to temporary file.
755+
const filePath = (session.toolUse?.input as any).path ?? message.filePath
667756
const fileExists = await fs.existsFile(filePath)
757+
if (fileExists) {
758+
const fileContent = await fs.readFileText(filePath)
759+
await fs.writeFile(tempFilePath, fileContent)
760+
}
761+
762+
// Create a deep clone of the toolUse object and pass this toolUse to FsWrite tool execution to get the modified temporary file.
763+
const clonedToolUse = structuredClone(session.toolUse)
764+
if (!clonedToolUse) {
765+
return
766+
}
767+
const input = clonedToolUse.input as unknown as FsWriteParams
768+
input.path = tempFilePath
769+
const result = ToolUtils.tryFromToolUse(clonedToolUse)
770+
if (!('type' in result)) {
771+
return
772+
}
773+
const tool: Tool = result
774+
await ToolUtils.validate(tool)
775+
776+
const chatStream = new ChatStream(this.messenger, message.tabID, randomUUID(), clonedToolUse)
777+
await ToolUtils.invoke(tool, chatStream)
778+
668779
// Check if fileExists=false, If yes, return instead of showing broken diff experience.
669-
if (!session.tempFilePath) {
780+
if (!tempFilePath) {
670781
void vscode.window.showInformationMessage('Generated code changes have been reviewed and processed.')
671782
return
672783
}
673784
const leftUri = fileExists ? vscode.Uri.file(filePath) : vscode.Uri.from({ scheme: 'untitled' })
674-
const rightUri = vscode.Uri.file(session.tempFilePath ?? filePath)
785+
const rightUri = vscode.Uri.file(tempFilePath ?? filePath)
675786
const fileName = path.basename(filePath)
676787
await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, `${fileName} ${amazonQTabSuffix}`)
677788
} else {
@@ -925,95 +1036,6 @@ export class ChatController {
9251036
}
9261037
}
9271038

928-
private async processToolUseMessage(message: CustomFormActionMessage) {
929-
const tabID = message.tabID
930-
if (!tabID) {
931-
return
932-
}
933-
this.editorContextExtractor
934-
.extractContextForTrigger('ChatMessage')
935-
.then(async (context) => {
936-
const triggerID = randomUUID()
937-
this.triggerEventsStorage.addTriggerEvent({
938-
id: triggerID,
939-
tabID: message.tabID,
940-
message: undefined,
941-
type: 'chat_message',
942-
context,
943-
})
944-
const session = this.sessionStorage.getSession(tabID)
945-
const toolUse = session.toolUse
946-
if (!toolUse || !toolUse.input) {
947-
return
948-
}
949-
session.setToolUse(undefined)
950-
951-
const toolResults: ToolResult[] = []
952-
953-
const result = ToolUtils.tryFromToolUse(toolUse)
954-
if ('type' in result) {
955-
const tool: Tool = result
956-
957-
try {
958-
await ToolUtils.validate(tool)
959-
960-
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse.toolUseId)
961-
const output = await ToolUtils.invoke(tool, chatStream)
962-
963-
toolResults.push({
964-
content: [
965-
output.output.kind === OutputKind.Text
966-
? { text: output.output.content }
967-
: { json: output.output.content },
968-
],
969-
toolUseId: toolUse.toolUseId,
970-
status: ToolResultStatus.SUCCESS,
971-
})
972-
} catch (e: any) {
973-
toolResults.push({
974-
content: [{ text: e.message }],
975-
toolUseId: toolUse.toolUseId,
976-
status: ToolResultStatus.ERROR,
977-
})
978-
}
979-
} else {
980-
const toolResult: ToolResult = result
981-
toolResults.push(toolResult)
982-
}
983-
984-
await this.generateResponse(
985-
{
986-
message: '',
987-
trigger: ChatTriggerType.ChatMessage,
988-
query: undefined,
989-
codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock,
990-
fileText: context?.focusAreaContext?.extendedCodeBlock ?? '',
991-
fileLanguage: context?.activeFileContext?.fileLanguage,
992-
filePath: context?.activeFileContext?.filePath,
993-
matchPolicy: context?.activeFileContext?.matchPolicy,
994-
codeQuery: context?.focusAreaContext?.names,
995-
userIntent: undefined,
996-
customization: getSelectedCustomization(),
997-
toolResults: toolResults,
998-
origin: Origin.IDE,
999-
chatHistory: this.chatHistoryManager.getHistory(),
1000-
context: [],
1001-
relevantTextDocuments: [],
1002-
additionalContents: [],
1003-
documentReferences: [],
1004-
useRelevantDocuments: false,
1005-
contextLengths: {
1006-
...defaultContextLengths,
1007-
},
1008-
},
1009-
triggerID
1010-
)
1011-
})
1012-
.catch((e) => {
1013-
this.processException(e, tabID)
1014-
})
1015-
}
1016-
10171039
private async processPromptMessageAsNewThread(message: PromptMessage) {
10181040
const session = this.sessionStorage.getSession(message.tabID)
10191041
session.clearListOfReadFiles()

0 commit comments

Comments
 (0)