Skip to content

Commit 9376bd7

Browse files
committed
feat(chat): group consecutive read tool messages
1 parent 66da962 commit 9376bd7

File tree

7 files changed

+115
-19
lines changed

7 files changed

+115
-19
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,28 @@ export class Connector extends BaseConnector {
154154
}
155155
}
156156

157+
private processToolMessage = async (messageData: any): Promise<void> => {
158+
if (this.onChatAnswerUpdated === undefined) {
159+
return
160+
}
161+
const answer: CWCChatItem = {
162+
type: messageData.messageType,
163+
messageId: messageData.messageID ?? messageData.triggerID,
164+
body: messageData.message,
165+
followUp: messageData.followUps,
166+
canBeVoted: messageData.canBeVoted ?? false,
167+
codeReference: messageData.codeReference,
168+
userIntent: messageData.contextList,
169+
codeBlockLanguage: messageData.codeBlockLanguage,
170+
contextList: messageData.contextList,
171+
title: messageData.title,
172+
buttons: messageData.buttons,
173+
fileList: messageData.fileList,
174+
}
175+
this.onChatAnswerUpdated(messageData.tabID, answer)
176+
return
177+
}
178+
157179
processContextCommandData(messageData: any) {
158180
if (messageData.data) {
159181
this.onContextCommandDataReceived(messageData.data)
@@ -199,6 +221,11 @@ export class Connector extends BaseConnector {
199221
return
200222
}
201223

224+
if (messageData.type === 'toolMessage') {
225+
await this.processToolMessage(messageData)
226+
return
227+
}
228+
202229
if (messageData.type === 'editorContextCommandMessage') {
203230
await this.processEditorContextCommandMessage(messageData)
204231
return

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ 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
})
335336
} else {
336337
mynahUI.updateLastChatAnswer(tabID, {

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
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
@@ -474,16 +520,16 @@ export class Messenger {
474520
})
475521
}
476522

477-
this.dispatcher.sendChatMessage(
478-
new ChatMessage(
523+
this.dispatcher.sendToolMessage(
524+
new ToolMessage(
479525
{
480526
message: message,
481527
messageType: 'answer-part',
482528
followUps: undefined,
483529
followUpsHeader: undefined,
484530
relatedSuggestions: undefined,
485531
triggerID,
486-
messageID: toolUse?.toolUseId ?? `tool-output`,
532+
messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '',
487533
userIntent: undefined,
488534
codeBlockLanguage: undefined,
489535
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
@@ -253,6 +253,10 @@ export class ChatMessage extends UiMessage {
253253
}
254254
}
255255

256+
export class ToolMessage extends ChatMessage {
257+
override type = 'toolMessage'
258+
}
259+
256260
export interface FollowUp {
257261
readonly type: string
258262
readonly pillText: string
@@ -307,6 +311,10 @@ export class AppToWebViewMessageDispatcher {
307311
this.appsToWebViewMessagePublisher.publish(message)
308312
}
309313

314+
public sendToolMessage(message: ToolMessage) {
315+
this.appsToWebViewMessagePublisher.publish(message)
316+
}
317+
310318
public sendEditorContextCommandMessage(message: EditorContextCommandMessage) {
311319
this.appsToWebViewMessagePublisher.publish(message)
312320
}

0 commit comments

Comments
 (0)