Skip to content

Commit c77225e

Browse files
authored
feat(chat): show stop button for agentic loop (aws#6947)
## Problem Agentic chat should have a stop button ## Solution Implement `asyncEventProgressMessage` for CWChat --- - 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.
1 parent 69de65c commit c77225e

File tree

6 files changed

+42
-121
lines changed

6 files changed

+42
-121
lines changed

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

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,21 @@ export interface ConnectorProps extends BaseConnectorProps {
2727
description?: string
2828
) => void
2929
onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
30+
onAsyncEventProgress: (
31+
tabID: string,
32+
inProgress: boolean,
33+
message: string,
34+
messageId: string | undefined,
35+
enableStopAction: boolean
36+
) => void
3037
}
3138

3239
export class Connector extends BaseConnector {
3340
private readonly onCWCContextCommandMessage
3441
private readonly onContextCommandDataReceived
3542
private readonly onShowCustomForm
3643
private readonly onChatAnswerUpdated
44+
private readonly onAsyncEventProgress
3745
private chatItems: Map<string, Map<string, ChatItem>> = new Map() // tabId -> messageId -> ChatItem
3846

3947
override getTabType(): TabType {
@@ -46,6 +54,7 @@ export class Connector extends BaseConnector {
4654
this.onContextCommandDataReceived = props.onContextCommandDataReceived
4755
this.onShowCustomForm = props.onShowCustomForm
4856
this.onChatAnswerUpdated = props.onChatAnswerUpdated
57+
this.onAsyncEventProgress = props.onAsyncEventProgress
4958
}
5059

5160
onSourceLinkClick = (tabID: string, messageId: string, link: string): void => {
@@ -168,35 +177,6 @@ export class Connector extends BaseConnector {
168177
}
169178
}
170179

171-
private processToolMessage = async (messageData: any): Promise<void> => {
172-
if (this.onChatAnswerUpdated === undefined) {
173-
return
174-
}
175-
const answer: CWCChatItem = {
176-
type: messageData.messageType,
177-
messageId: messageData.messageID ?? messageData.triggerID,
178-
body: messageData.message,
179-
followUp: messageData.followUps,
180-
canBeVoted: messageData.canBeVoted ?? false,
181-
codeReference: messageData.codeReference,
182-
userIntent: messageData.contextList,
183-
codeBlockLanguage: messageData.codeBlockLanguage,
184-
contextList: messageData.contextList,
185-
title: messageData.title,
186-
buttons: messageData.buttons,
187-
fileList: messageData.fileList,
188-
header: messageData.header ?? undefined,
189-
padding: messageData.padding ?? undefined,
190-
fullWidth: messageData.fullWidth ?? undefined,
191-
codeBlockActions: messageData.codeBlockActions ?? undefined,
192-
}
193-
if (answer.messageId) {
194-
this.storeChatItem(messageData.tabID, answer.messageId, answer)
195-
}
196-
this.onChatAnswerUpdated(messageData.tabID, answer)
197-
return
198-
}
199-
200180
private storeChatItem(tabId: string, messageId: string, item: ChatItem): void {
201181
if (!this.chatItems.has(tabId)) {
202182
this.chatItems.set(tabId, new Map())
@@ -253,11 +233,6 @@ export class Connector extends BaseConnector {
253233
return
254234
}
255235

256-
if (messageData.type === 'toolMessage') {
257-
await this.processToolMessage(messageData)
258-
return
259-
}
260-
261236
if (messageData.type === 'editorContextCommandMessage') {
262237
await this.processEditorContextCommandMessage(messageData)
263238
return
@@ -276,6 +251,18 @@ export class Connector extends BaseConnector {
276251
this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action)
277252
return
278253
}
254+
255+
if (messageData.type === 'asyncEventProgressMessage') {
256+
const enableStopAction = true
257+
this.onAsyncEventProgress(
258+
messageData.tabID,
259+
messageData.inProgress,
260+
messageData.message ?? undefined,
261+
messageData.messageId ?? undefined,
262+
enableStopAction
263+
)
264+
return
265+
}
279266
// For other message types, call the base class handleMessageReceive
280267
await this.baseHandleMessageReceive(messageData)
281268
}

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ export class ChatSession {
2222
* _readFiles = list of files read from the project to gather context before generating response.
2323
* _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user
2424
* _context = Additional context to be passed to the LLM for generating the response
25-
* _messageIdToUpdate = messageId of a chat message to be updated, used for reducing consecutive tool messages
2625
*/
2726
private _readFiles: string[] = []
2827
private _toolUse: ToolUse | undefined
2928
private _showDiffOnFileWrite: boolean = false
3029
private _context: PromptMessage['context']
3130
private _pairProgrammingModeOn: boolean = true
32-
private _messageIdToUpdate: string | undefined
3331

3432
contexts: Map<string, { first: number; second: number }[]> = new Map()
3533
// 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 {
6361
this._context = context
6462
}
6563

66-
public get messageIdToUpdate(): string | undefined {
67-
return this._messageIdToUpdate
68-
}
69-
70-
public setMessageIdToUpdate(messageId: string | undefined) {
71-
this._messageIdToUpdate = messageId
72-
}
73-
7464
public tokenSource!: vscode.CancellationTokenSource
7565

7666
constructor() {

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ export class ChatController {
716716
type: 'chat_message',
717717
context,
718718
})
719+
this.messenger.sendAsyncEventProgress(tabID, true, '')
719720
const session = this.sessionStorage.getSession(tabID)
720721
const toolUse = session.toolUse
721722
if (!toolUse || !toolUse.input) {
@@ -734,16 +735,9 @@ export class ChatController {
734735
try {
735736
await ToolUtils.validate(tool)
736737

737-
const chatStream = new ChatStream(
738-
this.messenger,
739-
tabID,
740-
triggerID,
741-
// Pass in a different toolUseId so that the output does not overwrite
742-
// any previous messages
743-
{ ...toolUse, toolUseId: `${toolUse.toolUseId}-output` },
744-
{ requiresAcceptance: false },
745-
undefined
746-
)
738+
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, {
739+
requiresAcceptance: false,
740+
})
747741
const output = await ToolUtils.invoke(tool, chatStream)
748742
if (output.output.content.length > maxToolOutputCharacterLength) {
749743
throw Error(
@@ -1185,6 +1179,8 @@ export class ChatController {
11851179
context,
11861180
})
11871181

1182+
this.messenger.sendAsyncEventProgress(message.tabID, true, '')
1183+
11881184
// Save the context for the agentic loop
11891185
session.setContext(message.context)
11901186

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

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
OpenSettingsMessage,
1515
QuickActionMessage,
1616
ShowCustomFormMessage,
17-
ToolMessage,
1817
} from '../../../view/connector/connector'
1918
import { EditorContextCommandType } from '../../../commands/registerCommands'
2019
import { ChatResponseStream as qdevChatResponseStream } from '@amzn/amazon-q-developer-streaming-client'
@@ -49,6 +48,7 @@ import { extractErrorInfo } from '../../../../shared/utilities/messageUtil'
4948
import { noWriteTools, tools } from '../../../constants'
5049
import { Change } from 'diff'
5150
import { FsWriteParams } from '../../../tools/fsWrite'
51+
import { AsyncEventProgressMessage } from '../../../../amazonq/commons/connector/connectorMessages'
5252

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

@@ -238,36 +238,10 @@ export class Messenger {
238238
session.setShowDiffOnFileWrite(true)
239239
changeList = await tool.tool.getDiffChanges()
240240
}
241-
if (
242-
tool.type === ToolType.FsWrite ||
243-
tool.type === ToolType.ExecuteBash ||
244-
eventCounts.has('assistantResponseEvent')
245-
) {
246-
// FsWrite and ExecuteBash should never replace older messages
247-
// If the current stream also has assistantResponseEvent then reset this as well.
248-
session.setMessageIdToUpdate(undefined)
249-
}
250241
const validation = ToolUtils.requiresAcceptance(tool)
251-
252-
const chatStream = new ChatStream(
253-
this,
254-
tabID,
255-
triggerID,
256-
toolUse,
257-
validation,
258-
session.messageIdToUpdate,
259-
changeList
260-
)
242+
const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation, changeList)
261243
await ToolUtils.queueDescription(tool, chatStream)
262244

263-
if (
264-
session.messageIdToUpdate === undefined &&
265-
(tool.type === ToolType.FsRead || tool.type === ToolType.ListDirectory)
266-
) {
267-
// Store the first messageId in a chain of tool uses
268-
session.setMessageIdToUpdate(toolUse.toolUseId)
269-
}
270-
271245
if (!validation.requiresAcceptance) {
272246
// 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.
273247
this.dispatcher.sendCustomFormActionMessage(
@@ -469,33 +443,12 @@ export class Messenger {
469443
)
470444
}
471445

472-
public sendInitialToolMessage(tabID: string, triggerID: string, toolUseId: string | undefined) {
473-
this.dispatcher.sendChatMessage(
474-
new ChatMessage(
475-
{
476-
message: '',
477-
messageType: 'answer',
478-
followUps: undefined,
479-
followUpsHeader: undefined,
480-
relatedSuggestions: undefined,
481-
triggerID,
482-
messageID: toolUseId ?? 'toolUse',
483-
userIntent: undefined,
484-
codeBlockLanguage: undefined,
485-
contextList: undefined,
486-
},
487-
tabID
488-
)
489-
)
490-
}
491-
492446
public sendPartialToolLog(
493447
message: string,
494448
tabID: string,
495449
triggerID: string,
496450
toolUse: ToolUse | undefined,
497451
validation: CommandValidation,
498-
messageIdToUpdate: string | undefined,
499452
changeList?: Change[]
500453
) {
501454
const buttons: ChatItemButton[] = []
@@ -550,16 +503,16 @@ export class Messenger {
550503
})
551504
}
552505

553-
this.dispatcher.sendToolMessage(
554-
new ToolMessage(
506+
this.dispatcher.sendChatMessage(
507+
new ChatMessage(
555508
{
556509
message: message,
557510
messageType: 'answer-part',
558511
followUps: undefined,
559512
followUpsHeader: undefined,
560513
relatedSuggestions: undefined,
561514
triggerID,
562-
messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '',
515+
messageID: toolUse?.toolUseId ?? '',
563516
userIntent: undefined,
564517
codeBlockLanguage: undefined,
565518
contextList: undefined,
@@ -738,4 +691,8 @@ export class Messenger {
738691
new ShowCustomFormMessage(tabID, formItems, buttons, title, description)
739692
)
740693
}
694+
695+
public sendAsyncEventProgress(tabID: string, inProgress: boolean, message: string | undefined) {
696+
this.dispatcher.sendAsyncEventProgress(new AsyncEventProgressMessage(tabID, 'CWChat', inProgress, message))
697+
}
741698
}

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,12 @@ export class ChatStream extends Writable {
2323
private readonly triggerID: string,
2424
private readonly toolUse: ToolUse | undefined,
2525
private readonly validation: CommandValidation,
26-
private readonly messageIdToUpdate: string | undefined,
2726
private readonly changeList?: Change[],
2827
private readonly logger = getLogger('chatStream')
2928
) {
3029
super()
3130
this.logger.debug(`ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}`)
32-
if (!messageIdToUpdate) {
33-
// If messageIdToUpdate is undefined, we need to first create an empty message
34-
// with messageId so it can be updated later
35-
this.messenger.sendInitialToolMessage(tabID, triggerID, toolUse?.toolUseId)
36-
}
31+
this.messenger.sendInitalStream(tabID, triggerID, undefined)
3732
}
3833

3934
override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
@@ -46,7 +41,6 @@ export class ChatStream extends Writable {
4641
this.triggerID,
4742
this.toolUse,
4843
this.validation,
49-
this.messageIdToUpdate,
5044
this.changeList
5145
)
5246
callback()

packages/core/src/codewhispererChat/view/connector/connector.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
Status,
2020
} from '@aws/mynah-ui'
2121
import { DocumentReference } from '../../controllers/chat/model'
22+
import { AsyncEventProgressMessage } from '../../../amazonq/commons/connector/connectorMessages'
2223

2324
class UiMessage {
2425
readonly time: number = Date.now()
@@ -286,10 +287,6 @@ export class ChatMessage extends UiMessage {
286287
}
287288
}
288289

289-
export class ToolMessage extends ChatMessage {
290-
override type = 'toolMessage'
291-
}
292-
293290
export interface FollowUp {
294291
readonly type: string
295292
readonly pillText: string
@@ -344,10 +341,6 @@ export class AppToWebViewMessageDispatcher {
344341
this.appsToWebViewMessagePublisher.publish(message)
345342
}
346343

347-
public sendToolMessage(message: ToolMessage) {
348-
this.appsToWebViewMessagePublisher.publish(message)
349-
}
350-
351344
public sendEditorContextCommandMessage(message: EditorContextCommandMessage) {
352345
this.appsToWebViewMessagePublisher.publish(message)
353346
}
@@ -375,4 +368,8 @@ export class AppToWebViewMessageDispatcher {
375368
public sendCustomFormActionMessage(message: CustomFormActionMessage) {
376369
this.appsToWebViewMessagePublisher.publish(message)
377370
}
371+
372+
public sendAsyncEventProgress(message: AsyncEventProgressMessage) {
373+
this.appsToWebViewMessagePublisher.publish(message)
374+
}
378375
}

0 commit comments

Comments
 (0)