Repository: aws/aws-toolkit-vscode
Pull request #6950: fix(chat): agentic chat working stop
Author: login: ashishrp-aws
Created: 2025-04-07T18:08:27Z | Merged: 2025-04-07T19:23:28Z

## Problem
Conversation needs stop button to stop the agentic chat loop and conversations topping for new user prompt.

## Solution
- Enables prompt when chat loop is running for new user prompt.
- Fixes chat history for stop/user prompts and followup prompts.
---

- Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time.
- Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the Apache 2.0 license.



Diff:
diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
index 85ef83a9265..67469ac391a 100644
--- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
+++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
@@ -32,7 +32,8 @@ export interface ConnectorProps extends BaseConnectorProps {
         inProgress: boolean,
         message: string,
         messageId: string | undefined,
-        enableStopAction: boolean
+        enableStopAction: boolean,
+        isPromptInputDisabled: boolean
     ) => void
 }
 
@@ -259,7 +260,8 @@ export class Connector extends BaseConnector {
                 messageData.inProgress,
                 messageData.message ?? undefined,
                 messageData.messageId ?? undefined,
-                enableStopAction
+                enableStopAction,
+                false
             )
             return
         }
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/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
index 34c675f8b22..bfa6d57ec79 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()
     }
 
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index 9f24656cebc..101d38fb57f 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -735,4 +735,28 @@ export class Messenger {
     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..71c91d9232a 100644
--- a/packages/core/src/codewhispererChat/storages/chatHistory.ts
+++ b/packages/core/src/codewhispererChat/storages/chatHistory.ts
@@ -80,8 +80,13 @@ export class ChatHistoryManager {
      * Push an assistant message to the history
      */
     public pushAssistantMessage(newMessage: ChatMessage): void {
-        if (newMessage !== undefined && this.lastUserMessage !== undefined) {
-            this.logger.warn('last Message should not be defined when pushing an assistant message')
+        if (newMessage !== undefined && this.lastUserMessage === undefined) {
+            this.logger.warn('first assistant response should always come after user input message')
+            return
+        }
+        // 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 +106,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
         }


Reviews:
user: login: jguoamz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T18:17:33Z
user: login: yueny2020
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T18:22:43Z
user: login: ashishrp-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T18:24:51Z

Review comments:
diff_hunk: @@ -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')
path: packages/core/src/codewhispererChat/storages/chatHistory.ts
user: login: jguoamz
body: can we fix the warning?
created_at: 2025-04-07T18:16:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
+        ) {
path: packages/core/src/codewhispererChat/storages/chatHistory.ts
user: login: jguoamz
body: can you please explain what this do?
created_at: 2025-04-07T18:17:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
+        ) {
path: packages/core/src/codewhispererChat/storages/chatHistory.ts
user: login: ashishrp-aws
body: In some scenarios we are breaking the breaking the assistant response and that empty message is being pushed to chat history as assistant response. so in that specific case, we are correcting the history by clearing up the recent messages. Also making sure that to not toolUse messages with empty content
created_at: 2025-04-07T18:24:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-07T18:10:01Z
body: - This pull request modifies code in `src/*` but no tests were added/updated.
    - Confirm whether tests should be added or ensure the PR description explains why tests are not required.

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-07T18:12:37Z
body: - This pull request modifies code in `src/*` but no tests were added/updated.
    - Confirm whether tests should be added or ensure the PR description explains why tests are not required.
- This pull request implements a `feat` or `fix`, so it must include a changelog entry (unless the fix is for an *unreleased* feature). Review the [changelog guidelines](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#changelog).
    - Note: beta or "experiment" features that have active users *should* announce fixes in the changelog.
    - If this is not a feature or fix, use an appropriate type from the [title guidelines](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#pull-request-title). For example, telemetry-only changes should use the `telemetry` type.

author_association: NONE