Skip to content

Commit 547a207

Browse files
authored
feat(chat): support multi file write with reject and open diff (#23) POC
1 parent 116cc77 commit 547a207

File tree

6 files changed

+73
-64
lines changed

6 files changed

+73
-64
lines changed

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ export class Connector extends BaseConnector {
314314

315315
if (
316316
!this.onChatAnswerUpdated ||
317-
!['accept-code-diff', 'reject-code-diff', 'confirm-tool-use'].includes(action.id)
317+
!(['accept-code-diff', 'confirm-tool-use'].includes(action.id) || action.id.startsWith('reject-code-diff'))
318318
) {
319319
return
320320
}
@@ -341,17 +341,6 @@ export class Connector extends BaseConnector {
341341
answer.body = ' '
342342
}
343343
break
344-
case 'reject-code-diff':
345-
if (answer.header) {
346-
answer.header.status = {
347-
icon: 'cancel' as MynahIconsType,
348-
text: 'Rejected',
349-
status: 'error',
350-
}
351-
answer.header.buttons = []
352-
answer.body = ' '
353-
}
354-
break
355344
case 'confirm-tool-use':
356345
answer.buttons = [
357346
{
@@ -367,6 +356,17 @@ export class Connector extends BaseConnector {
367356
default:
368357
break
369358
}
359+
if (action.id.startsWith('reject-code-diff')) {
360+
if (answer.header) {
361+
answer.header.status = {
362+
icon: 'cancel' as MynahIconsType,
363+
text: 'Rejected',
364+
status: 'error',
365+
}
366+
answer.header.buttons = []
367+
answer.body = ' '
368+
}
369+
}
370370

371371
if (currentChatItem && answer.messageId) {
372372
const updatedItem = { ...currentChatItem, ...answer }

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class ChatSession {
2828
private _showDiffOnFileWrite: boolean = false
2929
private _context: PromptMessage['context']
3030
private _pairProgrammingModeOn: boolean = true
31+
private _fsWriteBackups: Map<string, { filePath: string; content: string; isNew: boolean }> = new Map()
3132

3233
contexts: Map<string, { first: number; second: number }[]> = new Map()
3334
// TODO: doesn't handle the edge case when two files share the same relativePath string but from different root
@@ -53,6 +54,20 @@ export class ChatSession {
5354
this._toolUse = toolUse
5455
}
5556

57+
public get fsWriteBackups() {
58+
return this._fsWriteBackups
59+
}
60+
61+
public setFsWriteBackups(
62+
toolUseId: string | undefined,
63+
content: { filePath: string; content: string; isNew: boolean }
64+
) {
65+
if (!toolUseId) {
66+
return
67+
}
68+
this._fsWriteBackups.set(toolUseId, content)
69+
}
70+
5671
public get context(): PromptMessage['context'] {
5772
return this._context
5873
}

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

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ import { maxToolOutputCharacterLength, OutputKind } from '../../tools/toolShared
9292
import { ToolUtils, Tool, ToolType } from '../../tools/toolUtils'
9393
import { ChatStream } from '../../tools/chatStream'
9494
import { ChatHistoryStorage } from '../../storages/chatHistoryStorage'
95-
import { FsWrite, FsWriteParams } from '../../tools/fsWrite'
95+
import { FsWriteParams } from '../../tools/fsWrite'
9696
import { tempDirPath } from '../../../shared/filesystemUtilities'
9797

9898
export interface ChatControllerMessagePublishers {
@@ -739,6 +739,10 @@ export class ChatController {
739739
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, {
740740
requiresAcceptance: false,
741741
})
742+
if (tool.type === ToolType.FsWrite) {
743+
const backup = await tool.tool.getOldContent()
744+
session.setFsWriteBackups(toolUse.toolUseId, backup)
745+
}
742746
const output = await ToolUtils.invoke(tool, chatStream)
743747
if (output.output.content.length > maxToolOutputCharacterLength) {
744748
throw Error(
@@ -818,22 +822,29 @@ export class ChatController {
818822
case 'submit-create-prompt':
819823
await this.handleCreatePrompt(message)
820824
break
821-
case 'accept-code-diff':
822-
await this.closeDiffView()
823-
break
824825
case 'confirm-tool-use':
825826
case 'generic-tool-execution':
826827
await this.processToolUseMessage(message)
827828
break
828-
case 'reject-code-diff':
829-
await this.closeDiffView()
830-
break
831829
case 'tool-unavailable':
832830
await this.processUnavailableToolUseMessage(message)
833831
break
834832
default:
835833
getLogger().warn(`Unhandled action: ${message.action.id}`)
836834
}
835+
836+
if (message.action.id.startsWith('reject-code-diff')) {
837+
// revert the changes
838+
const toolUseId = message.action.id.split('/')[1]
839+
const backups = this.sessionStorage.getSession(message.tabID!).fsWriteBackups
840+
const { filePath, content, isNew } = backups.get(toolUseId) ?? {}
841+
if (filePath && isNew) {
842+
await fs.delete(filePath)
843+
} else if (filePath && content !== undefined) {
844+
await fs.writeFile(filePath, content)
845+
}
846+
await this.closeDiffView()
847+
}
837848
}
838849

839850
private async processContextSelected(message: ContextSelectedMessage) {
@@ -855,8 +866,10 @@ export class ChatController {
855866

856867
private async processFileClickMessage(message: FileClick) {
857868
const session = this.sessionStorage.getSession(message.tabID)
869+
const toolUseId = message.messageId
870+
const backup = session.fsWriteBackups.get(toolUseId)
858871
// Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly.
859-
if (session.showDiffOnFileWrite) {
872+
if (session.showDiffOnFileWrite && backup?.filePath) {
860873
try {
861874
// Create a temporary file path to show the diff view
862875
const pathToArchiveDir = path.join(tempDirPath, 'q-chat')
@@ -867,40 +880,13 @@ export class ChatController {
867880
await fs.mkdir(pathToArchiveDir)
868881
const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts')
869882
await fs.mkdir(resultArtifactsDir)
870-
const tempFilePath = path.join(
871-
resultArtifactsDir,
872-
`temp-${path.basename((session.toolUse?.input as unknown as FsWriteParams).path)}`
873-
)
883+
const tempFilePath = path.join(resultArtifactsDir, `temp-${path.basename(backup.filePath)}`)
874884

875-
// If we have existing filePath copy file content from existing file to temporary file.
876-
const filePath = (session.toolUse?.input as any).path ?? message.filePath
877-
const fileExists = await fs.existsFile(filePath)
878-
if (fileExists) {
879-
const fileContent = await fs.readFileText(filePath)
880-
await fs.writeFile(tempFilePath, fileContent)
881-
}
885+
await fs.writeFile(tempFilePath, backup.content)
882886

883-
// Create a deep clone of the toolUse object and pass this toolUse to FsWrite tool execution to get the modified temporary file.
884-
const clonedToolUse = structuredClone(session.toolUse)
885-
if (!clonedToolUse) {
886-
return
887-
}
888-
const input = clonedToolUse.input as unknown as FsWriteParams
889-
input.path = tempFilePath
890-
891-
const fsWrite = new FsWrite(input)
892-
await fsWrite.invoke()
893-
894-
// Check if fileExists=false, If yes, return instead of showing broken diff experience.
895-
if (!tempFilePath) {
896-
void vscode.window.showInformationMessage(
897-
'Generated code changes have been reviewed and processed.'
898-
)
899-
return
900-
}
901-
const leftUri = fileExists ? vscode.Uri.file(filePath) : vscode.Uri.from({ scheme: 'untitled' })
902-
const rightUri = vscode.Uri.file(tempFilePath ?? filePath)
903-
const fileName = path.basename(filePath)
887+
const leftUri = vscode.Uri.file(tempFilePath)
888+
const rightUri = vscode.Uri.file(backup.filePath)
889+
const fileName = path.basename(backup.filePath)
904890
await vscode.commands.executeCommand(
905891
'vscode.diff',
906892
leftUri,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ export class Messenger {
489489
}
490490
// Buttons
491491
buttons.push({
492-
id: 'reject-code-diff',
492+
id: `reject-code-diff/${toolUse.toolUseId}`,
493493
status: 'clear',
494494
icon: 'cancel' as MynahIconsType,
495495
})

packages/core/src/codewhispererChat/tools/fsWrite.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,31 +83,39 @@ export class FsWrite {
8383
}
8484

8585
public async getDiffChanges(): Promise<Change[]> {
86-
const sanitizedPath = sanitizePath(this.params.path)
86+
const { filePath, content: oldContent } = await this.getOldContent()
8787
let newContent
88-
let oldContent
89-
try {
90-
oldContent = await fs.readFileText(sanitizedPath)
91-
} catch (err) {
92-
oldContent = ''
93-
}
9488
switch (this.params.command) {
9589
case 'create':
9690
newContent = this.getCreateCommandText(this.params)
9791
break
9892
case 'strReplace':
99-
newContent = await this.getStrReplaceContent(this.params, sanitizedPath)
93+
newContent = await this.getStrReplaceContent(this.params, filePath)
10094
break
10195
case 'insert':
102-
newContent = await this.getInsertContent(this.params, sanitizedPath)
96+
newContent = await this.getInsertContent(this.params, filePath)
10397
break
10498
case 'append':
105-
newContent = await this.getAppendContent(this.params, sanitizedPath)
99+
newContent = await this.getAppendContent(this.params, filePath)
106100
break
107101
}
108102
return diffLines(oldContent, newContent)
109103
}
110104

105+
public async getOldContent(): Promise<{ filePath: string; content: string; isNew: boolean }> {
106+
const sanitizedPath = sanitizePath(this.params.path)
107+
let oldContent
108+
let isNew
109+
try {
110+
oldContent = await fs.readFileText(sanitizedPath)
111+
isNew = false
112+
} catch (err) {
113+
oldContent = ''
114+
isNew = true
115+
}
116+
return { filePath: sanitizedPath, content: oldContent, isNew }
117+
}
118+
111119
public async validate(): Promise<void> {
112120
switch (this.params.command) {
113121
case 'create':

packages/core/src/codewhispererChat/tools/toolUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class ToolUtils {
4242
case ToolType.FsRead:
4343
return { requiresAcceptance: false }
4444
case ToolType.FsWrite:
45-
return { requiresAcceptance: true }
45+
return { requiresAcceptance: false }
4646
case ToolType.ExecuteBash:
4747
return tool.tool.requiresAcceptance()
4848
case ToolType.ListDirectory:

0 commit comments

Comments
 (0)