Skip to content

Commit 0b9f81e

Browse files
committed
Adding Tool execution
Executing default commands in the terminal Temporary moving the command execution logic to messenger.ts, need to implement this in controller.ts Disabling the Run command button Agentic IDE Terminal integration and show the logs in the terminal Agentic IDE Terminal integration
1 parent d35bb9e commit 0b9f81e

File tree

7 files changed

+212
-17
lines changed

7 files changed

+212
-17
lines changed

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui'
6+
import {
7+
ChatItem,
8+
ChatItemButton,
9+
ChatItemFormItem,
10+
ChatItemType,
11+
MynahUIDataModel,
12+
QuickActionCommand,
13+
} from '@aws/mynah-ui'
714
import { TabType } from '../storages/tabsStorage'
815
import { CWCChatItem } from '../connector'
916
import { BaseConnector, BaseConnectorProps } from './baseConnector'
@@ -18,12 +25,14 @@ export interface ConnectorProps extends BaseConnectorProps {
1825
title?: string,
1926
description?: string
2027
) => void
28+
onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
2129
}
2230

2331
export class Connector extends BaseConnector {
2432
private readonly onCWCContextCommandMessage
2533
private readonly onContextCommandDataReceived
2634
private readonly onShowCustomForm
35+
private readonly onChatAnswerUpdated
2736

2837
override getTabType(): TabType {
2938
return 'cwc'
@@ -34,6 +43,7 @@ export class Connector extends BaseConnector {
3443
this.onCWCContextCommandMessage = props.onCWCContextCommandMessage
3544
this.onContextCommandDataReceived = props.onContextCommandDataReceived
3645
this.onShowCustomForm = props.onShowCustomForm
46+
this.onChatAnswerUpdated = props.onChatAnswerUpdated
3747
}
3848

3949
onSourceLinkClick = (tabID: string, messageId: string, link: string): void => {
@@ -96,6 +106,7 @@ export class Connector extends BaseConnector {
96106
userIntent: messageData.userIntent,
97107
codeBlockLanguage: messageData.codeBlockLanguage,
98108
contextList: messageData.contextList,
109+
buttons: messageData.buttons ?? undefined,
99110
}
100111

101112
// If it is not there we will not set it
@@ -137,6 +148,7 @@ export class Connector extends BaseConnector {
137148
options: messageData.followUps,
138149
}
139150
: undefined,
151+
buttons: messageData.buttons ?? undefined,
140152
}
141153
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
142154

@@ -204,7 +216,7 @@ export class Connector extends BaseConnector {
204216
}
205217

206218
if (messageData.type === 'customFormActionMessage') {
207-
this.onCustomFormAction(messageData.tabID, messageData.action)
219+
this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action)
208220
return
209221
}
210222
// For other message types, call the base class handleMessageReceive
@@ -235,6 +247,7 @@ export class Connector extends BaseConnector {
235247

236248
onCustomFormAction(
237249
tabId: string,
250+
messageId: string,
238251
action: {
239252
id: string
240253
text?: string | undefined
@@ -248,9 +261,37 @@ export class Connector extends BaseConnector {
248261
this.sendMessageToExtension({
249262
command: 'form-action-click',
250263
action: action,
264+
formSelectedValues: action.formItemValues,
251265
tabType: this.getTabType(),
252266
tabID: tabId,
253267
})
268+
269+
if (this.onChatAnswerUpdated === undefined) {
270+
return
271+
}
272+
const answer: ChatItem = {
273+
type: ChatItemType.ANSWER,
274+
messageId: messageId,
275+
buttons: [],
276+
}
277+
278+
switch (action.id) {
279+
case 'RunCommand':
280+
answer.buttons = [
281+
{
282+
keepCardAfterClick: true,
283+
text: 'Executing Command',
284+
id: 'RunCommandClicked',
285+
status: 'success',
286+
position: 'outside',
287+
disabled: true,
288+
},
289+
]
290+
break
291+
default:
292+
break
293+
}
294+
this.onChatAnswerUpdated(tabId, answer)
254295
}
255296

256297
onFileClick = (tabID: string, filePath: string, messageId?: string) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ export class Connector {
711711
tabType: 'cwc',
712712
})
713713
} else {
714-
this.cwChatConnector.onCustomFormAction(tabId, action)
714+
this.cwChatConnector.onCustomFormAction(tabId, messageId ?? '', action)
715715
}
716716
break
717717
case 'agentWalkthrough': {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ export const createMynahUI = (
352352
...(item.followUp !== undefined ? { followUp: item.followUp } : {}),
353353
...(item.fileList !== undefined ? { fileList: item.fileList } : {}),
354354
...(item.header !== undefined ? { header: item.header } : { header: undefined }),
355+
...(item.buttons !== undefined ? { buttons: item.buttons } : { buttons: undefined }),
355356
})
356357
if (
357358
item.messageId !== undefined &&

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export class ChatSession {
1818
// TODO: doesn't handle the edge case when two files share the same relativePath string but from different root
1919
// e.g. root_a/file1 vs root_b/file1
2020
relativePathToWorkspaceRoot: Map<string, string> = new Map()
21+
22+
public storedBashCommands: string[] = []
23+
2124
public get sessionIdentifier(): string | undefined {
2225
return this.sessionId
2326
}

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

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
*/
55
import * as path from 'path'
66
import * as vscode from 'vscode'
7+
import * as os from 'os'
8+
import { fs } from '../../../shared/fs/fs'
9+
import { ChildProcess } from '../../../shared/utilities/processUtils'
710
import { Event as VSCodeEvent, Uri, workspace, window, ViewColumn, Position, Selection } from 'vscode'
811
import { EditorContextExtractor } from '../../editor/context/extractor'
912
import { ChatSessionStorage } from '../../storages/chatSession'
@@ -68,7 +71,6 @@ import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah
6871
import { LspClient } from '../../../amazonq/lsp/lspClient'
6972
import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types'
7073
import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants'
71-
import fs from '../../../shared/fs/fs'
7274
import { FeatureConfigProvider, Features } from '../../../shared/featureConfig'
7375
import { i18n } from '../../../shared/i18n-helper'
7476
import {
@@ -132,6 +134,8 @@ export interface ChatControllerMessageListeners {
132134
}
133135

134136
export class ChatController {
137+
// Store the last terminal output
138+
private lastTerminalOutput: string = ''
135139
private readonly sessionStorage: ChatSessionStorage
136140
private readonly triggerEventsStorage: TriggerEventsStorage
137141
private readonly messenger: Messenger
@@ -560,20 +564,99 @@ export class ChatController {
560564
}
561565

562566
private async processCustomFormAction(message: CustomFormActionMessage) {
563-
if (message.action.id === 'submit-create-prompt') {
564-
const userPromptsDirectory = getUserPromptsDirectory()
567+
const session = this.sessionStorage.getSession(message.tabID ?? '')
568+
if (!session) {
569+
getLogger().error(`No session found for tab: ${message.tabID ?? 'unknown'}`)
570+
return
571+
}
572+
if (message.action.id === 'RunCommand') {
573+
const terminals = vscode.window.terminals
574+
let terminal: vscode.Terminal
565575

566-
const title = message.action.formItemValues?.['prompt-name']
567-
const newFilePath = path.join(
568-
userPromptsDirectory,
569-
title ? `${title}${promptFileExtension}` : `default${promptFileExtension}`
570-
)
571-
const newFileContent = new Uint8Array(Buffer.from(''))
572-
await fs.writeFile(newFilePath, newFileContent)
573-
const newFileDoc = await vscode.workspace.openTextDocument(newFilePath)
574-
await vscode.window.showTextDocument(newFileDoc)
575-
telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' })
576+
if (terminals.length > 0) {
577+
terminal = terminals[0]
578+
} else {
579+
terminal = vscode.window.createTerminal('Amazon Q Terminal')
580+
}
581+
582+
terminal.show()
583+
584+
const command = session.storedBashCommands[0]
585+
586+
// Get the current path of the terminal
587+
const currentPath = await this.getCurrentTerminalPath(terminal)
588+
589+
let terminalOutput = ''
590+
591+
try {
592+
// Execute the command in the terminal's current directory
593+
const childProcess = new ChildProcess('bash', ['-c', command], {
594+
spawnOptions: { cwd: currentPath },
595+
collect: true,
596+
})
597+
598+
const result = await childProcess.run()
599+
600+
if (result.exitCode !== 0 || result.error) {
601+
const errorMessage = result.error ? result.error.message : result.stderr
602+
getLogger().error(`Error executing command: ${errorMessage}`)
603+
terminal.sendText(`echo "Error executing command: ${errorMessage}"`)
604+
this.lastTerminalOutput = `Error: ${errorMessage}`
605+
} else {
606+
terminalOutput = result.stdout.trim()
607+
terminal.sendText(command)
608+
getLogger().info(`Command executed: ${command}`)
609+
getLogger().info(`Command output: ${terminalOutput}`)
610+
this.lastTerminalOutput = terminalOutput
611+
}
612+
} catch (error) {
613+
const errorMessage = error instanceof Error ? error.message : String(error)
614+
getLogger().error(`Error executing command: ${errorMessage}`)
615+
terminal.sendText(`echo "Error executing command: ${errorMessage}"`)
616+
this.lastTerminalOutput = `Error: ${errorMessage}`
617+
}
576618
}
619+
getLogger().error(`Last terminal output: ${this.getLastTerminalOutput()}`)
620+
this.messenger.sendMessage(this.getLastTerminalOutput(), message.tabID ?? '', message.tabID ?? 'unknown')
621+
session.storedBashCommands = []
622+
}
623+
624+
private async getCurrentTerminalPath(terminal: vscode.Terminal): Promise<string> {
625+
try {
626+
// Get the current workspace folders
627+
const workspaceFolders = vscode.workspace.workspaceFolders
628+
629+
// Try to get the terminal's creation options
630+
// We need to use type assertion since the API types might not expose cwd directly
631+
const options = terminal.creationOptions as any
632+
if (options) {
633+
// Check if cwd exists in the options
634+
if (options.cwd) {
635+
if (typeof options.cwd === 'string') {
636+
return options.cwd
637+
} else if (options.cwd instanceof vscode.Uri) {
638+
return options.cwd.fsPath
639+
}
640+
}
641+
}
642+
643+
// If there's an active workspace folder, use its path
644+
if (workspaceFolders && workspaceFolders.length > 0) {
645+
const activeWorkspace = workspaceFolders[0]
646+
return activeWorkspace.uri.fsPath
647+
}
648+
649+
// Fallback to user's home directory
650+
return os.homedir()
651+
} catch (err) {
652+
getLogger().error(`Failed to get terminal path: ${err}`)
653+
return os.homedir()
654+
}
655+
}
656+
657+
// Get the last terminal output
658+
public getLastTerminalOutput(): string {
659+
return this.lastTerminalOutput
577660
}
578661

579662
private async processContextSelected(message: ContextSelectedMessage) {

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

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { LspController } from '../../../../amazonq/lsp/lspController'
3737
import { extractCodeBlockLanguage } from '../../../../shared/markdown'
3838
import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils'
3939
import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants'
40-
import { ChatItemButton, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui'
40+
import { ChatItemButton, ChatItemFormItem, MynahIcons, MynahUIDataModel } from '@aws/mynah-ui'
4141

4242
export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help'
4343

@@ -313,6 +313,50 @@ export class Messenger {
313313
)
314314
}
315315

316+
if (message.includes('```bash')) {
317+
let bashCommand: string | undefined
318+
try {
319+
const bashRegex = /```bash\s*([\s\S]*?)```/
320+
const match = message.match(bashRegex)
321+
if (match && match[1]) {
322+
bashCommand = match[1].trim()
323+
session.storedBashCommands.push(bashCommand)
324+
getLogger().info(`Extracted bash command: ${bashCommand}`)
325+
}
326+
327+
const buttons: ChatItemButton[] = []
328+
buttons.push({
329+
keepCardAfterClick: true,
330+
text: 'Run the bash command in terminal',
331+
id: 'RunCommand',
332+
disabled: false, // allow button to be re-clicked
333+
position: 'outside',
334+
icon: 'comment' as MynahIcons,
335+
})
336+
337+
this.dispatcher.sendChatMessage(
338+
new ChatMessage(
339+
{
340+
message: message,
341+
messageType: 'answer-part',
342+
followUps: followUps,
343+
followUpsHeader: undefined,
344+
relatedSuggestions: undefined,
345+
triggerID,
346+
messageID,
347+
userIntent: triggerPayload.userIntent,
348+
codeBlockLanguage: undefined,
349+
contextList: undefined,
350+
buttons,
351+
},
352+
tabID
353+
)
354+
)
355+
} catch (error) {
356+
const errorMessage = error instanceof Error ? error.message : String(error)
357+
getLogger().error(`Error executing command: ${errorMessage}`)
358+
}
359+
}
316360
this.dispatcher.sendChatMessage(
317361
new ChatMessage(
318362
{
@@ -365,6 +409,26 @@ export class Messenger {
365409
)
366410
}
367411

412+
public sendMessage(message: string | undefined, messageID: string, tabID: string) {
413+
this.dispatcher.sendChatMessage(
414+
new ChatMessage(
415+
{
416+
message: message,
417+
messageType: 'answer',
418+
followUps: [],
419+
followUpsHeader: undefined,
420+
relatedSuggestions: undefined,
421+
triggerID: '',
422+
messageID,
423+
userIntent: undefined,
424+
codeBlockLanguage: undefined,
425+
contextList: undefined,
426+
},
427+
tabID
428+
)
429+
)
430+
}
431+
368432
private editorContextMenuCommandVerbs: Map<EditorContextCommandType, string> = new Map([
369433
['aws.amazonq.explainCode', 'Explain'],
370434
['aws.amazonq.explainIssue', 'Explain'],

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export interface ChatMessageProps {
208208
readonly userIntent: string | undefined
209209
readonly codeBlockLanguage: string | undefined
210210
readonly contextList: DocumentReference[] | undefined
211+
readonly buttons?: ChatItemButton[] | undefined
211212
}
212213

213214
export class ChatMessage extends UiMessage {
@@ -223,6 +224,7 @@ export class ChatMessage extends UiMessage {
223224
readonly userIntent: string | undefined
224225
readonly codeBlockLanguage: string | undefined
225226
readonly contextList: DocumentReference[] | undefined
227+
readonly buttons?: ChatItemButton[] | undefined
226228
override type = 'chatMessage'
227229

228230
constructor(props: ChatMessageProps, tabID: string) {
@@ -238,6 +240,7 @@ export class ChatMessage extends UiMessage {
238240
this.userIntent = props.userIntent
239241
this.codeBlockLanguage = props.codeBlockLanguage
240242
this.contextList = props.contextList
243+
this.buttons = props.buttons
241244
}
242245
}
243246

0 commit comments

Comments
 (0)