Skip to content

Commit b222e0b

Browse files
authored
feat(chat): Grouping read and list directory messages UX (aws#7006)
### Problem: We identified and fixed an issue where the IDE's UI would prematurely stop displaying when executing certain commands. This occurred specifically when the Language Model (LLM) attempted to run the executeBash command before the Read/List Directory tools. ### Example: When a user asks "write a script that tells me whoami", the operation only requires the fsWrite command and not any directory reading tools. In such cases, the IDE would execute fsWrite but fail to display the executeBash tool in the chat interface. This[ UI behavior has been corrected ](https://github.com/aws/aws-toolkit-vscode/blob/2028754f3d933909f86bb65f18f06dab526d2708/packages/core/src/codewhispererChat/tools/chatStream.ts#L43-L47)in this PR, along with improvements to the Reading File user experience. - Major changes lies in this [commit](https://github.com/aws/aws-toolkit-vscode/blob/2028754f3d933909f86bb65f18f06dab526d2708/packages/core/src/codewhispererChat/tools/chatStream.ts#L43-L47) in `ChatStream.ts` ``` // For FsRead and ListDirectory tools If messageIdToUpdate is undefined, we need to first create an empty message with messageId so it can be updated later if (isReadorList && !messageIdToUpdate) { this.messenger.sendInitialToolMessage(tabID, triggerID, toolUse?.toolUseId) } else { this.messenger.sendInitalStream(tabID, triggerID) } ``` --- - 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 68f9468 commit b222e0b

File tree

11 files changed

+308
-85
lines changed

11 files changed

+308
-85
lines changed

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

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,33 @@ export class Connector extends BaseConnector {
179179
}
180180
}
181181

182+
private processToolMessage = async (messageData: any): Promise<void> => {
183+
if (this.onChatAnswerUpdated === undefined) {
184+
return
185+
}
186+
const answer: CWCChatItem = {
187+
type: messageData.messageType,
188+
messageId: messageData.messageID ?? messageData.triggerID,
189+
body: messageData.message,
190+
followUp: messageData.followUps,
191+
canBeVoted: messageData.canBeVoted ?? false,
192+
codeReference: messageData.codeReference,
193+
userIntent: messageData.contextList,
194+
codeBlockLanguage: messageData.codeBlockLanguage,
195+
contextList: messageData.contextList,
196+
title: messageData.title,
197+
buttons: messageData.buttons,
198+
fileList: messageData.fileList,
199+
header: messageData.header ?? undefined,
200+
padding: messageData.padding ?? undefined,
201+
fullWidth: messageData.fullWidth ?? undefined,
202+
codeBlockActions: messageData.codeBlockActions ?? undefined,
203+
rootFolderTitle: messageData.rootFolderTitle,
204+
}
205+
this.onChatAnswerUpdated(messageData.tabID, answer)
206+
return
207+
}
208+
182209
private storeChatItem(tabId: string, messageId: string, item: ChatItem): void {
183210
if (!this.chatItems.has(tabId)) {
184211
this.chatItems.set(tabId, new Map())
@@ -238,6 +265,11 @@ export class Connector extends BaseConnector {
238265
return
239266
}
240267

268+
if (messageData.type === 'toolMessage') {
269+
await this.processToolMessage(messageData)
270+
return
271+
}
272+
241273
if (messageData.type === 'editorContextCommandMessage') {
242274
await this.processEditorContextCommandMessage(messageData)
243275
return
@@ -369,45 +401,43 @@ export class Connector extends BaseConnector {
369401
}
370402
break
371403
case 'run-shell-command':
372-
answer.header = {
373-
body: 'shell',
374-
status: {
404+
if (answer.header) {
405+
answer.header.status = {
375406
icon: 'ok' as MynahIconsType,
376407
text: 'Accepted',
377408
status: 'success',
378-
},
409+
}
410+
answer.header.buttons = []
379411
}
380412
break
381413
case 'reject-shell-command':
382-
answer.header = {
383-
body: 'shell',
384-
status: {
414+
if (answer.header) {
415+
answer.header.status = {
385416
icon: 'cancel' as MynahIconsType,
386417
text: 'Rejected',
387418
status: 'error',
388-
},
419+
}
420+
answer.header.buttons = []
389421
}
390422
break
391423
case 'confirm-tool-use':
392-
answer.header = {
393-
icon: 'shell' as MynahIconsType,
394-
body: 'shell',
395-
status: {
424+
if (answer.header) {
425+
answer.header.status = {
396426
icon: 'ok' as MynahIconsType,
397427
text: 'Accepted',
398428
status: 'success',
399-
},
429+
}
430+
answer.header.buttons = []
400431
}
401432
break
402433
case 'reject-tool-use':
403-
answer.header = {
404-
icon: 'shell' as MynahIconsType,
405-
body: 'shell',
406-
status: {
434+
if (answer.header) {
435+
answer.header.status = {
407436
icon: 'cancel' as MynahIconsType,
408437
text: 'Rejected',
409438
status: 'error',
410-
},
439+
}
440+
answer.header.buttons = []
411441
}
412442
break
413443
default:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export interface ConnectorProps {
7878
sendMessageToExtension: (message: ExtensionMessage) => void
7979
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
8080
onRunTestMessageReceived?: (tabID: string, showRunTestMessage: boolean) => void
81-
onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
81+
onChatAnswerUpdated?: (tabID: string, message: CWCChatItem) => void
8282
onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void
8383
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => void
8484
onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined) => void

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

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,41 @@ export const createMynahUI = (
100100
welcomeCount += 1
101101
}
102102

103+
/**
104+
* Creates a file list header from context list
105+
* @param contextList List of file contexts
106+
* @param rootFolderTitle Title for the root folder
107+
* @returns Header object with file list
108+
*/
109+
const createFileListHeader = (contextList: any[], rootFolderTitle?: string) => {
110+
return {
111+
fileList: {
112+
fileTreeTitle: '',
113+
filePaths: contextList.map((file) => file.relativeFilePath),
114+
rootFolderTitle: rootFolderTitle,
115+
flatList: true,
116+
collapsed: true,
117+
hideFileCount: true,
118+
details: Object.fromEntries(
119+
contextList.map((file) => [
120+
file.relativeFilePath,
121+
{
122+
label: file.lineRanges
123+
.map((range: { first: number; second: number }) =>
124+
range.first === -1 || range.second === -1
125+
? ''
126+
: `line ${range.first} - ${range.second}`
127+
)
128+
.join(', '),
129+
description: file.relativeFilePath,
130+
clickable: true,
131+
},
132+
])
133+
),
134+
},
135+
}
136+
}
137+
103138
// Adding the first tab as CWC tab
104139
tabsStorage.addTab({
105140
id: 'tab-1',
@@ -346,8 +381,11 @@ export const createMynahUI = (
346381
sendMessageToExtension: (message) => {
347382
ideApi.postMessage(message)
348383
},
349-
onChatAnswerUpdated: (tabID: string, item: ChatItem) => {
384+
onChatAnswerUpdated: (tabID: string, item: CWCChatItem) => {
350385
if (item.messageId !== undefined) {
386+
if (item.contextList !== undefined && item.contextList.length > 0) {
387+
item.header = createFileListHeader(item.contextList, item.rootFolderTitle)
388+
}
351389
mynahUI.updateChatAnswerWithMessageId(tabID, item.messageId, {
352390
...(item.body !== undefined ? { body: item.body } : {}),
353391
...(item.buttons !== undefined ? { buttons: item.buttons } : {}),
@@ -409,32 +447,7 @@ export const createMynahUI = (
409447
}
410448

411449
if (item.contextList !== undefined && item.contextList.length > 0) {
412-
item.header = {
413-
fileList: {
414-
fileTreeTitle: '',
415-
filePaths: item.contextList.map((file) => file.relativeFilePath),
416-
rootFolderTitle: item.rootFolderTitle,
417-
flatList: true,
418-
collapsed: true,
419-
hideFileCount: true,
420-
details: Object.fromEntries(
421-
item.contextList.map((file) => [
422-
file.relativeFilePath,
423-
{
424-
label: file.lineRanges
425-
.map((range) =>
426-
range.first === -1 || range.second === -1
427-
? ''
428-
: `line ${range.first} - ${range.second}`
429-
)
430-
.join(', '),
431-
description: file.relativeFilePath,
432-
clickable: true,
433-
},
434-
])
435-
),
436-
},
437-
}
450+
item.header = createFileListHeader(item.contextList, item.rootFolderTitle)
438451
}
439452

440453
if (

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { ToolkitError } from '../../../../shared/errors'
1414
import { createCodeWhispererChatStreamingClient } from '../../../../shared/clients/codewhispererChatClient'
1515
import { createQDeveloperStreamingClient } from '../../../../shared/clients/qDeveloperChatClient'
1616
import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWrittenCodeTracker'
17-
import { PromptMessage } from '../../../controllers/chat/model'
17+
import { DocumentReference, PromptMessage } from '../../../controllers/chat/model'
1818
import { FsWriteBackup } from '../../../../codewhispererChat/tools/fsWrite'
1919

2020
export type ToolUseWithError = {
@@ -30,8 +30,10 @@ export class ChatSession {
3030
* _readFiles = list of files read from the project to gather context before generating response.
3131
* _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user
3232
* _context = Additional context to be passed to the LLM for generating the response
33+
* _messageIdToUpdate = messageId of a chat message to be updated, used for reducing consecutive tool messages
3334
*/
34-
private _readFiles: string[] = []
35+
private _readFiles: DocumentReference[] = []
36+
private _readFolders: DocumentReference[] = []
3537
private _toolUseWithError: ToolUseWithError | undefined
3638
private _showDiffOnFileWrite: boolean = false
3739
private _context: PromptMessage['context']
@@ -41,6 +43,8 @@ export class ChatSession {
4143
* True if messages from local history have been sent to session.
4244
*/
4345
localHistoryHydrated: boolean = false
46+
private _messageIdToUpdate: string | undefined
47+
private _messageIdToUpdateListDirectory: string | undefined
4448

4549
contexts: Map<string, { first: number; second: number }[]> = new Map()
4650
// TODO: doesn't handle the edge case when two files share the same relativePath string but from different root
@@ -49,6 +53,21 @@ export class ChatSession {
4953
public get sessionIdentifier(): string | undefined {
5054
return this.sessionId
5155
}
56+
public get messageIdToUpdate(): string | undefined {
57+
return this._messageIdToUpdate
58+
}
59+
60+
public setMessageIdToUpdate(messageId: string | undefined) {
61+
this._messageIdToUpdate = messageId
62+
}
63+
64+
public get messageIdToUpdateListDirectory(): string | undefined {
65+
return this._messageIdToUpdateListDirectory
66+
}
67+
68+
public setMessageIdToUpdateListDirectory(messageId: string | undefined) {
69+
this._messageIdToUpdateListDirectory = messageId
70+
}
5271

5372
public get pairProgrammingModeOn(): boolean {
5473
return this._pairProgrammingModeOn
@@ -95,21 +114,30 @@ export class ChatSession {
95114
public setSessionID(id?: string) {
96115
this.sessionId = id
97116
}
98-
public get readFiles(): string[] {
117+
public get readFiles(): DocumentReference[] {
99118
return this._readFiles
100119
}
120+
public get readFolders(): DocumentReference[] {
121+
return this._readFolders
122+
}
101123
public get showDiffOnFileWrite(): boolean {
102124
return this._showDiffOnFileWrite
103125
}
104126
public setShowDiffOnFileWrite(value: boolean) {
105127
this._showDiffOnFileWrite = value
106128
}
107-
public addToReadFiles(filePath: string) {
129+
public addToReadFiles(filePath: DocumentReference) {
108130
this._readFiles.push(filePath)
109131
}
110132
public clearListOfReadFiles() {
111133
this._readFiles = []
112134
}
135+
public setReadFolders(folder: DocumentReference) {
136+
this._readFolders.push(folder)
137+
}
138+
public clearListOfReadFolders() {
139+
this._readFolders = []
140+
}
113141
async chatIam(chatRequest: SendMessageRequest): Promise<SendMessageCommandOutput> {
114142
const client = await createQDeveloperStreamingClient()
115143

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,19 @@ export class ChatController {
728728
try {
729729
await ToolUtils.validate(tool)
730730

731-
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, {
732-
requiresAcceptance: false,
733-
})
731+
const chatStream = new ChatStream(
732+
this.messenger,
733+
tabID,
734+
triggerID,
735+
toolUse,
736+
session,
737+
undefined,
738+
false,
739+
{
740+
requiresAcceptance: false,
741+
},
742+
false
743+
)
734744
if (tool.type === ToolType.FsWrite && toolUse.toolUseId) {
735745
const backup = await tool.tool.getBackup()
736746
session.setFsWriteBackup(toolUse.toolUseId, backup)
@@ -1221,6 +1231,7 @@ export class ChatController {
12211231
private async processPromptMessageAsNewThread(message: PromptMessage) {
12221232
const session = this.sessionStorage.getSession(message.tabID)
12231233
session.clearListOfReadFiles()
1234+
session.clearListOfReadFolders()
12241235
session.setShowDiffOnFileWrite(false)
12251236
this.editorContextExtractor
12261237
.extractContextForTrigger('ChatMessage')

0 commit comments

Comments
 (0)