Skip to content

Commit 5d63306

Browse files
authored
Merge pull request aws#6931 from ctlai95/replace-read-messages
feat(chat): group consecutive read tool messages
2 parents cc41a7f + f2bfc78 commit 5d63306

File tree

7 files changed

+130
-19
lines changed

7 files changed

+130
-19
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,35 @@ export class Connector extends BaseConnector {
168168
}
169169
}
170170

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+
171200
private storeChatItem(tabId: string, messageId: string, item: ChatItem): void {
172201
if (!this.chatItems.has(tabId)) {
173202
this.chatItems.set(tabId, new Map())
@@ -224,6 +253,11 @@ export class Connector extends BaseConnector {
224253
return
225254
}
226255

256+
if (messageData.type === 'toolMessage') {
257+
await this.processToolMessage(messageData)
258+
return
259+
}
260+
227261
if (messageData.type === 'editorContextCommandMessage') {
228262
await this.processEditorContextCommandMessage(messageData)
229263
return

packages/core/src/amazonq/webview/ui/main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,12 @@ export const createMynahUI = (
331331
...(item.followUp !== undefined ? { followUp: item.followUp } : {}),
332332
...(item.footer !== undefined ? { footer: item.footer } : {}),
333333
...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}),
334+
...(item.fileList !== undefined ? { fileList: item.fileList } : {}),
334335
...(item.header !== undefined ? { header: item.header } : {}),
336+
...(item.buttons !== undefined ? { buttons: item.buttons } : {}),
337+
...(item.fullWidth !== undefined ? { fullWidth: item.fullWidth } : {}),
338+
...(item.padding !== undefined ? { padding: item.padding } : {}),
339+
...(item.codeBlockActions !== undefined ? { codeBlockActions: item.codeBlockActions } : {}),
335340
})
336341
} else {
337342
mynahUI.updateLastChatAnswer(tabID, {
@@ -341,6 +346,10 @@ export const createMynahUI = (
341346
...(item.footer !== undefined ? { footer: item.footer } : {}),
342347
...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}),
343348
...(item.header !== undefined ? { header: item.header } : {}),
349+
...(item.buttons !== undefined ? { buttons: item.buttons } : {}),
350+
...(item.fullWidth !== undefined ? { fullWidth: item.fullWidth } : {}),
351+
...(item.padding !== undefined ? { padding: item.padding } : {}),
352+
...(item.codeBlockActions !== undefined ? { codeBlockActions: item.codeBlockActions } : {}),
344353
})
345354
}
346355
},

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ 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
2526
*/
2627
private _readFiles: string[] = []
2728
private _toolUse: ToolUse | undefined
2829
private _showDiffOnFileWrite: boolean = false
2930
private _context: PromptMessage['context']
31+
private _messageIdToUpdate: string | undefined
3032

3133
contexts: Map<string, { first: number; second: number }[]> = new Map()
3234
// TODO: doesn't handle the edge case when two files share the same relativePath string but from different root
@@ -52,6 +54,14 @@ export class ChatSession {
5254
this._context = context
5355
}
5456

57+
public get messageIdToUpdate(): string | undefined {
58+
return this._messageIdToUpdate
59+
}
60+
61+
public setMessageIdToUpdate(messageId: string | undefined) {
62+
this._messageIdToUpdate = messageId
63+
}
64+
5565
public tokenSource!: vscode.CancellationTokenSource
5666

5767
constructor() {

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -663,9 +663,16 @@ export class ChatController {
663663
try {
664664
await ToolUtils.validate(tool)
665665

666-
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, {
667-
requiresAcceptance: false,
668-
})
666+
const chatStream = new ChatStream(
667+
this.messenger,
668+
tabID,
669+
triggerID,
670+
// Pass in a different toolUseId so that the output does not overwrite
671+
// any previous messages
672+
{ ...toolUse, toolUseId: `${toolUse.toolUseId}-output` },
673+
{ requiresAcceptance: false },
674+
undefined
675+
)
669676
const output = await ToolUtils.invoke(tool, chatStream)
670677

671678
toolResults.push({

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

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
OpenSettingsMessage,
1515
QuickActionMessage,
1616
ShowCustomFormMessage,
17+
ToolMessage,
1718
} from '../../../view/connector/connector'
1819
import { EditorContextCommandType } from '../../../commands/registerCommands'
1920
import { ChatResponseStream as qdevChatResponseStream } from '@amzn/amazon-q-developer-streaming-client'
@@ -221,11 +222,35 @@ export class Messenger {
221222
if (tool.type === ToolType.FsWrite) {
222223
session.setShowDiffOnFileWrite(true)
223224
}
225+
if (
226+
tool.type === ToolType.FsWrite ||
227+
tool.type === ToolType.ExecuteBash ||
228+
eventCounts.has('assistantResponseEvent')
229+
) {
230+
// FsWrite and ExecuteBash should never replace older messages
231+
// If the current stream also has assistantResponseEvent then reset this as well.
232+
session.setMessageIdToUpdate(undefined)
233+
}
224234
const validation = ToolUtils.requiresAcceptance(tool)
225235

226-
const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation)
236+
const chatStream = new ChatStream(
237+
this,
238+
tabID,
239+
triggerID,
240+
toolUse,
241+
validation,
242+
session.messageIdToUpdate
243+
)
227244
await ToolUtils.queueDescription(tool, chatStream)
228245

246+
if (
247+
session.messageIdToUpdate === undefined &&
248+
(tool.type === ToolType.FsRead || tool.type === ToolType.ListDirectory)
249+
) {
250+
// Store the first messageId in a chain of tool uses
251+
session.setMessageIdToUpdate(toolUse.toolUseId)
252+
}
253+
229254
if (!validation.requiresAcceptance) {
230255
// 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.
231256
this.dispatcher.sendCustomFormActionMessage(
@@ -429,12 +454,33 @@ export class Messenger {
429454
)
430455
}
431456

457+
public sendInitialToolMessage(tabID: string, triggerID: string, toolUseId: string | undefined) {
458+
this.dispatcher.sendChatMessage(
459+
new ChatMessage(
460+
{
461+
message: '',
462+
messageType: 'answer',
463+
followUps: undefined,
464+
followUpsHeader: undefined,
465+
relatedSuggestions: undefined,
466+
triggerID,
467+
messageID: toolUseId ?? 'toolUse',
468+
userIntent: undefined,
469+
codeBlockLanguage: undefined,
470+
contextList: undefined,
471+
},
472+
tabID
473+
)
474+
)
475+
}
476+
432477
public sendPartialToolLog(
433478
message: string,
434479
tabID: string,
435480
triggerID: string,
436481
toolUse: ToolUse | undefined,
437-
validation: CommandValidation
482+
validation: CommandValidation,
483+
messageIdToUpdate: string | undefined
438484
) {
439485
const buttons: ChatItemButton[] = []
440486
let fileList: ChatItemContent['fileList'] = undefined
@@ -483,16 +529,16 @@ export class Messenger {
483529
})
484530
}
485531

486-
this.dispatcher.sendChatMessage(
487-
new ChatMessage(
532+
this.dispatcher.sendToolMessage(
533+
new ToolMessage(
488534
{
489535
message: message,
490536
messageType: 'answer-part',
491537
followUps: undefined,
492538
followUpsHeader: undefined,
493539
relatedSuggestions: undefined,
494540
triggerID,
495-
messageID: toolUse?.toolUseId ?? `tool-output`,
541+
messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '',
496542
userIntent: undefined,
497543
codeBlockLanguage: undefined,
498544
contextList: undefined,

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@ export class ChatStream extends Writable {
2222
private readonly triggerID: string,
2323
private readonly toolUse: ToolUse | undefined,
2424
private readonly validation: CommandValidation,
25+
private readonly messageIdToUpdate: string | undefined,
2526
private readonly logger = getLogger('chatStream')
2627
) {
2728
super()
2829
this.logger.debug(`ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}`)
29-
this.messenger.sendInitalStream(tabID, triggerID, undefined)
30+
if (!messageIdToUpdate) {
31+
// If messageIdToUpdate is undefined, we need to first create an empty message
32+
// with messageId so it can be updated later
33+
this.messenger.sendInitialToolMessage(tabID, triggerID, toolUse?.toolUseId)
34+
}
3035
}
3136

3237
override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
@@ -38,21 +43,13 @@ export class ChatStream extends Writable {
3843
this.tabID,
3944
this.triggerID,
4045
this.toolUse,
41-
this.validation
46+
this.validation,
47+
this.messageIdToUpdate
4248
)
4349
callback()
4450
}
4551

4652
override _final(callback: (error?: Error | null) => void): void {
47-
if (this.accumulatedLogs.trim().length > 0) {
48-
this.messenger.sendPartialToolLog(
49-
this.accumulatedLogs,
50-
this.tabID,
51-
this.triggerID,
52-
this.toolUse,
53-
this.validation
54-
)
55-
}
5653
callback()
5754
}
5855
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ export class ChatMessage extends UiMessage {
286286
}
287287
}
288288

289+
export class ToolMessage extends ChatMessage {
290+
override type = 'toolMessage'
291+
}
292+
289293
export interface FollowUp {
290294
readonly type: string
291295
readonly pillText: string
@@ -340,6 +344,10 @@ export class AppToWebViewMessageDispatcher {
340344
this.appsToWebViewMessagePublisher.publish(message)
341345
}
342346

347+
public sendToolMessage(message: ToolMessage) {
348+
this.appsToWebViewMessagePublisher.publish(message)
349+
}
350+
343351
public sendEditorContextCommandMessage(message: EditorContextCommandMessage) {
344352
this.appsToWebViewMessagePublisher.publish(message)
345353
}

0 commit comments

Comments
 (0)