diff --git a/package-lock.json b/package-lock.json index b3e0473408d..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", @@ -11957,10 +11957,11 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.27.0.tgz", - "integrity": "sha512-DkwenNLU+BHUYf0ntwkGllfjM+LrQXvCTiV2Eh7zV3HA59+ba5e34zge3sf7F71XukR9ckoKHXrxc0GjvVchbA==", + "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", @@ -22511,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": { @@ -22848,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" @@ -22870,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" @@ -24914,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, @@ -25060,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": { @@ -26752,7 +26778,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.27.0", + "@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", @@ -30277,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", @@ -31844,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/package.json b/packages/core/package.json index 9af22e79ef3..c519b486343 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -524,7 +524,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.27.0", + "@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/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); +} diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index b6dd4f7be0b..1ac2efd8cbb 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) } @@ -325,7 +314,10 @@ export class Connector extends BaseConnector { if ( !this.onChatAnswerUpdated || - !['accept-code-diff', 'reject-code-diff', 'confirm-tool-use'].includes(action.id) + !( + ['accept-code-diff', 'run-shell-command', 'reject-shell-command'].includes(action.id) || + action.id.startsWith('reject-code-diff') + ) ) { return } @@ -352,7 +344,26 @@ export class Connector extends BaseConnector { answer.body = ' ' } break - case 'reject-code-diff': + case 'run-shell-command': + 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': if (answer.header) { answer.header.status = { icon: 'cancel' as MynahIconsType, @@ -360,24 +371,22 @@ export class Connector extends BaseConnector { status: 'error', } answer.header.buttons = [] - answer.body = ' ' } break - case 'confirm-tool-use': - answer.buttons = [ - { - keepCardAfterClick: true, - text: 'Confirmed', - id: 'confirmed-tool-use', - status: 'success', - position: 'outside', - disabled: true, - }, - ] - break 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/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/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 } diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts index 5541ef389c5..c5359a2bfb8 100644 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts @@ -27,18 +27,16 @@ 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' 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 +247,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/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index 17b26586772..d7616b214b2 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -22,14 +22,13 @@ 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 + 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 @@ -55,20 +54,26 @@ export class ChatSession { this._toolUse = toolUse } - public get context(): PromptMessage['context'] { - return this._context + public get fsWriteBackups() { + return this._fsWriteBackups } - public setContext(context: PromptMessage['context']) { - this._context = context + public setFsWriteBackups( + toolUseId: string | undefined, + content: { filePath: string; content: string; isNew: boolean } + ) { + if (!toolUseId) { + return + } + this._fsWriteBackups.set(toolUseId, content) } - public get messageIdToUpdate(): string | undefined { - return this._messageIdToUpdate + public get context(): PromptMessage['context'] { + return this._context } - public setMessageIdToUpdate(messageId: string | undefined) { - this._messageIdToUpdate = messageId + public setContext(context: PromptMessage['context']) { + this._context = context } public tokenSource!: vscode.CancellationTokenSource diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 8df4cbcf903..ce37b6f019c 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 { @@ -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,13 @@ 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, + }) + 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,19 +817,43 @@ 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) { + 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) break - case 'accept-code-diff': - case 'confirm-tool-use': + case 'run-shell-command': case 'generic-tool-execution': - await this.closeDiffView() await this.processToolUseMessage(message) break - case 'reject-code-diff': - await this.closeDiffView() + case 'reject-shell-command': + await this.rejectShellCommand(message) break case 'tool-unavailable': await this.processUnavailableToolUseMessage(message) @@ -859,8 +882,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') @@ -871,40 +896,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)}` - ) - - // 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) - } - - // 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 tempFilePath = path.join(resultArtifactsDir, `temp-${path.basename(backup.filePath)}`) - const fsWrite = new FsWrite(input) - await fsWrite.invoke() + await fs.writeFile(tempFilePath, backup.content) - // 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, @@ -1185,6 +1183,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 3fbe6a03f08..7560895ac23 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -14,13 +14,11 @@ 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' import { ChatResponseStream as cwChatResponseStream, - CodeWhispererStreamingServiceException, SupplementaryWebLink, ToolUse, } from '@amzn/codewhisperer-streaming' @@ -29,7 +27,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' @@ -46,11 +44,18 @@ import { ToolType, ToolUtils } from '../../../tools/toolUtils' import { ChatStream } from '../../../tools/chatStream' import path from 'path' import { CommandValidation } from '../../../tools/executeBash' +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' +export type StaticTextResponseType = + | 'quick-action-help' + | 'onboarding-help' + | 'transform' + | 'help' + | 'reject-shell-command' export type MessengerResponseType = { $metadata: { requestId?: string; httpStatusCode?: number } @@ -238,47 +243,23 @@ 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) - } + const actionId = + tool.type === ToolType.ExecuteBash ? 'run-shell-command' : 'generic-tool-execution' 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', - }) + new CustomFormActionMessage(tabID, { id: actionId }) ) } } else { // TODO: Handle the error } + } else if (cwChatEvent.toolUseEvent?.stop === undefined && toolUseInput !== '') { + // TODO: Add a spinner component if required for fsWrite if not we can remove this if condition check } if ( @@ -340,26 +321,21 @@ export class Messenger { { timeout: 600000, 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 ( @@ -471,46 +447,40 @@ 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[] = [] 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 = { + 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 @@ -541,7 +511,7 @@ export class Messenger { } // Buttons buttons.push({ - id: 'reject-code-diff', + id: `reject-code-diff/${toolUse.toolUseId}`, status: 'clear', icon: 'cancel' as MynahIconsType, }) @@ -552,30 +522,37 @@ export class Messenger { }) } - this.dispatcher.sendToolMessage( - new ToolMessage( + this.dispatcher.sendChatMessage( + new ChatMessage( { - message: message, + message: toolUse?.name === ToolType.FsWrite ? ' ' : message, messageType: 'answer-part', followUps: undefined, followUpsHeader: undefined, relatedSuggestions: undefined, triggerID, - messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '', + messageID: toolUse?.toolUseId ?? '', userIntent: undefined, 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 ) @@ -627,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( @@ -740,4 +721,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..18e84bccf3f 100644 --- a/packages/core/src/codewhispererChat/tools/chatStream.ts +++ b/packages/core/src/codewhispererChat/tools/chatStream.ts @@ -23,36 +23,40 @@ 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 { - 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.messageIdToUpdate, - 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/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index cee84fceca6..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 } @@ -327,8 +312,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() } } 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/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: 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) + } } diff --git a/packages/core/src/shared/clients/codewhispererChatClient.ts b/packages/core/src/shared/clients/codewhispererChatClient.ts index 93b7b3bceb4..2a23608ce8f 100644 --- a/packages/core/src/shared/clients/codewhispererChatClient.ts +++ b/packages/core/src/shared/clients/codewhispererChatClient.ts @@ -17,6 +17,11 @@ export async function createCodeWhispererChatStreamingClient(): Promise 500 + attempt ** 10), }) return streamingClient diff --git a/packages/core/src/shared/utilities/messageUtil.ts b/packages/core/src/shared/utilities/messageUtil.ts new file mode 100644 index 00000000000..f1aa690ff26 --- /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: ' + error.message + let statusCode = undefined + let requestId = undefined + + if (error instanceof CodeWhispererStreamingServiceException) { + errorMessage = error.message + statusCode = getHttpStatusCode(error) ?? 0 + requestId = getRequestId(error) + } + + return { + errorMessage, + statusCode, + requestId, + } +} 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 () {