Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ export class Connector extends BaseConnector {
tabID: tabId,
})

if (!this.onChatAnswerUpdated || !['accept-code-diff', 'reject-code-diff'].includes(action.id)) {
if (
!this.onChatAnswerUpdated ||
!['accept-code-diff', 'reject-code-diff', 'confirm-tool-use'].includes(action.id)
) {
return
}
const answer: ChatItem = {
Expand Down Expand Up @@ -297,6 +300,18 @@ export class Connector extends BaseConnector {
},
]
break
case 'confirm-tool-use':
answer.buttons = [
{
keepCardAfterClick: true,
text: 'Confirmed',
id: 'confirmed-tool-use',
status: 'success',
position: 'outside',
disabled: true,
},
]
break
default:
break
}
Expand Down
16 changes: 0 additions & 16 deletions packages/core/src/codewhispererChat/clients/chat/v0/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ 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.
* _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user
*/
private _readFiles: string[] = []
private _filePath: string | undefined
private _tempFilePath: string | undefined
private _toolUse: ToolUse | undefined
private _showDiffOnFileWrite: boolean = false

Expand Down Expand Up @@ -61,24 +57,12 @@ export class ChatSession {
public get readFiles(): string[] {
return this._readFiles
}
public get filePath(): string | undefined {
return this._filePath
}
public get tempFilePath(): string | undefined {
return this._tempFilePath
}
public get showDiffOnFileWrite(): boolean {
return this._showDiffOnFileWrite
}
public setShowDiffOnFileWrite(value: boolean) {
this._showDiffOnFileWrite = value
}
public setFilePath(filePath: string | undefined) {
this._filePath = filePath
}
public setTempFilePath(tempFilePath: string | undefined) {
this._tempFilePath = tempFilePath
}
public addToReadFiles(filePath: string) {
this._readFiles.push(filePath)
}
Expand Down
301 changes: 163 additions & 138 deletions packages/core/src/codewhispererChat/controllers/chat/controller.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ import { LspController } from '../../../../amazonq/lsp/lspController'
import { extractCodeBlockLanguage } from '../../../../shared/markdown'
import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils'
import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants'
import { ChatItemButton, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui'
import { ChatItemButton, ChatItemContent, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui'
import { ChatHistoryManager } from '../../../storages/chatHistory'
import { ToolUtils } from '../../../tools/toolUtils'
import { ToolType, ToolUtils } from '../../../tools/toolUtils'
import { ChatStream } from '../../../tools/chatStream'
import path from 'path'
import { getWorkspaceForFile } from '../../../../shared/utilities/workspaceUtils'

export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help'

Expand Down Expand Up @@ -215,21 +217,18 @@ export class Messenger {

const tool = ToolUtils.tryFromToolUse(toolUse)
if ('type' in tool) {
if (tool.type === ToolType.FsWrite) {
session.setShowDiffOnFileWrite(true)
}
const requiresAcceptance = ToolUtils.requiresAcceptance(tool)

const chatStream = new ChatStream(
this,
tabID,
triggerID,
toolUse.toolUseId,
requiresAcceptance
)
const chatStream = new ChatStream(this, tabID, triggerID, toolUse, requiresAcceptance)
ToolUtils.queueDescription(tool, chatStream)

if (!requiresAcceptance) {
// Need separate id for read tool and safe bash command execution as 'confirm-tool-use' id is required to change button status from `Confirm` to `Confirmed` state in cwChatConnector.ts which will impact generic tool execution.
this.dispatcher.sendCustomFormActionMessage(
new CustomFormActionMessage(tabID, {
id: 'confirm-tool-use',
id: 'generic-tool-execution',
})
)
}
Expand Down Expand Up @@ -432,15 +431,40 @@ export class Messenger {
message: string,
tabID: string,
triggerID: string,
toolUseId: string | undefined,
toolUse: ToolUse | undefined,
requiresAcceptance = false
) {
const buttons: ChatItemButton[] = []
if (requiresAcceptance) {
let fileList: ChatItemContent['fileList'] = {}
if (requiresAcceptance && toolUse?.name === ToolType.ExecuteBash) {
buttons.push({
id: 'confirm-tool-use',
text: 'Confirm',
position: 'outside',
status: 'info',
})
} else if (toolUse?.name === ToolType.FsWrite) {
// FileList
const absoluteFilePath = (toolUse?.input as any).path
const projectPath = getWorkspaceForFile(absoluteFilePath)
const relativePath = projectPath ? path.relative(projectPath, absoluteFilePath) : absoluteFilePath
fileList = {
fileTreeTitle: 'Code suggestions',
rootFolderTitle: path.basename(projectPath ?? 'Default'),
filePaths: [relativePath],
}
// Buttons
buttons.push({
id: 'reject-code-diff',
text: 'Reject',
position: 'outside',
status: 'error',
})
buttons.push({
id: 'accept-code-diff',
text: 'Accept',
position: 'outside',
status: 'success',
})
}

Expand All @@ -453,12 +477,13 @@ export class Messenger {
followUpsHeader: undefined,
relatedSuggestions: undefined,
triggerID,
messageID: toolUseId ?? `tool-output`,
messageID: toolUse?.toolUseId ?? `tool-output`,
userIntent: undefined,
codeBlockLanguage: undefined,
contextList: undefined,
canBeVoted: false,
buttons,
fileList: toolUse?.name === ToolType.FsWrite ? fileList : undefined,
},
tabID
)
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/codewhispererChat/tools/chatStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Writable } from 'stream'
import { getLogger } from '../../shared/logger/logger'
import { Messenger } from '../controllers/chat/messenger/messenger'
import { ToolUse } from '@amzn/codewhisperer-streaming'

/**
* A writable stream that feeds each chunk/line to the chat UI.
Expand All @@ -18,7 +19,7 @@ export class ChatStream extends Writable {
private readonly messenger: Messenger,
private readonly tabID: string,
private readonly triggerID: string,
private readonly toolUseId: string | undefined,
private readonly toolUse: ToolUse | undefined,
private readonly requiresAcceptance = false,
private readonly logger = getLogger('chatStream')
) {
Expand All @@ -35,7 +36,7 @@ export class ChatStream extends Writable {
this.accumulatedLogs,
this.tabID,
this.triggerID,
this.toolUseId,
this.toolUse,
this.requiresAcceptance
)
callback()
Expand All @@ -47,7 +48,7 @@ export class ChatStream extends Writable {
this.accumulatedLogs,
this.tabID,
this.triggerID,
this.toolUseId,
this.toolUse,
this.requiresAcceptance
)
}
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/codewhispererChat/tools/executeBash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,19 @@ export class ExecuteBash {
}
}

public async invoke(updates: Writable): Promise<InvokeOutput> {
public async invoke(updates?: Writable): Promise<InvokeOutput> {
this.logger.info(`Invoking bash command: "${this.command}" in cwd: "${this.workingDirectory}"`)
if (!updates) {
// updates passed as undefined only in the code diff is clicked to avoid showing Q Streaming spinner component to the user.
return new Promise(async (resolve) => {
resolve({
output: {
kind: OutputKind.Json,
content: '',
},
})
})
}

return new Promise(async (resolve, reject) => {
this.logger.debug(`Spawning process with command: bash -c "${this.command}" (cwd=${this.workingDirectory})`)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/codewhispererChat/tools/fsRead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class FsRead {
updates.end()
}

public async invoke(updates: Writable): Promise<InvokeOutput> {
public async invoke(updates?: Writable): Promise<InvokeOutput> {
try {
const fileUri = vscode.Uri.file(this.fsPath)

Expand Down
13 changes: 4 additions & 9 deletions packages/core/src/codewhispererChat/tools/fsWrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class FsWrite {

constructor(private readonly params: FsWriteParams) {}

public async invoke(updates: Writable): Promise<InvokeOutput> {
public async invoke(updates?: Writable): Promise<InvokeOutput> {
const sanitizedPath = sanitizePath(this.params.path)

switch (this.params.command) {
Expand Down Expand Up @@ -72,8 +72,9 @@ export class FsWrite {

public queueDescription(updates: Writable): void {
const fileName = path.basename(this.params.path)
const fileUri = vscode.Uri.file(this.params.path)
updates.write(`Writing to: [${fileName}](${fileUri})`)
updates.write(
`Please see the generated code below for \`${fileName}\`. Click on the file to review the changes in the code editor and select Accept or Reject.`
)
updates.end()
}

Expand Down Expand Up @@ -135,8 +136,6 @@ export class FsWrite {

const newContent = fileContent.replace(params.oldStr, params.newStr)
await fs.writeFile(sanitizedPath, newContent)

void vscode.window.showInformationMessage(`Updated: ${sanitizedPath}`)
}

private async handleInsert(params: InsertParams, sanitizedPath: string): Promise<void> {
Expand All @@ -154,8 +153,6 @@ export class FsWrite {
}

await fs.writeFile(sanitizedPath, newContent)

void vscode.window.showInformationMessage(`Updated: ${sanitizedPath}`)
}

private async handleAppend(params: AppendParams, sanitizedPath: string): Promise<void> {
Expand All @@ -169,8 +166,6 @@ export class FsWrite {

const newContent = fileContent + contentToAppend
await fs.writeFile(sanitizedPath, newContent)

void vscode.window.showInformationMessage(`Updated: ${sanitizedPath}`)
}

private getCreateCommandText(params: CreateParams): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/codewhispererChat/tools/toolUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class ToolUtils {
}
}

static async invoke(tool: Tool, updates: Writable): Promise<InvokeOutput> {
static async invoke(tool: Tool, updates?: Writable): Promise<InvokeOutput> {
switch (tool.type) {
case ToolType.FsRead:
return tool.tool.invoke(updates)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('ToolUtils', function () {
const result = await ToolUtils.invoke(tool, mockWritable as unknown as Writable)

assert.deepStrictEqual(result, expectedOutput)
assert(mockFsRead.invoke.calledOnceWith(mockWritable))
assert(mockFsRead.invoke.calledOnceWith(mockWritable as unknown as Writable | undefined))
})

it('delegates to FsWrite tool invoke method', async function () {
Expand All @@ -106,7 +106,7 @@ describe('ToolUtils', function () {
const result = await ToolUtils.invoke(tool, mockWritable as unknown as Writable)

assert.deepStrictEqual(result, expectedOutput)
assert(mockFsWrite.invoke.calledOnceWith(mockWritable))
assert(mockFsWrite.invoke.calledOnceWith(mockWritable as unknown as Writable | undefined))
})

it('delegates to ExecuteBash tool invoke method', async function () {
Expand All @@ -122,7 +122,7 @@ describe('ToolUtils', function () {
const result = await ToolUtils.invoke(tool, mockWritable as unknown as Writable)

assert.deepStrictEqual(result, expectedOutput)
assert(mockExecuteBash.invoke.calledOnceWith(mockWritable))
assert(mockExecuteBash.invoke.calledOnceWith(mockWritable as unknown as Writable | undefined))
})
})

Expand Down
Loading