From 22fbd166ea1e60206b1b2fb7b8bff5a2e93707da Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Tue, 1 Apr 2025 14:58:30 -0700 Subject: [PATCH 01/19] formatting the stderr --- packages/core/src/codewhispererChat/tools/executeBash.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index 73aa52e3762..8c42d1dd205 100644 --- a/packages/core/src/codewhispererChat/tools/executeBash.ts +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -75,6 +75,7 @@ export class ExecuteBash { const stderrBuffer: string[] = [] let firstChunk = true + let firstStderrChunk = true const childProcessOptions: ChildProcessOptions = { spawnOptions: { cwd: this.workingDirectory, @@ -87,7 +88,8 @@ export class ExecuteBash { firstChunk = false }, onStderr: (chunk: string) => { - ExecuteBash.handleChunk(chunk, stderrBuffer, updates) + ExecuteBash.handleChunk(firstStderrChunk ? '```console\n' + chunk : chunk, stderrBuffer, updates) + firstStderrChunk = false }, } From 68a18b44b8af7f9be19f2615c3d02c31a9776214 Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Thu, 3 Apr 2025 17:17:27 -0700 Subject: [PATCH 02/19] fix the no spinner for writig a file & change the UI on console & update mynahUI version --- packages/core/package.json | 2 +- .../codewhispererChat/controllers/chat/messenger/messenger.ts | 4 ++++ packages/core/src/codewhispererChat/tools/executeBash.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 7d64a740305..12d5154bfb0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -522,7 +522,7 @@ "@aws-sdk/s3-request-presigner": "<3.731.0", "@aws-sdk/smithy-client": "<3.731.0", "@aws-sdk/util-arn-parser": "<3.731.0", - "@aws/mynah-ui": "^4.26.1", + "@aws/mynah-ui": "^4.28.0", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index d19e88c0b47..ba878a6582e 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -235,6 +235,10 @@ export class Messenger { } else { // TODO: Handle the error } + } else if (cwChatEvent.toolUseEvent?.stop === undefined) { + if (toolUseInput !== '') { + this.sendInitalStream(tabID, triggerID, undefined) + } } if ( diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index 8c42d1dd205..d4d28bedca0 100644 --- a/packages/core/src/codewhispererChat/tools/executeBash.ts +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -208,7 +208,7 @@ export class ExecuteBash { public queueDescription(updates: Writable): void { updates.write(`I will run the following shell command:\n`) - updates.write('```bash\n' + this.command + '\n```') + updates.write('```shell\n' + this.command + '\n```') updates.end() } } From ce978c6550b4ff9c1c877d565925e00cd6fa9810 Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Thu, 3 Apr 2025 19:43:27 -0700 Subject: [PATCH 03/19] add lock file --- package-lock.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fd4035f62d..36d993de7b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11966,10 +11966,11 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.26.1.tgz", - "integrity": "sha512-qUgQ6NVmiCSp6qF43cM6522U8QtBTBbqDv5yKS/5tl9cEQuZJSfKDq2zFUstQNZmsw0GE8p/NboTDo3mRv4sSQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.28.0.tgz", + "integrity": "sha512-HKL65KBOMap7ZZrQs/Ol08zfj48A7paPWzm/vNsAYeL/dE25EmXYkkS3YIZSveaYF0ltYUt7hzyQnNWl/18KKA==", "hasInstallScript": true, + "license": "Apache License 2.0", "dependencies": { "escape-html": "^1.0.3", "highlight.js": "^11.11.0", @@ -26744,7 +26745,7 @@ "@aws-sdk/s3-request-presigner": "<3.731.0", "@aws-sdk/smithy-client": "<3.731.0", "@aws-sdk/util-arn-parser": "<3.731.0", - "@aws/mynah-ui": "^4.26.1", + "@aws/mynah-ui": "^4.28.0", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", From 799a84e1790c1bf7358ac8c03cf97280ff5cfc99 Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Thu, 3 Apr 2025 22:33:39 -0700 Subject: [PATCH 04/19] adress comment --- .../controllers/chat/messenger/messenger.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 45f502329b3..71d086c0a89 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -262,10 +262,8 @@ export class Messenger { } else { // TODO: Handle the error } - } else if (cwChatEvent.toolUseEvent?.stop === undefined) { - if (toolUseInput !== '') { - this.sendInitalStream(tabID, triggerID, undefined) - } + } else if (cwChatEvent.toolUseEvent?.stop === undefined && toolUseInput !== '') { + this.sendInitalStream(tabID, triggerID, undefined) } if ( From cc2fa97c811f71f67ae61a09d46b7a12d6f70030 Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Fri, 4 Apr 2025 12:14:53 -0700 Subject: [PATCH 05/19] add comment for the fix --- .../codewhispererChat/controllers/chat/messenger/messenger.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 71d086c0a89..ceca2650534 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -263,6 +263,7 @@ export class Messenger { // TODO: Handle the error } } else if (cwChatEvent.toolUseEvent?.stop === undefined && toolUseInput !== '') { + // This is for the case when writing tool is executed. The toolUseEvent is non stop but in toolUseInput is not empty. In this case we need show user the current spinner UI. this.sendInitalStream(tabID, triggerID, undefined) } From 583174552397178bfb0a5279841b0af03e958f74 Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Fri, 4 Apr 2025 15:03:53 -0700 Subject: [PATCH 06/19] fix the duplication --- .../chat/controller/messenger/messenger.ts | 24 ++++++-------- .../controllers/chat/messenger/messenger.ts | 24 ++++++-------- .../core/src/shared/utilities/messageUtil.ts | 31 +++++++++++++++++++ 3 files changed, 50 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/shared/utilities/messageUtil.ts diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts index 5541ef389c5..433d0d8b280 100644 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts @@ -39,6 +39,7 @@ import { keys } from '../../../../shared/utilities/tsUtils' import { cancellingProgressField, testGenCompletedField } from '../../../models/constants' import { testGenState } from '../../../../codewhisperer/models/model' import { TelemetryHelper } from '../../../../codewhisperer/util/telemetryHelper' +import { extractErrorInfo } from '../../../../shared/utilities/messageUtil' export type UnrecoverableErrorType = 'no-project-found' | 'no-open-file-found' | 'invalid-file-type' @@ -249,26 +250,19 @@ export class Messenger { { timeout: 60000, truthy: true } ) .catch((error: any) => { - let errorMessage = 'Error reading chat stream.' - let statusCode = undefined - let requestID = undefined - if (error instanceof CodeWhispererStreamingServiceException) { - errorMessage = error.message - statusCode = getHttpStatusCode(error) ?? 0 - requestID = getRequestId(error) - } + const errorInfo = extractErrorInfo(error) let message = 'This error is reported to the team automatically. Please try sending your message again.' - if (errorMessage !== undefined) { - message += `\n\nDetails: ${errorMessage}` + if (errorInfo.errorMessage !== undefined) { + message += `\n\nDetails: ${errorInfo.errorMessage}` } - if (statusCode !== undefined) { - message += `\n\nStatus Code: ${statusCode}` + if (errorInfo.statusCode !== undefined) { + message += `\n\nStatus Code: ${errorInfo.statusCode}` } - if (requestID !== undefined) { - messageId = requestID - message += `\n\nRequest ID: ${requestID}` + if (errorInfo.requestId !== undefined) { + messageId = errorInfo.requestId + message += `\n\nRequest ID: ${errorInfo.requestId}` } this.sendMessage(message.trim(), tabID, 'answer') }) diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index ceca2650534..b035dbf6dcf 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -47,6 +47,7 @@ import { ChatStream } from '../../../tools/chatStream' import { getWorkspaceForFile } from '../../../../shared/utilities/workspaceUtils' import path from 'path' import { CommandValidation } from '../../../tools/executeBash' +import { extractErrorInfo } from '../../../../shared/utilities/messageUtil' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -326,26 +327,21 @@ export class Messenger { { timeout: 60000, truthy: true } ) .catch((error: any) => { - let errorMessage = 'Error reading chat stream.' - let statusCode = undefined - let requestID = undefined - - if (error instanceof CodeWhispererStreamingServiceException) { - errorMessage = error.message - statusCode = getHttpStatusCode(error) ?? 0 - requestID = getRequestId(error) - } - + const errorInfo = extractErrorInfo(error) this.showChatExceptionMessage( - { errorMessage, statusCode: statusCode?.toString(), sessionID: undefined }, + { + errorMessage: errorInfo.errorMessage, + statusCode: errorInfo.statusCode?.toString(), + sessionID: undefined, + }, tabID, - requestID + errorInfo.requestId ) - getLogger().error(`error: ${errorMessage} tabID: ${tabID} requestID: ${requestID}`) + getLogger().error(`error: ${errorInfo.errorMessage} tabID: ${tabID} requestID: ${errorInfo.requestId}`) followUps = [] relatedSuggestions = [] - this.telemetryHelper.recordMessageResponseError(triggerPayload, tabID, statusCode ?? 0) + this.telemetryHelper.recordMessageResponseError(triggerPayload, tabID, errorInfo.statusCode ?? 0) }) .finally(async () => { if ( diff --git a/packages/core/src/shared/utilities/messageUtil.ts b/packages/core/src/shared/utilities/messageUtil.ts new file mode 100644 index 00000000000..f2cefe824de --- /dev/null +++ b/packages/core/src/shared/utilities/messageUtil.ts @@ -0,0 +1,31 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' +import { getHttpStatusCode, getRequestId } from '../errors' + +export interface MessageErrorInfo { + errorMessage: string + statusCode?: number + requestId?: string +} + +export function extractErrorInfo(error: any): MessageErrorInfo { + let errorMessage = 'Error reading chat stream.' + let statusCode = undefined + let requestId = undefined + + if (error instanceof CodeWhispererStreamingServiceException) { + errorMessage = error.message + statusCode = getHttpStatusCode(error) ?? 0 + requestId = getRequestId(error) + } + + return { + errorMessage, + statusCode, + requestId, + } +} From 06864a9c9339a33dd9831823effa13298f238909 Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Fri, 4 Apr 2025 17:18:21 -0700 Subject: [PATCH 07/19] fix linter --- package-lock.json | 55 ++++++++++++------- package.json | 2 +- .../chat/controller/messenger/messenger.ts | 7 +-- .../controllers/chat/messenger/messenger.ts | 3 +- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 191fc6203ad..5ece1a81273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "husky": "^9.0.7", "prettier": "^3.3.3", "prettier-plugin-sh": "^0.14.0", - "pretty-quick": "^4.0.0", + "pretty-quick": "^4.1.1", "ts-node": "^10.9.1", "typescript": "^5.0.4", "webpack": "^5.95.0", @@ -22512,7 +22512,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -22849,17 +22851,19 @@ } }, "node_modules/pretty-quick": { - "version": "4.0.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-4.1.1.tgz", + "integrity": "sha512-9Ud0l/CspNTmyIdYac9X7Inb3o8fuUsw+1zJFvCGn+at0t1UwUcUdo2RSZ41gcmfLv1fxgWQxWEfItR7CBwugg==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.1.1", "find-up": "^5.0.0", - "ignore": "^5.3.0", + "ignore": "^7.0.3", "mri": "^1.2.0", - "picocolors": "^1.0.0", - "picomatch": "^3.0.1", - "tslib": "^2.6.2" + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "tinyexec": "^0.3.2", + "tslib": "^2.8.1" }, "bin": { "pretty-quick": "lib/cli.mjs" @@ -22871,12 +22875,24 @@ "prettier": "^3.0.0" } }, + "node_modules/pretty-quick/node_modules/ignore": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/pretty-quick/node_modules/picomatch": { - "version": "3.0.1", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -24915,6 +24931,13 @@ "next-tick": "1" } }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.1", "dev": true, @@ -25061,7 +25084,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsscmp": { @@ -30278,10 +30303,6 @@ "tree-kill": "cli.js" } }, - "src.gen/@amzn/amazon-q-developer-streaming-client/node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, "src.gen/@amzn/amazon-q-developer-streaming-client/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -31845,10 +31866,6 @@ "tree-kill": "cli.js" } }, - "src.gen/@amzn/codewhisperer-streaming/node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, "src.gen/@amzn/codewhisperer-streaming/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index d33ff4f4f51..0719860c2b0 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "husky": "^9.0.7", "prettier": "^3.3.3", "prettier-plugin-sh": "^0.14.0", - "pretty-quick": "^4.0.0", + "pretty-quick": "^4.1.1", "ts-node": "^10.9.1", "typescript": "^5.0.4", "webpack": "^5.95.0", diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts index 433d0d8b280..c5359a2bfb8 100644 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts @@ -27,13 +27,10 @@ import { ChatItemType } from '../../../../amazonq/commons/model' import { ChatItemAction, ChatItemButton, ProgressField } from '@aws/mynah-ui' import * as CodeWhispererConstants from '../../../../codewhisperer/models/constants' import { TriggerPayload } from '../../../../codewhispererChat/controllers/chat/model' -import { - CodeWhispererStreamingServiceException, - GenerateAssistantResponseCommandOutput, -} from '@amzn/codewhisperer-streaming' +import { GenerateAssistantResponseCommandOutput } from '@amzn/codewhisperer-streaming' import { Session } from '../../session/session' import { CodeReference } from '../../../../amazonq/webview/ui/apps/amazonqCommonsConnector' -import { getHttpStatusCode, getRequestId, getTelemetryReasonDesc, ToolkitError } from '../../../../shared/errors' +import { getTelemetryReasonDesc, ToolkitError } from '../../../../shared/errors' import { sleep, waitUntil } from '../../../../shared/utilities/timeoutUtils' import { keys } from '../../../../shared/utilities/tsUtils' import { cancellingProgressField, testGenCompletedField } from '../../../models/constants' diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index b035dbf6dcf..e993bd49ca0 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -20,7 +20,6 @@ import { EditorContextCommandType } from '../../../commands/registerCommands' import { ChatResponseStream as qdevChatResponseStream } from '@amzn/amazon-q-developer-streaming-client' import { ChatResponseStream as cwChatResponseStream, - CodeWhispererStreamingServiceException, SupplementaryWebLink, ToolUse, } from '@amzn/codewhisperer-streaming' @@ -29,7 +28,7 @@ import { ChatSession } from '../../../clients/chat/v0/chat' import { ChatException } from './model' import { CWCTelemetryHelper } from '../telemetryHelper' import { ChatPromptCommandType, DocumentReference, TriggerPayload } from '../model' -import { getHttpStatusCode, getRequestId, ToolkitError } from '../../../../shared/errors' +import { ToolkitError } from '../../../../shared/errors' import { keys } from '../../../../shared/utilities/tsUtils' import { getLogger } from '../../../../shared/logger/logger' import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' From b5491f2a1b96630b9815e41af7777d561d109ef1 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Sat, 5 Apr 2025 14:42:09 -0700 Subject: [PATCH 08/19] Adding Stop button and stopping generation for new user prompt --- .../webview/ui/apps/cwChatConnector.ts | 57 +++++-------- packages/core/src/amazonq/webview/ui/main.ts | 5 +- .../codewhispererChat/clients/chat/v0/chat.ts | 10 --- .../controllers/chat/controller.ts | 17 ++-- .../controllers/chat/messenger/messenger.ts | 85 +++++++------------ .../codewhispererChat/storages/chatHistory.ts | 16 ++++ .../src/codewhispererChat/tools/chatStream.ts | 8 +- .../view/connector/connector.ts | 13 ++- 8 files changed, 88 insertions(+), 123 deletions(-) diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index b6dd4f7be0b..113ca8dd23c 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -27,6 +27,14 @@ export interface ConnectorProps extends BaseConnectorProps { description?: string ) => void onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void + onAsyncEventProgress: ( + tabID: string, + inProgress: boolean, + message: string, + messageId: string | undefined, + enableStopAction: boolean, + isPromptInputDisabled: boolean + ) => void } export class Connector extends BaseConnector { @@ -34,6 +42,7 @@ export class Connector extends BaseConnector { private readonly onContextCommandDataReceived private readonly onShowCustomForm private readonly onChatAnswerUpdated + private readonly onAsyncEventProgress private chatItems: Map> = new Map() // tabId -> messageId -> ChatItem override getTabType(): TabType { @@ -46,6 +55,7 @@ export class Connector extends BaseConnector { this.onContextCommandDataReceived = props.onContextCommandDataReceived this.onShowCustomForm = props.onShowCustomForm this.onChatAnswerUpdated = props.onChatAnswerUpdated + this.onAsyncEventProgress = props.onAsyncEventProgress } onSourceLinkClick = (tabID: string, messageId: string, link: string): void => { @@ -168,35 +178,6 @@ export class Connector extends BaseConnector { } } - private processToolMessage = async (messageData: any): Promise => { - if (this.onChatAnswerUpdated === undefined) { - return - } - const answer: CWCChatItem = { - type: messageData.messageType, - messageId: messageData.messageID ?? messageData.triggerID, - body: messageData.message, - followUp: messageData.followUps, - canBeVoted: messageData.canBeVoted ?? false, - codeReference: messageData.codeReference, - userIntent: messageData.contextList, - codeBlockLanguage: messageData.codeBlockLanguage, - contextList: messageData.contextList, - title: messageData.title, - buttons: messageData.buttons, - fileList: messageData.fileList, - header: messageData.header ?? undefined, - padding: messageData.padding ?? undefined, - fullWidth: messageData.fullWidth ?? undefined, - codeBlockActions: messageData.codeBlockActions ?? undefined, - } - if (answer.messageId) { - this.storeChatItem(messageData.tabID, answer.messageId, answer) - } - this.onChatAnswerUpdated(messageData.tabID, answer) - return - } - private storeChatItem(tabId: string, messageId: string, item: ChatItem): void { if (!this.chatItems.has(tabId)) { this.chatItems.set(tabId, new Map()) @@ -253,11 +234,6 @@ export class Connector extends BaseConnector { return } - if (messageData.type === 'toolMessage') { - await this.processToolMessage(messageData) - return - } - if (messageData.type === 'editorContextCommandMessage') { await this.processEditorContextCommandMessage(messageData) return @@ -276,6 +252,19 @@ export class Connector extends BaseConnector { this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action) return } + + if (messageData.type === 'asyncEventProgressMessage') { + const enableStopAction = true + this.onAsyncEventProgress( + messageData.tabID, + messageData.inProgress, + messageData.message ?? undefined, + messageData.messageId ?? undefined, + enableStopAction, + false + ) + return + } // For other message types, call the base class handleMessageReceive await this.baseHandleMessageReceive(messageData) } diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index f7ed50dbd6f..1b4f7930143 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -287,12 +287,13 @@ export const createMynahUI = ( inProgress: boolean, message: string | undefined, messageId: string | undefined = undefined, - enableStopAction: boolean = false + enableStopAction: boolean = false, + isPromptInputDisabled: boolean = true ) => { if (inProgress) { mynahUI.updateStore(tabID, { loadingChat: true, - promptInputDisabledState: true, + promptInputDisabledState: isPromptInputDisabled, cancelButtonWhenLoading: enableStopAction, }) diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index 17b26586772..ae3d6e8ee26 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -22,14 +22,12 @@ export class ChatSession { * _readFiles = list of files read from the project to gather context before generating response. * _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user * _context = Additional context to be passed to the LLM for generating the response - * _messageIdToUpdate = messageId of a chat message to be updated, used for reducing consecutive tool messages */ private _readFiles: string[] = [] private _toolUse: ToolUse | undefined private _showDiffOnFileWrite: boolean = false private _context: PromptMessage['context'] private _pairProgrammingModeOn: boolean = true - private _messageIdToUpdate: string | undefined contexts: Map = new Map() // TODO: doesn't handle the edge case when two files share the same relativePath string but from different root @@ -63,14 +61,6 @@ export class ChatSession { this._context = context } - public get messageIdToUpdate(): string | undefined { - return this._messageIdToUpdate - } - - public setMessageIdToUpdate(messageId: string | undefined) { - this._messageIdToUpdate = messageId - } - public tokenSource!: vscode.CancellationTokenSource constructor() { diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 8df4cbcf903..bc16c06d829 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -373,6 +373,7 @@ export class ChatController { private async processStopResponseMessage(message: StopResponseMessage) { const session = this.sessionStorage.getSession(message.tabID) session.tokenSource.cancel() + this.messenger.sendEmptyMessage(message.tabID, '', undefined) this.chatHistoryStorage.getTabHistory(message.tabID).clearRecentHistory() } @@ -716,6 +717,7 @@ export class ChatController { type: 'chat_message', context, }) + this.messenger.sendAsyncEventProgress(tabID, true, '') const session = this.sessionStorage.getSession(tabID) const toolUse = session.toolUse if (!toolUse || !toolUse.input) { @@ -734,16 +736,9 @@ export class ChatController { try { await ToolUtils.validate(tool) - const chatStream = new ChatStream( - this.messenger, - tabID, - triggerID, - // Pass in a different toolUseId so that the output does not overwrite - // any previous messages - { ...toolUse, toolUseId: `${toolUse.toolUseId}-output` }, - { requiresAcceptance: false }, - undefined - ) + const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, { + requiresAcceptance: false, + }) const output = await ToolUtils.invoke(tool, chatStream) if (output.output.content.length > maxToolOutputCharacterLength) { throw Error( @@ -1185,6 +1180,8 @@ export class ChatController { context, }) + this.messenger.sendAsyncEventProgress(message.tabID, true, '') + // Save the context for the agentic loop session.setContext(message.context) diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 1efab5c2d75..356c7d20575 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -14,7 +14,6 @@ import { OpenSettingsMessage, QuickActionMessage, ShowCustomFormMessage, - ToolMessage, } from '../../../view/connector/connector' import { EditorContextCommandType } from '../../../commands/registerCommands' import { ChatResponseStream as qdevChatResponseStream } from '@amzn/amazon-q-developer-streaming-client' @@ -49,6 +48,7 @@ import { extractErrorInfo } from '../../../../shared/utilities/messageUtil' import { noWriteTools, tools } from '../../../constants' import { Change } from 'diff' import { FsWriteParams } from '../../../tools/fsWrite' +import { AsyncEventProgressMessage } from '../../../../amazonq/commons/connector/connectorMessages' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -238,36 +238,10 @@ export class Messenger { session.setShowDiffOnFileWrite(true) changeList = await tool.tool.getDiffChanges() } - if ( - tool.type === ToolType.FsWrite || - tool.type === ToolType.ExecuteBash || - eventCounts.has('assistantResponseEvent') - ) { - // FsWrite and ExecuteBash should never replace older messages - // If the current stream also has assistantResponseEvent then reset this as well. - session.setMessageIdToUpdate(undefined) - } const validation = ToolUtils.requiresAcceptance(tool) - - const chatStream = new ChatStream( - this, - tabID, - triggerID, - toolUse, - validation, - session.messageIdToUpdate, - changeList - ) + const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation, changeList) await ToolUtils.queueDescription(tool, chatStream) - if ( - session.messageIdToUpdate === undefined && - (tool.type === ToolType.FsRead || tool.type === ToolType.ListDirectory) - ) { - // Store the first messageId in a chain of tool uses - session.setMessageIdToUpdate(toolUse.toolUseId) - } - if (!validation.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( @@ -469,33 +443,12 @@ export class Messenger { ) } - public sendInitialToolMessage(tabID: string, triggerID: string, toolUseId: string | undefined) { - this.dispatcher.sendChatMessage( - new ChatMessage( - { - message: '', - messageType: 'answer', - followUps: undefined, - followUpsHeader: undefined, - relatedSuggestions: undefined, - triggerID, - messageID: toolUseId ?? 'toolUse', - userIntent: undefined, - codeBlockLanguage: undefined, - contextList: undefined, - }, - tabID - ) - ) - } - public sendPartialToolLog( message: string, tabID: string, triggerID: string, toolUse: ToolUse | undefined, validation: CommandValidation, - messageIdToUpdate: string | undefined, changeList?: Change[] ) { const buttons: ChatItemButton[] = [] @@ -550,8 +503,8 @@ export class Messenger { }) } - this.dispatcher.sendToolMessage( - new ToolMessage( + this.dispatcher.sendChatMessage( + new ChatMessage( { message: message, messageType: 'answer-part', @@ -559,7 +512,7 @@ export class Messenger { followUpsHeader: undefined, relatedSuggestions: undefined, triggerID, - messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '', + messageID: toolUse?.toolUseId ?? '', userIntent: undefined, codeBlockLanguage: undefined, contextList: undefined, @@ -738,4 +691,32 @@ export class Messenger { new ShowCustomFormMessage(tabID, formItems, buttons, title, description) ) } + + public sendAsyncEventProgress(tabID: string, inProgress: boolean, message: string | undefined) { + this.dispatcher.sendAsyncEventProgress(new AsyncEventProgressMessage(tabID, 'CWChat', inProgress, message)) + } + + public sendEmptyMessage( + tabID: string, + triggerId: string, + mergedRelevantDocuments: DocumentReference[] | undefined + ) { + this.dispatcher.sendChatMessage( + new ChatMessage( + { + message: '', + messageType: 'answer', + followUps: undefined, + followUpsHeader: undefined, + relatedSuggestions: undefined, + triggerID: triggerId, + messageID: '', + userIntent: undefined, + codeBlockLanguage: undefined, + contextList: undefined, + }, + tabID + ) + ) + } } diff --git a/packages/core/src/codewhispererChat/storages/chatHistory.ts b/packages/core/src/codewhispererChat/storages/chatHistory.ts index 1029e2eeec5..329b0e92815 100644 --- a/packages/core/src/codewhispererChat/storages/chatHistory.ts +++ b/packages/core/src/codewhispererChat/storages/chatHistory.ts @@ -83,6 +83,10 @@ export class ChatHistoryManager { if (newMessage !== undefined && this.lastUserMessage !== undefined) { this.logger.warn('last Message should not be defined when pushing an assistant message') } + // check if last message in histroy is assistant message and now replace it in that case + if (this.history.length > 0 && this.history.at(-1)?.assistantResponseMessage) { + this.history.pop() + } this.history.push(newMessage) } @@ -101,6 +105,18 @@ export class ChatHistoryManager { } private trimConversationHistory(): void { + // make sure the UseInputMessage is the first stored message + if (this.history.length === 1 && this.history[0].assistantResponseMessage) { + this.history = [] + } + + if ( + this.history.at(-1)?.assistantResponseMessage?.content === '' && + this.history.at(-1)?.assistantResponseMessage?.toolUses === undefined + ) { + this.clearRecentHistory() + } + if (this.history.length <= MaxConversationHistoryLength) { return } diff --git a/packages/core/src/codewhispererChat/tools/chatStream.ts b/packages/core/src/codewhispererChat/tools/chatStream.ts index 4894bf1c498..40177813593 100644 --- a/packages/core/src/codewhispererChat/tools/chatStream.ts +++ b/packages/core/src/codewhispererChat/tools/chatStream.ts @@ -23,17 +23,12 @@ export class ChatStream extends Writable { private readonly triggerID: string, private readonly toolUse: ToolUse | undefined, private readonly validation: CommandValidation, - private readonly messageIdToUpdate: string | undefined, private readonly changeList?: Change[], private readonly logger = getLogger('chatStream') ) { super() this.logger.debug(`ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}`) - if (!messageIdToUpdate) { - // If messageIdToUpdate is undefined, we need to first create an empty message - // with messageId so it can be updated later - this.messenger.sendInitialToolMessage(tabID, triggerID, toolUse?.toolUseId) - } + this.messenger.sendInitalStream(tabID, triggerID, undefined) } override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void { @@ -46,7 +41,6 @@ export class ChatStream extends Writable { this.triggerID, this.toolUse, this.validation, - this.messageIdToUpdate, this.changeList ) callback() diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index d37f55cd453..30a5850053a 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -19,6 +19,7 @@ import { Status, } from '@aws/mynah-ui' import { DocumentReference } from '../../controllers/chat/model' +import { AsyncEventProgressMessage } from '../../../amazonq/commons/connector/connectorMessages' class UiMessage { readonly time: number = Date.now() @@ -286,10 +287,6 @@ export class ChatMessage extends UiMessage { } } -export class ToolMessage extends ChatMessage { - override type = 'toolMessage' -} - export interface FollowUp { readonly type: string readonly pillText: string @@ -344,10 +341,6 @@ export class AppToWebViewMessageDispatcher { this.appsToWebViewMessagePublisher.publish(message) } - public sendToolMessage(message: ToolMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - public sendEditorContextCommandMessage(message: EditorContextCommandMessage) { this.appsToWebViewMessagePublisher.publish(message) } @@ -375,4 +368,8 @@ export class AppToWebViewMessageDispatcher { public sendCustomFormActionMessage(message: CustomFormActionMessage) { this.appsToWebViewMessagePublisher.publish(message) } + + public sendAsyncEventProgress(message: AsyncEventProgressMessage) { + this.appsToWebViewMessagePublisher.publish(message) + } } From 4f552cf2763177811b8fc88eda997193311f4d17 Mon Sep 17 00:00:00 2001 From: laileni Date: Sat, 5 Apr 2025 15:38:30 -0700 Subject: [PATCH 09/19] Adding conditional check to restrict pair programming to chat only --- .../src/amazonq/webview/ui/tabs/generator.ts | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts index 6f887f5817b..15f00e9baeb 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts @@ -72,23 +72,26 @@ export class TabDataGenerator { }, ] : [], - promptInputOptions: [ - { - type: 'toggle', - id: 'prompt-type', - value: 'ask', - options: [ - { - value: 'pair-programming-on', - icon: 'code-block', // TODO: correct icons - }, - { - value: 'pair-programming-off', - icon: 'chat', // TODO: correct icons - }, - ], - }, - ], + promptInputOptions: + tabType === 'cwc' + ? [ + { + type: 'toggle', + id: 'prompt-type', + value: 'ask', + options: [ + { + value: 'pair-programming-on', + icon: 'code-block', // TODO: correct icons + }, + { + value: 'pair-programming-off', + icon: 'chat', // TODO: correct icons + }, + ], + }, + ] + : [], } return tabData } From 1411d1f286d4725e2543abdc572e0a6d2ff6f0e5 Mon Sep 17 00:00:00 2001 From: laileni Date: Sat, 5 Apr 2025 15:41:48 -0700 Subject: [PATCH 10/19] Support multi file code diff POC --- .../controllers/chat/controller.ts | 3 ++- .../controllers/chat/messenger/messenger.ts | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index bc16c06d829..49ecdd9ff2d 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -819,9 +819,10 @@ export class ChatController { await this.handleCreatePrompt(message) break case 'accept-code-diff': + await this.closeDiffView() + break case 'confirm-tool-use': case 'generic-tool-execution': - await this.closeDiffView() await this.processToolUseMessage(message) break case 'reject-code-diff': diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 356c7d20575..af9afeb9eb5 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -242,14 +242,11 @@ export class Messenger { const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation, changeList) await ToolUtils.queueDescription(tool, chatStream) - if (!validation.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: 'generic-tool-execution', - }) - ) - } + this.dispatcher.sendCustomFormActionMessage( + new CustomFormActionMessage(tabID, { + id: 'generic-tool-execution', + }) + ) } else { // TODO: Handle the error } From 166041fdfb3012021eac0933dce947dd14e13e8c Mon Sep 17 00:00:00 2001 From: Tai Lai Date: Sat, 5 Apr 2025 16:05:43 -0700 Subject: [PATCH 11/19] feat(chat): support multi file write with reject and open diff (#23) POC --- .../webview/ui/apps/cwChatConnector.ts | 24 +++---- .../codewhispererChat/clients/chat/v0/chat.ts | 15 +++++ .../controllers/chat/controller.ts | 66 ++++++++----------- .../controllers/chat/messenger/messenger.ts | 2 +- .../src/codewhispererChat/tools/fsWrite.ts | 28 +++++--- .../src/codewhispererChat/tools/toolUtils.ts | 2 +- 6 files changed, 73 insertions(+), 64 deletions(-) diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 113ca8dd23c..111350b8e6a 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -314,7 +314,7 @@ export class Connector extends BaseConnector { if ( !this.onChatAnswerUpdated || - !['accept-code-diff', 'reject-code-diff', 'confirm-tool-use'].includes(action.id) + !(['accept-code-diff', 'confirm-tool-use'].includes(action.id) || action.id.startsWith('reject-code-diff')) ) { return } @@ -341,17 +341,6 @@ export class Connector extends BaseConnector { answer.body = ' ' } break - case 'reject-code-diff': - if (answer.header) { - answer.header.status = { - icon: 'cancel' as MynahIconsType, - text: 'Rejected', - status: 'error', - } - answer.header.buttons = [] - answer.body = ' ' - } - break case 'confirm-tool-use': answer.buttons = [ { @@ -367,6 +356,17 @@ export class Connector extends BaseConnector { default: break } + if (action.id.startsWith('reject-code-diff')) { + if (answer.header) { + answer.header.status = { + icon: 'cancel' as MynahIconsType, + text: 'Rejected', + status: 'error', + } + answer.header.buttons = [] + answer.body = ' ' + } + } if (currentChatItem && answer.messageId) { const updatedItem = { ...currentChatItem, ...answer } diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index ae3d6e8ee26..d7616b214b2 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -28,6 +28,7 @@ export class ChatSession { private _showDiffOnFileWrite: boolean = false private _context: PromptMessage['context'] private _pairProgrammingModeOn: boolean = true + private _fsWriteBackups: Map = new Map() contexts: Map = new Map() // 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 { this._toolUse = toolUse } + public get fsWriteBackups() { + return this._fsWriteBackups + } + + public setFsWriteBackups( + toolUseId: string | undefined, + content: { filePath: string; content: string; isNew: boolean } + ) { + if (!toolUseId) { + return + } + this._fsWriteBackups.set(toolUseId, content) + } + public get context(): PromptMessage['context'] { return this._context } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 49ecdd9ff2d..a00ec8df99f 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -92,7 +92,7 @@ import { maxToolOutputCharacterLength, OutputKind } from '../../tools/toolShared import { ToolUtils, Tool, ToolType } from '../../tools/toolUtils' import { ChatStream } from '../../tools/chatStream' import { ChatHistoryStorage } from '../../storages/chatHistoryStorage' -import { FsWrite, FsWriteParams } from '../../tools/fsWrite' +import { FsWriteParams } from '../../tools/fsWrite' import { tempDirPath } from '../../../shared/filesystemUtilities' export interface ChatControllerMessagePublishers { @@ -739,6 +739,10 @@ export class ChatController { const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, { requiresAcceptance: false, }) + if (tool.type === ToolType.FsWrite) { + const backup = await tool.tool.getOldContent() + session.setFsWriteBackups(toolUse.toolUseId, backup) + } const output = await ToolUtils.invoke(tool, chatStream) if (output.output.content.length > maxToolOutputCharacterLength) { throw Error( @@ -818,22 +822,29 @@ export class ChatController { case 'submit-create-prompt': await this.handleCreatePrompt(message) break - case 'accept-code-diff': - await this.closeDiffView() - break case 'confirm-tool-use': case 'generic-tool-execution': await this.processToolUseMessage(message) break - case 'reject-code-diff': - await this.closeDiffView() - break case 'tool-unavailable': await this.processUnavailableToolUseMessage(message) break default: getLogger().warn(`Unhandled action: ${message.action.id}`) } + + if (message.action.id.startsWith('reject-code-diff')) { + // revert the changes + const toolUseId = message.action.id.split('/')[1] + const backups = this.sessionStorage.getSession(message.tabID!).fsWriteBackups + const { filePath, content, isNew } = backups.get(toolUseId) ?? {} + if (filePath && isNew) { + await fs.delete(filePath) + } else if (filePath && content !== undefined) { + await fs.writeFile(filePath, content) + } + await this.closeDiffView() + } } private async processContextSelected(message: ContextSelectedMessage) { @@ -855,8 +866,10 @@ export class ChatController { private async processFileClickMessage(message: FileClick) { const session = this.sessionStorage.getSession(message.tabID) + const toolUseId = message.messageId + const backup = session.fsWriteBackups.get(toolUseId) // Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly. - if (session.showDiffOnFileWrite) { + if (session.showDiffOnFileWrite && backup?.filePath) { try { // Create a temporary file path to show the diff view const pathToArchiveDir = path.join(tempDirPath, 'q-chat') @@ -867,40 +880,13 @@ export class ChatController { await fs.mkdir(pathToArchiveDir) const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts') await fs.mkdir(resultArtifactsDir) - const tempFilePath = path.join( - resultArtifactsDir, - `temp-${path.basename((session.toolUse?.input as unknown as FsWriteParams).path)}` - ) + const tempFilePath = path.join(resultArtifactsDir, `temp-${path.basename(backup.filePath)}`) - // If we have existing filePath copy file content from existing file to temporary file. - const filePath = (session.toolUse?.input as any).path ?? message.filePath - const fileExists = await fs.existsFile(filePath) - if (fileExists) { - const fileContent = await fs.readFileText(filePath) - await fs.writeFile(tempFilePath, fileContent) - } + await fs.writeFile(tempFilePath, backup.content) - // Create a deep clone of the toolUse object and pass this toolUse to FsWrite tool execution to get the modified temporary file. - const clonedToolUse = structuredClone(session.toolUse) - if (!clonedToolUse) { - return - } - const input = clonedToolUse.input as unknown as FsWriteParams - input.path = tempFilePath - - const fsWrite = new FsWrite(input) - await fsWrite.invoke() - - // Check if fileExists=false, If yes, return instead of showing broken diff experience. - if (!tempFilePath) { - void vscode.window.showInformationMessage( - 'Generated code changes have been reviewed and processed.' - ) - return - } - const leftUri = fileExists ? vscode.Uri.file(filePath) : vscode.Uri.from({ scheme: 'untitled' }) - const rightUri = vscode.Uri.file(tempFilePath ?? filePath) - const fileName = path.basename(filePath) + const leftUri = vscode.Uri.file(tempFilePath) + const rightUri = vscode.Uri.file(backup.filePath) + const fileName = path.basename(backup.filePath) await vscode.commands.executeCommand( 'vscode.diff', leftUri, diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index af9afeb9eb5..dcc20d7e6c4 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -489,7 +489,7 @@ export class Messenger { } // Buttons buttons.push({ - id: 'reject-code-diff', + id: `reject-code-diff/${toolUse.toolUseId}`, status: 'clear', icon: 'cancel' as MynahIconsType, }) diff --git a/packages/core/src/codewhispererChat/tools/fsWrite.ts b/packages/core/src/codewhispererChat/tools/fsWrite.ts index 847d60bc331..54f00514289 100644 --- a/packages/core/src/codewhispererChat/tools/fsWrite.ts +++ b/packages/core/src/codewhispererChat/tools/fsWrite.ts @@ -83,31 +83,39 @@ export class FsWrite { } public async getDiffChanges(): Promise { - const sanitizedPath = sanitizePath(this.params.path) + const { filePath, content: oldContent } = await this.getOldContent() let newContent - let oldContent - try { - oldContent = await fs.readFileText(sanitizedPath) - } catch (err) { - oldContent = '' - } switch (this.params.command) { case 'create': newContent = this.getCreateCommandText(this.params) break case 'strReplace': - newContent = await this.getStrReplaceContent(this.params, sanitizedPath) + newContent = await this.getStrReplaceContent(this.params, filePath) break case 'insert': - newContent = await this.getInsertContent(this.params, sanitizedPath) + newContent = await this.getInsertContent(this.params, filePath) break case 'append': - newContent = await this.getAppendContent(this.params, sanitizedPath) + newContent = await this.getAppendContent(this.params, filePath) break } return diffLines(oldContent, newContent) } + public async getOldContent(): Promise<{ filePath: string; content: string; isNew: boolean }> { + const sanitizedPath = sanitizePath(this.params.path) + let oldContent + let isNew + try { + oldContent = await fs.readFileText(sanitizedPath) + isNew = false + } catch (err) { + oldContent = '' + isNew = true + } + return { filePath: sanitizedPath, content: oldContent, isNew } + } + public async validate(): Promise { switch (this.params.command) { case 'create': diff --git a/packages/core/src/codewhispererChat/tools/toolUtils.ts b/packages/core/src/codewhispererChat/tools/toolUtils.ts index 93d51244ebf..1549f89b0dc 100644 --- a/packages/core/src/codewhispererChat/tools/toolUtils.ts +++ b/packages/core/src/codewhispererChat/tools/toolUtils.ts @@ -42,7 +42,7 @@ export class ToolUtils { case ToolType.FsRead: return { requiresAcceptance: false } case ToolType.FsWrite: - return { requiresAcceptance: true } + return { requiresAcceptance: false } case ToolType.ExecuteBash: return tool.tool.requiresAcceptance() case ToolType.ListDirectory: From 031250b5cb482135eb6975173c9401fbe8e39eb2 Mon Sep 17 00:00:00 2001 From: laileni Date: Sat, 5 Apr 2025 16:07:02 -0700 Subject: [PATCH 12/19] nit: Fixing test cases --- .../core/src/test/codewhispererChat/tools/toolShared.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts index b6dec845e3e..4b7f9365db4 100644 --- a/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts +++ b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts @@ -69,7 +69,7 @@ describe('ToolUtils', function () { it('returns true for FsWrite tool', function () { const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite } - assert.strictEqual(ToolUtils.requiresAcceptance(tool).requiresAcceptance, true) + assert.strictEqual(ToolUtils.requiresAcceptance(tool).requiresAcceptance, false) }) it('delegates to the tool for ExecuteBash', function () { From 77e59ca46ba77ae1b091b8656014f91fa25aadf5 Mon Sep 17 00:00:00 2001 From: Na Yue Date: Sat, 5 Apr 2025 15:03:51 -0700 Subject: [PATCH 13/19] Merging Na Changes PR:6948 into mega --- .../webview/ui/apps/cwChatConnector.ts | 31 +++++---- .../controllers/chat/controller.ts | 17 ++++- .../controllers/chat/messenger/messenger.ts | 65 ++++++++++++++----- .../codewhispererChat/tools/executeBash.ts | 1 - 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 111350b8e6a..ed159f6e44b 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -314,7 +314,10 @@ export class Connector extends BaseConnector { if ( !this.onChatAnswerUpdated || - !(['accept-code-diff', 'confirm-tool-use'].includes(action.id) || action.id.startsWith('reject-code-diff')) + !( + ['accept-code-diff', 'run-shell-command', 'reject-shell-command'].includes(action.id) || + action.id.startsWith('reject-code-diff') + ) ) { return } @@ -341,17 +344,23 @@ export class Connector extends BaseConnector { answer.body = ' ' } break - case 'confirm-tool-use': - answer.buttons = [ - { - keepCardAfterClick: true, - text: 'Confirmed', - id: 'confirmed-tool-use', + case 'run-shell-command': + if (answer.header) { + answer.header.status = { + icon: 'ok' as MynahIconsType, + text: 'Accepted', status: 'success', - position: 'outside', - disabled: true, - }, - ] + } + } + break + case 'reject-shell-command': + if (answer.header) { + answer.header.status = { + icon: 'cancel' as MynahIconsType, + text: 'Rejected', + status: 'error', + } + } break default: break diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index a00ec8df99f..c06bfc1db98 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -817,15 +817,30 @@ export class ChatController { } } + private async rejectShellCommand(message: CustomFormActionMessage) { + const triggerId = randomUUID() + this.triggerEventsStorage.addTriggerEvent({ + id: triggerId, + tabID: message.tabID, + message: undefined, + type: 'chat_message', + context: undefined, + }) + await this.generateStaticTextResponse('reject-shell-command', triggerId) + } + private async processCustomFormAction(message: CustomFormActionMessage) { switch (message.action.id) { case 'submit-create-prompt': await this.handleCreatePrompt(message) break - case 'confirm-tool-use': + case 'run-shell-command': case 'generic-tool-execution': await this.processToolUseMessage(message) break + case 'reject-shell-command': + await this.rejectShellCommand(message) + break case 'tool-unavailable': await this.processUnavailableToolUseMessage(message) break diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index dcc20d7e6c4..9fe06263a28 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -50,7 +50,12 @@ import { Change } from 'diff' import { FsWriteParams } from '../../../tools/fsWrite' import { AsyncEventProgressMessage } from '../../../../amazonq/commons/connector/connectorMessages' -export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' +export type StaticTextResponseType = + | 'quick-action-help' + | 'onboarding-help' + | 'transform' + | 'help' + | 'reject-shell-command' export type MessengerResponseType = { $metadata: { requestId?: string; httpStatusCode?: number } @@ -242,10 +247,11 @@ export class Messenger { const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation, changeList) await ToolUtils.queueDescription(tool, chatStream) + const actionId = + tool.type === ToolType.ExecuteBash ? 'run-shell-command' : 'generic-tool-execution' + this.dispatcher.sendCustomFormActionMessage( - new CustomFormActionMessage(tabID, { - id: 'generic-tool-execution', - }) + new CustomFormActionMessage(tabID, { id: actionId }) ) } else { // TODO: Handle the error @@ -450,12 +456,28 @@ export class Messenger { ) { const buttons: ChatItemButton[] = [] let fileList: ChatItemContent['fileList'] = undefined - if (validation.requiresAcceptance && toolUse?.name === ToolType.ExecuteBash) { - buttons.push({ - id: 'confirm-tool-use', - text: 'Confirm', - status: 'info', - }) + let shellCommandHeader = undefined + if (toolUse?.name === ToolType.ExecuteBash && message.startsWith('```shell')) { + if (validation.requiresAcceptance) { + buttons.push({ + id: 'run-shell-command', + text: 'Run', + status: 'main', + icon: 'play' as MynahIconsType, + }) + buttons.push({ + id: 'reject-shell-command', + text: 'Reject', + status: 'clear', + icon: 'cancel' as MynahIconsType, + }) + } + + shellCommandHeader = { + icon: 'code-block' as MynahIconsType, + body: 'shell', + buttons: buttons, + } if (validation.warning) { message = validation.warning + message @@ -514,16 +536,23 @@ export class Messenger { codeBlockLanguage: undefined, contextList: undefined, canBeVoted: false, - buttons: toolUse?.name === ToolType.FsWrite ? undefined : buttons, - fullWidth: toolUse?.name === ToolType.FsWrite, - padding: !(toolUse?.name === ToolType.FsWrite), + buttons: + toolUse?.name === ToolType.FsWrite || toolUse?.name === ToolType.ExecuteBash + ? undefined + : buttons, + fullWidth: toolUse?.name === ToolType.FsWrite || toolUse?.name === ToolType.ExecuteBash, + padding: !(toolUse?.name === ToolType.FsWrite || toolUse?.name === ToolType.ExecuteBash), header: toolUse?.name === ToolType.FsWrite ? { icon: 'code-block' as MynahIconsType, buttons: buttons, fileList: fileList } - : undefined, + : toolUse?.name === ToolType.ExecuteBash + ? shellCommandHeader + : undefined, codeBlockActions: - // eslint-disable-next-line unicorn/no-null, prettier/prettier - toolUse?.name === ToolType.FsWrite ? { 'insert-to-cursor': null, copy: null } : undefined, + toolUse?.name === ToolType.FsWrite || toolUse?.name === ToolType.ExecuteBash + ? // eslint-disable-next-line unicorn/no-null + { 'insert-to-cursor': null, copy: null } + : undefined, }, tabID ) @@ -575,6 +604,10 @@ export class Messenger { ] followUpsHeader = 'Try Examples:' break + case 'reject-shell-command': + // need to update the string later + message = 'The shell command execution rejected. Abort.' + break } this.dispatcher.sendChatMessage( diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index 73da1126a88..84308390673 100644 --- a/packages/core/src/codewhispererChat/tools/executeBash.ts +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -327,7 +327,6 @@ export class ExecuteBash { } public queueDescription(updates: Writable): void { - updates.write(`I will run the following shell command:\n`) updates.write('```shell\n' + this.command + '\n```') updates.end() } From 30b13c6228c1191911ffca999140567e9c1a4535 Mon Sep 17 00:00:00 2001 From: Tai Lai Date: Sat, 5 Apr 2025 17:46:56 -0700 Subject: [PATCH 14/19] update css --- .../core/resources/css/amazonq-webview.css | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/packages/core/resources/css/amazonq-webview.css b/packages/core/resources/css/amazonq-webview.css index 97d8549bf1b..ec1d95f255a 100644 --- a/packages/core/resources/css/amazonq-webview.css +++ b/packages/core/resources/css/amazonq-webview.css @@ -19,3 +19,107 @@ body.vscode-high-contrast:not(.vscode-high-contrast-light) { body .mynah-card-body h1 { --mynah-line-height: 1.5rem; } + +div.mynah-card.padding-large { + padding: var(--mynah-sizing-4) var(--mynah-sizing-3); +} + +body { + --mynah-font-family: var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + 'Amazon Ember', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: var(--vscode-font-size, 12px); + font-family: var(--mynah-font-family, 'system-ui'); + --mynah-max-width: 2560px; + --mynah-sizing-base: 0.2rem; + --mynah-sizing-half: calc(var(--mynah-sizing-base) / 2); + --mynah-sizing-1: var(--mynah-sizing-base); + --mynah-sizing-2: calc(var(--mynah-sizing-base) * 2); + --mynah-sizing-3: calc(var(--mynah-sizing-base) * 3); + --mynah-sizing-4: calc(var(--mynah-sizing-base) * 4); + --mynah-sizing-5: calc(var(--mynah-sizing-base) * 5); + --mynah-sizing-6: calc(var(--mynah-sizing-base) * 6); + --mynah-sizing-7: calc(var(--mynah-sizing-base) * 7); + --mynah-sizing-8: calc(var(--mynah-sizing-base) * 8); + --mynah-sizing-9: calc(var(--mynah-sizing-base) * 9); + --mynah-sizing-10: calc(var(--mynah-sizing-base) * 10); + --mynah-sizing-11: calc(var(--mynah-sizing-base) * 11); + --mynah-sizing-12: calc(var(--mynah-sizing-base) * 12); + --mynah-sizing-13: calc(var(--mynah-sizing-base) * 13); + --mynah-sizing-14: calc(var(--mynah-sizing-base) * 14); + --mynah-sizing-15: calc(var(--mynah-sizing-base) * 15); + --mynah-sizing-16: calc(var(--mynah-sizing-base) * 16); + --mynah-sizing-17: calc(var(--mynah-sizing-base) * 17); + --mynah-sizing-18: calc(var(--mynah-sizing-base) * 18); + --mynah-chat-wrapper-spacing: var(--mynah-sizing-2); + --mynah-button-border-width: 1px; + --mynah-border-width: 1px; + + --mynah-color-text-default: var(--vscode-foreground); + --mynah-color-text-strong: var(--vscode-input-foreground); + --mynah-color-text-weak: var(--vscode-disabledForeground); + --mynah-color-text-link: var(--vscode-textLink-foreground); + --mynah-color-text-input: var(--vscode-input-foreground); + + --mynah-color-bg: var(--vscode-sideBar-background); + --mynah-color-tab-active: var(--vscode-tab-activeBackground, var(--vscode-editor-background, var(--mynah-card-bg))); + --mynah-color-light: rgba(0, 0, 0, 0.05); + + --mynah-color-border-default: var(--vscode-panel-border, var(--vscode-tab-border, rgba(0, 0, 0, 0.1))); + + --mynah-color-highlight: rgba(255, 179, 0, 0.25); + --mynah-color-highlight-text: #886411; + + --mynah-color-toggle: var(--vscode-sideBar-background); + --mynah-color-toggle-reverse: rgba(0, 0, 0, 0.5); + + --mynah-color-syntax-bg: var(--vscode-terminal-dropBackground); + --mynah-color-syntax-variable: var(--vscode-debugTokenExpression-name); + --mynah-color-syntax-function: var(--vscode-gitDecoration-modifiedResourceForeground); + --mynah-color-syntax-operator: var(--vscode-debugTokenExpression-name); + --mynah-color-syntax-attr-value: var(--vscode-debugIcon-stepBackForeground); + --mynah-color-syntax-attr: var(--vscode-debugTokenExpression-string); + --mynah-color-syntax-property: var(--vscode-terminal-ansiCyan); + --mynah-color-syntax-comment: var(--vscode-debugConsole-sourceForeground); + --mynah-color-syntax-code: var(--vscode-terminal-ansiBlue); + --mynah-syntax-code-font-family: var(--vscode-editor-font-family); + --mynah-syntax-code-font-size: var(--vscode-editor-font-size, var(--mynah-font-size-medium)); + --mynah-syntax-code-block-max-height: calc(25 * var(--mynah-syntax-code-line-height)); + + --mynah-color-status-info: var(--vscode-editorInfo-foreground); + --mynah-color-status-success: var(--vscode-terminal-ansiGreen); + --mynah-color-status-warning: var(--vscode-editorWarning-foreground); + --mynah-color-status-error: var(--vscode-editorError-foreground); + + --mynah-color-button: var(--vscode-button-background); + --mynah-color-button-reverse: var(--vscode-button-foreground); + + --mynah-color-alternate: var(--vscode-button-secondaryBackground); + --mynah-color-alternate-reverse: var(--vscode-button-secondaryForeground); + + --mynah-card-bg: var(--vscode-editor-background); + + --mynah-shadow-button: none; + --mynah-shadow-card: none; + --mynah-shadow-overlay: 0 0px 15px -5px rgba(0, 0, 0, 0.4); + + --mynah-font-size-xxsmall: 0.75rem; + --mynah-font-size-xsmall: 0.85rem; + --mynah-font-size-small: 0.95rem; + --mynah-font-size-medium: 1rem; + --mynah-font-size-large: 1.125rem; + --mynah-line-height: 1.1rem; + --mynah-syntax-code-line-height: 1.1rem; + + --mynah-card-radius: var(--mynah-sizing-2); + --mynah-input-radius: var(--mynah-sizing-1); + --mynah-card-radius-corner: 0; + --mynah-button-radius: var(--mynah-sizing-1); + + --mynah-bottom-panel-transition: all 850ms cubic-bezier(0.25, 1, 0, 1); + --mynah-very-short-transition: all 600ms cubic-bezier(0.25, 1, 0, 1); + --mynah-very-long-transition: all 1650ms cubic-bezier(0.25, 1, 0, 1); + --mynah-short-transition: all 550ms cubic-bezier(0.85, 0.15, 0, 1); + --mynah-short-transition-rev: all 580ms cubic-bezier(0.35, 1, 0, 1); + --mynah-short-transition-rev-opacity: opacity 750ms cubic-bezier(0.35, 1, 0, 1); + --mynah-text-flow-transition: all 800ms cubic-bezier(0.35, 1.2, 0, 1), transform 800ms cubic-bezier(0.2, 1.05, 0, 1); +} From d549064ae7e1162a6fa84989afad81023c987632 Mon Sep 17 00:00:00 2001 From: Jason Guo <81202082+jguoamz@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:27:19 -0700 Subject: [PATCH 15/19] fix(chat): Add more logs and permission fix (#24) --- .../controllers/chat/controller.ts | 27 +++++++------- .../controllers/chat/messenger/messenger.ts | 8 +++-- .../src/codewhispererChat/tools/chatStream.ts | 36 ++++++++++++------- .../src/codewhispererChat/tools/fsRead.ts | 6 ++-- .../core/src/shared/utilities/messageUtil.ts | 2 +- 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index c06bfc1db98..ce37b6f019c 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -830,6 +830,20 @@ export class ChatController { } private async processCustomFormAction(message: CustomFormActionMessage) { + if (message.action.id.startsWith('reject-code-diff')) { + // revert the file changes + const toolUseId = message.action.id.split('/')[1] + const backups = this.sessionStorage.getSession(message.tabID!).fsWriteBackups + const { filePath, content, isNew } = backups.get(toolUseId) ?? {} + if (filePath && isNew) { + await fs.delete(filePath) + } else if (filePath && content !== undefined) { + await fs.writeFile(filePath, content) + } + await this.closeDiffView() + return + } + switch (message.action.id) { case 'submit-create-prompt': await this.handleCreatePrompt(message) @@ -847,19 +861,6 @@ export class ChatController { default: getLogger().warn(`Unhandled action: ${message.action.id}`) } - - if (message.action.id.startsWith('reject-code-diff')) { - // revert the changes - const toolUseId = message.action.id.split('/')[1] - const backups = this.sessionStorage.getSession(message.tabID!).fsWriteBackups - const { filePath, content, isNew } = backups.get(toolUseId) ?? {} - if (filePath && isNew) { - await fs.delete(filePath) - } else if (filePath && content !== undefined) { - await fs.writeFile(filePath, content) - } - await this.closeDiffView() - } } private async processContextSelected(message: ContextSelectedMessage) { diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 9fe06263a28..fcd1fcc27c2 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -250,9 +250,11 @@ export class Messenger { const actionId = tool.type === ToolType.ExecuteBash ? 'run-shell-command' : 'generic-tool-execution' - this.dispatcher.sendCustomFormActionMessage( - new CustomFormActionMessage(tabID, { id: actionId }) - ) + if (!validation.requiresAcceptance) { + this.dispatcher.sendCustomFormActionMessage( + new CustomFormActionMessage(tabID, { id: actionId }) + ) + } } else { // TODO: Handle the error } diff --git a/packages/core/src/codewhispererChat/tools/chatStream.ts b/packages/core/src/codewhispererChat/tools/chatStream.ts index 40177813593..18e84bccf3f 100644 --- a/packages/core/src/codewhispererChat/tools/chatStream.ts +++ b/packages/core/src/codewhispererChat/tools/chatStream.ts @@ -32,21 +32,31 @@ export class ChatStream extends Writable { } override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void { - const text = chunk.toString() - this.accumulatedLogs += text - this.logger.debug(`ChatStream received chunk: ${text}`) - this.messenger.sendPartialToolLog( - this.accumulatedLogs, - this.tabID, - this.triggerID, - this.toolUse, - this.validation, - this.changeList - ) - callback() + try { + const text = chunk.toString() + this.accumulatedLogs += text + this.logger.debug(`ChatStream received chunk: ${text}`) + this.messenger.sendPartialToolLog( + this.accumulatedLogs, + this.tabID, + this.triggerID, + this.toolUse, + this.validation, + this.changeList + ) + callback() + } catch (error) { + this.logger.error(`Error in ChatStream.write: ${error}`) + callback(error instanceof Error ? error : new Error(String(error))) + } } override _final(callback: (error?: Error | null) => void): void { - callback() + try { + callback() + } catch (error) { + this.logger.error(`Error in ChatStream.final: ${error}`) + callback(error instanceof Error ? error : new Error(String(error))) + } } } diff --git a/packages/core/src/codewhispererChat/tools/fsRead.ts b/packages/core/src/codewhispererChat/tools/fsRead.ts index 07fd64d5293..905056597af 100644 --- a/packages/core/src/codewhispererChat/tools/fsRead.ts +++ b/packages/core/src/codewhispererChat/tools/fsRead.ts @@ -50,15 +50,15 @@ export class FsRead { public queueDescription(updates: Writable): void { const fileName = path.basename(this.fsPath) const fileUri = vscode.Uri.file(this.fsPath) - updates.write(`Reading file: [${fileName}](${fileUri}), `) + updates.write(`Reading file: [${fileName}](${fileUri}) `) const [start, end] = this.readRange ?? [] if (start && end) { - updates.write(`from line ${start} to ${end}`) + updates.write(`L${start} to L${end}`) } else if (start) { if (start > 0) { - updates.write(`from line ${start} to end of file`) + updates.write(`from L${start} to end of file`) } else { updates.write(`${start} line from the end of file to end of file`) } diff --git a/packages/core/src/shared/utilities/messageUtil.ts b/packages/core/src/shared/utilities/messageUtil.ts index f2cefe824de..f1aa690ff26 100644 --- a/packages/core/src/shared/utilities/messageUtil.ts +++ b/packages/core/src/shared/utilities/messageUtil.ts @@ -13,7 +13,7 @@ export interface MessageErrorInfo { } export function extractErrorInfo(error: any): MessageErrorInfo { - let errorMessage = 'Error reading chat stream.' + let errorMessage = 'Error reading chat stream: ' + error.message let statusCode = undefined let requestId = undefined From fe8ccdf533d32f62a634b856322e99aba48d1ada Mon Sep 17 00:00:00 2001 From: Jason Guo <81202082+jguoamz@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:55:01 -0700 Subject: [PATCH 16/19] fix(chat): Fix read ECONNRESET (#25) --- packages/core/src/shared/clients/codewhispererChatClient.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/shared/clients/codewhispererChatClient.ts b/packages/core/src/shared/clients/codewhispererChatClient.ts index 93b7b3bceb4..075f51a8736 100644 --- a/packages/core/src/shared/clients/codewhispererChatClient.ts +++ b/packages/core/src/shared/clients/codewhispererChatClient.ts @@ -17,6 +17,10 @@ export async function createCodeWhispererChatStreamingClient(): Promise 500 + attempt ** 10), }) return streamingClient From 6d2398a1d04845354dc2269c4054be45ae7975bc Mon Sep 17 00:00:00 2001 From: laileni Date: Sat, 5 Apr 2025 19:43:30 -0700 Subject: [PATCH 17/19] Fix: Fixing the lag for file write due to sendInitialStream event --- .../controllers/chat/messenger/messenger.ts | 5 ++--- packages/core/src/shared/clients/codewhispererChatClient.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index fcd1fcc27c2..e4e6619e264 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -259,8 +259,7 @@ export class Messenger { // TODO: Handle the error } } else if (cwChatEvent.toolUseEvent?.stop === undefined && toolUseInput !== '') { - // This is for the case when writing tool is executed. The toolUseEvent is non stop but in toolUseInput is not empty. In this case we need show user the current spinner UI. - this.sendInitalStream(tabID, triggerID, undefined) + // TODO: Add a spinner component if required for fsWrite if not we can remove this if condition check } if ( @@ -527,7 +526,7 @@ export class Messenger { this.dispatcher.sendChatMessage( new ChatMessage( { - message: message, + message: toolUse?.name === ToolType.FsWrite ? ' ' : message, messageType: 'answer-part', followUps: undefined, followUpsHeader: undefined, diff --git a/packages/core/src/shared/clients/codewhispererChatClient.ts b/packages/core/src/shared/clients/codewhispererChatClient.ts index 075f51a8736..2a23608ce8f 100644 --- a/packages/core/src/shared/clients/codewhispererChatClient.ts +++ b/packages/core/src/shared/clients/codewhispererChatClient.ts @@ -20,6 +20,7 @@ export async function createCodeWhispererChatStreamingClient(): Promise 500 + attempt ** 10), }) From 56c481905d3412af205bf6553f06cb39dba45f98 Mon Sep 17 00:00:00 2001 From: Na Yue Date: Mon, 7 Apr 2025 15:08:08 -0700 Subject: [PATCH 18/19] fix(chat): remove shell command header buttons with status after click run or reject (#27) --- .../amazonq/webview/ui/apps/cwChatConnector.ts | 15 +++++++++++++-- .../controllers/chat/messenger/messenger.ts | 5 ++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index ed159f6e44b..1ac2efd8cbb 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -345,12 +345,22 @@ export class Connector extends BaseConnector { } break case 'run-shell-command': - if (answer.header) { - answer.header.status = { + if (Object.keys(answer.header!).length === 0) { + answer.header = { + body: '$ shell', + status: { + icon: 'ok' as MynahIconsType, + text: 'Accepted', + status: 'success', + }, + } + } else { + answer.header!.status = { icon: 'ok' as MynahIconsType, text: 'Accepted', status: 'success', } + answer.header!.buttons = [] } break case 'reject-shell-command': @@ -360,6 +370,7 @@ export class Connector extends BaseConnector { text: 'Rejected', status: 'error', } + answer.header.buttons = [] } break default: diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index e4e6619e264..7560895ac23 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -475,13 +475,12 @@ export class Messenger { } shellCommandHeader = { - icon: 'code-block' as MynahIconsType, - body: 'shell', + body: '$ shell', buttons: buttons, } if (validation.warning) { - message = validation.warning + message + message = validation.warning + message + '\nRun the command to proceed.\n' } } else if (toolUse?.name === ToolType.FsWrite) { const input = toolUse.input as unknown as FsWriteParams From 592a0a36f44b3c45f237fa5fd62c390ddc0339ca Mon Sep 17 00:00:00 2001 From: Randall-Jiang Date: Tue, 8 Apr 2025 13:52:10 -0700 Subject: [PATCH 19/19] change warning category and default present logic for uncategory command --- .../codewhispererChat/tools/executeBash.ts | 87 ++++++++----------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index 84308390673..10f1af02b34 100644 --- a/packages/core/src/codewhispererChat/tools/executeBash.ts +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -12,7 +12,7 @@ import { split } from 'shlex' export enum CommandCategory { ReadOnly, - HighRisk, + Mutate, Destructive, } @@ -47,45 +47,37 @@ export const commandCategories = new Map([ ['netstat', CommandCategory.ReadOnly], ['ss', CommandCategory.ReadOnly], ['dig', CommandCategory.ReadOnly], - ['grep', CommandCategory.ReadOnly], ['wc', CommandCategory.ReadOnly], ['sort', CommandCategory.ReadOnly], ['diff', CommandCategory.ReadOnly], ['head', CommandCategory.ReadOnly], ['tail', CommandCategory.ReadOnly], - // HighRisk commands - ['chmod', CommandCategory.HighRisk], - ['chown', CommandCategory.HighRisk], - ['mv', CommandCategory.HighRisk], - ['cp', CommandCategory.HighRisk], - ['ln', CommandCategory.HighRisk], - ['mount', CommandCategory.HighRisk], - ['umount', CommandCategory.HighRisk], - ['kill', CommandCategory.HighRisk], - ['killall', CommandCategory.HighRisk], - ['pkill', CommandCategory.HighRisk], - ['iptables', CommandCategory.HighRisk], - ['route', CommandCategory.HighRisk], - ['systemctl', CommandCategory.HighRisk], - ['service', CommandCategory.HighRisk], - ['crontab', CommandCategory.HighRisk], - ['at', CommandCategory.HighRisk], - ['tar', CommandCategory.HighRisk], - ['awk', CommandCategory.HighRisk], - ['sed', CommandCategory.HighRisk], - ['wget', CommandCategory.HighRisk], - ['curl', CommandCategory.HighRisk], - ['nc', CommandCategory.HighRisk], - ['ssh', CommandCategory.HighRisk], - ['scp', CommandCategory.HighRisk], - ['ftp', CommandCategory.HighRisk], - ['sftp', CommandCategory.HighRisk], - ['rsync', CommandCategory.HighRisk], - ['chroot', CommandCategory.HighRisk], - ['lsof', CommandCategory.HighRisk], - ['strace', CommandCategory.HighRisk], - ['gdb', CommandCategory.HighRisk], + // Mutable commands + ['chmod', CommandCategory.Mutate], + ['curl', CommandCategory.Mutate], + ['mount', CommandCategory.Mutate], + ['umount', CommandCategory.Mutate], + ['systemctl', CommandCategory.Mutate], + ['service', CommandCategory.Mutate], + ['crontab', CommandCategory.Mutate], + ['at', CommandCategory.Mutate], + ['nc', CommandCategory.Mutate], + ['ssh', CommandCategory.Mutate], + ['scp', CommandCategory.Mutate], + ['ftp', CommandCategory.Mutate], + ['sftp', CommandCategory.Mutate], + ['rsync', CommandCategory.Mutate], + ['chroot', CommandCategory.Mutate], + ['strace', CommandCategory.Mutate], + ['gdb', CommandCategory.Mutate], + ['apt', CommandCategory.Mutate], + ['yum', CommandCategory.Mutate], + ['dnf', CommandCategory.Mutate], + ['pacman', CommandCategory.Mutate], + ['exec', CommandCategory.Mutate], + ['eval', CommandCategory.Mutate], + ['xargs', CommandCategory.Mutate], // Destructive commands ['rm', CommandCategory.Destructive], @@ -104,22 +96,18 @@ export const commandCategories = new Map([ ['insmod', CommandCategory.Destructive], ['rmmod', CommandCategory.Destructive], ['modprobe', CommandCategory.Destructive], - ['apt', CommandCategory.Destructive], - ['yum', CommandCategory.Destructive], - ['dnf', CommandCategory.Destructive], - ['pacman', CommandCategory.Destructive], - ['perl', CommandCategory.Destructive], - ['python', CommandCategory.Destructive], - ['bash', CommandCategory.Destructive], - ['sh', CommandCategory.Destructive], - ['exec', CommandCategory.Destructive], - ['eval', CommandCategory.Destructive], - ['xargs', CommandCategory.Destructive], + ['kill', CommandCategory.Destructive], + ['killall', CommandCategory.Destructive], + ['pkill', CommandCategory.Destructive], + ['iptables', CommandCategory.Destructive], + ['route', CommandCategory.Destructive], + ['chown', CommandCategory.Destructive], ]) export const maxBashToolResponseSize: number = 1024 * 1024 // 1MB export const lineCount: number = 1024 export const destructiveCommandWarningMessage = '⚠️ WARNING: Destructive command detected:\n\n' export const highRiskCommandWarningMessage = '⚠️ WARNING: High risk command detected:\n\n' +export const mutateCommandWarningMessage = 'Mutation command:\n\n' export interface ExecuteBashParams { command: string @@ -197,11 +185,8 @@ export class ExecuteBash { switch (category) { case CommandCategory.Destructive: return { requiresAcceptance: true, warning: destructiveCommandWarningMessage } - case CommandCategory.HighRisk: - return { - requiresAcceptance: true, - warning: highRiskCommandWarningMessage, - } + case CommandCategory.Mutate: + return { requiresAcceptance: true, warning: mutateCommandWarningMessage } case CommandCategory.ReadOnly: if ( cmdArgs.some((arg) => @@ -212,7 +197,7 @@ export class ExecuteBash { } continue default: - return { requiresAcceptance: true, warning: highRiskCommandWarningMessage } + return { requiresAcceptance: true } } } return { requiresAcceptance: false }