Skip to content

Commit 087f338

Browse files
authored
feat(chat-client): add shortcut for stop/reject/run commands (aws#1932)
* feat(amazonq): add keyboard shortcut for run/reject/stop shell commands * feat(amazonq): add feature flag for keyboard shortcut and update key for run * fix: update package lock * fix: remove debug logs * feat: add back reject shortcut keyboard * fix: update keybind and resolve interface for KeyBinding to make it for general usage * fix: dont support user change default keybind for p0 * feat: add tooltip to Stop button on chat-item-card * fix: remove invalid interface * chore: clean up codes * fix: add type to executeShellCommandShortCut() params * fix: make shift shortcut text to icon instead * fix: update type name and UI for tooltip * chore: bump to the latest mynah-ui and lsr version * fix: bump up LSR version and change string to enum for executeShellCommandShortcut * fix(chat-client): properly stop chat through shortcut, make shortcut only available if focus
1 parent 3362956 commit 087f338

File tree

7 files changed

+150
-37
lines changed

7 files changed

+150
-37
lines changed

chat-client/src/client/chat.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import {
107107
OpenFileDialogParams,
108108
OPEN_FILE_DIALOG_METHOD,
109109
OpenFileDialogResult,
110+
EXECUTE_SHELL_COMMAND_SHORTCUT_METHOD,
110111
} from '@aws/language-server-runtimes-types'
111112
import { ConfigTexts, MynahUIDataModel, MynahUITabStoreModel } from '@aws/mynah-ui'
112113
import { ServerMessage, TELEMETRY, TelemetryParams } from '../contracts/serverContracts'
@@ -130,6 +131,7 @@ type ChatClientConfig = Pick<MynahUIDataModel, 'quickActionCommands'> & {
130131
agenticMode?: boolean
131132
modelSelectionEnabled?: boolean
132133
stringOverrides?: Partial<ConfigTexts>
134+
os?: string
133135
}
134136

135137
export const createChat = (
@@ -183,6 +185,9 @@ export const createChat = (
183185
}
184186

185187
switch (message?.command) {
188+
case EXECUTE_SHELL_COMMAND_SHORTCUT_METHOD:
189+
mynahApi.executeShellCommandShortCut(message.params)
190+
break
186191
case CHAT_REQUEST_METHOD:
187192
mynahApi.addChatResponse(message.params, message.tabId, message.isPartialResult)
188193
break
@@ -537,7 +542,8 @@ export const createChat = (
537542
chatClientAdapter,
538543
featureConfig,
539544
!!config?.agenticMode,
540-
config?.stringOverrides
545+
config?.stringOverrides,
546+
config?.os
541547
)
542548

543549
mynahApi = api

chat-client/src/client/mynahUi.ts

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
RuleClickResult,
3838
SourceLinkClickParams,
3939
ListAvailableModelsResult,
40+
ExecuteShellCommandParams,
4041
} from '@aws/language-server-runtimes-types'
4142
import {
4243
ChatItem,
@@ -94,6 +95,7 @@ export interface InboundChatApi {
9495
openTab(requestId: string, params: OpenTabParams): void
9596
sendContextCommands(params: ContextCommandParams): void
9697
listConversations(params: ListConversationsResult): void
98+
executeShellCommandShortCut(params: ExecuteShellCommandParams): void
9799
listRules(params: ListRulesResult): void
98100
conversationClicked(params: ConversationClickResult): void
99101
ruleClicked(params: RuleClickResult): void
@@ -313,7 +315,8 @@ export const createMynahUi = (
313315
customChatClientAdapter?: ChatClientAdapter,
314316
featureConfig?: Map<string, any>,
315317
agenticMode?: boolean,
316-
stringOverrides?: Partial<ConfigTexts>
318+
stringOverrides?: Partial<ConfigTexts>,
319+
os?: string
317320
): [MynahUI, InboundChatApi] => {
318321
let disclaimerCardActive = !disclaimerAcknowledged
319322
let programmingModeCardActive = !pairProgrammingCardAcknowledged
@@ -690,24 +693,7 @@ export const createMynahUi = (
690693
}
691694
},
692695
onStopChatResponse: tabId => {
693-
messager.onStopChatResponse(tabId)
694-
695-
// Reset loading state
696-
mynahUi.updateStore(tabId, {
697-
loadingChat: false,
698-
cancelButtonWhenLoading: true,
699-
promptInputDisabledState: false,
700-
})
701-
702-
// Add a small delay before adding the chat item
703-
setTimeout(() => {
704-
// Add cancellation message when stop button is clicked
705-
mynahUi.addChatItem(tabId, {
706-
type: ChatItemType.DIRECTIVE,
707-
messageId: 'stopped' + Date.now(),
708-
body: 'You stopped your current work, please provide additional examples or ask another question.',
709-
})
710-
}, 500) // 500ms delay
696+
handleUIStopChatResponse(messager, mynahUi, tabId)
711697
},
712698
onOpenFileDialogClick: (tabId, fileType, insertPosition) => {
713699
const imageContext = getImageContextCount(tabId)
@@ -818,6 +804,7 @@ export const createMynahUi = (
818804
dragOverlayText: 'Add image to context',
819805
// Fallback to original texts in non-agentic chat mode
820806
stopGenerating: agenticMode ? uiComponentsTexts.stopGenerating : 'Stop generating',
807+
stopGeneratingTooltip: getStopGeneratingToolTipText(os, agenticMode),
821808
spinnerText: agenticMode ? uiComponentsTexts.spinnerText : 'Generating your answer...',
822809
...stringOverrides,
823810
},
@@ -1459,6 +1446,55 @@ ${params.message}`,
14591446
messager.onError(params)
14601447
}
14611448

1449+
const executeShellCommandShortCut = (params: ExecuteShellCommandParams) => {
1450+
const activeElement = document.activeElement as HTMLElement
1451+
1452+
const tabId = mynahUi.getSelectedTabId()
1453+
if (!tabId) return
1454+
1455+
const chatItems = mynahUi.getTabData(tabId)?.getStore()?.chatItems || []
1456+
const buttonId = params.id
1457+
1458+
let messageId
1459+
for (const item of chatItems) {
1460+
if (buttonId === 'stop-shell-command' && item.buttons && item.buttons.some(b => b.id === buttonId)) {
1461+
messageId = item.messageId
1462+
break
1463+
}
1464+
if (item.header?.buttons && item.header.buttons.some(b => b.id === buttonId)) {
1465+
messageId = item.messageId
1466+
break
1467+
}
1468+
}
1469+
1470+
if (messageId) {
1471+
const payload: ButtonClickParams = {
1472+
tabId,
1473+
messageId,
1474+
buttonId,
1475+
}
1476+
messager.onButtonClick(payload)
1477+
if (buttonId === 'stop-shell-command') {
1478+
handleUIStopChatResponse(messager, mynahUi, tabId)
1479+
}
1480+
} else {
1481+
// handle global stop
1482+
const isLoading = mynahUi.getTabData(tabId)?.getStore()?.loadingChat
1483+
if (isLoading && buttonId === 'stop-shell-command') {
1484+
handleUIStopChatResponse(messager, mynahUi, tabId)
1485+
}
1486+
}
1487+
// this is a short-term solution to re-gain focus after executing a shortcut
1488+
// current behavior will emit exitFocus telemetry immediadately.
1489+
// use this to re-gain focus, so that user can use shortcut after shortcut
1490+
// without manually re-gain focus.
1491+
setTimeout(() => {
1492+
if (activeElement && activeElement.focus) {
1493+
activeElement.focus()
1494+
}
1495+
}, 100)
1496+
}
1497+
14621498
const openTab = (requestId: string, params: OpenTabParams) => {
14631499
if (params.tabId) {
14641500
if (params.tabId !== mynahUi.getSelectedTabId()) {
@@ -1684,6 +1720,7 @@ ${params.message}`,
16841720
openTab: openTab,
16851721
sendContextCommands: sendContextCommands,
16861722
sendPinnedContext: sendPinnedContext,
1723+
executeShellCommandShortCut: executeShellCommandShortCut,
16871724
listConversations: listConversations,
16881725
listRules: listRules,
16891726
conversationClicked: conversationClicked,
@@ -1730,4 +1767,37 @@ export const uiComponentsTexts = {
17301767
noMoreTabsTooltip: 'You can only open ten conversation tabs at a time.',
17311768
codeSuggestionWithReferenceTitle: 'Some suggestions contain code with references.',
17321769
spinnerText: 'Working...',
1770+
macStopButtonShortcut: '&#8679; &#8984; &#9003;',
1771+
windowStopButtonShortcut: 'Ctrl + &#8679; + &#9003;',
1772+
}
1773+
1774+
const getStopGeneratingToolTipText = (os: string | undefined, agenticMode: boolean | undefined): string => {
1775+
if (agenticMode && os) {
1776+
return os === 'darwin'
1777+
? `Stop: ${uiComponentsTexts.macStopButtonShortcut}`
1778+
: `Stop: ${uiComponentsTexts.windowStopButtonShortcut}`
1779+
}
1780+
1781+
return agenticMode ? uiComponentsTexts.stopGenerating : 'Stop generating'
1782+
}
1783+
1784+
const handleUIStopChatResponse = (messenger: Messager, mynahUi: MynahUI, tabId: string) => {
1785+
messenger.onStopChatResponse(tabId)
1786+
1787+
// Reset loading state
1788+
mynahUi.updateStore(tabId, {
1789+
loadingChat: false,
1790+
cancelButtonWhenLoading: true,
1791+
promptInputDisabledState: false,
1792+
})
1793+
1794+
// Add a small delay before adding the chat item
1795+
setTimeout(() => {
1796+
// Add cancellation message when stop button is clicked
1797+
mynahUi.addChatItem(tabId, {
1798+
type: ChatItemType.DIRECTIVE,
1799+
messageId: 'stopped' + Date.now(),
1800+
body: 'You stopped your current work, please provide additional examples or ask another question.',
1801+
})
1802+
}, 500) // 500ms delay
17331803
}

client/vscode/src/activation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as cp from 'child_process'
77
import * as path from 'path'
8+
import * as os from 'os'
89

910
import { ExtensionContext, env, version } from 'vscode'
1011

@@ -147,13 +148,15 @@ export async function activateDocumentsLanguageServer(extensionContext: Extensio
147148
const enableChat = process.env.ENABLE_CHAT === 'true'
148149
const agenticMode = process.env.ENABLE_AGENTIC_UI_MODE === 'true'
149150
const modelSelectionEnabled = process.env.ENABLE_MODEL_SELECTION === 'true'
151+
const osPlatform = os.platform()
150152
if (enableChat) {
151153
registerChat(
152154
client,
153155
extensionContext.extensionUri,
154156
enableEncryptionInit ? encryptionKey : undefined,
155157
agenticMode,
156-
modelSelectionEnabled
158+
modelSelectionEnabled,
159+
osPlatform
157160
)
158161
}
159162

client/vscode/src/chatActivation.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
CHAT_OPTIONS,
77
COPY_TO_CLIPBOARD,
88
UiMessageResultParams,
9-
OPEN_FILE_DIALOG,
109
} from '@aws/chat-client-ui-types'
1110
import {
1211
ChatResult,
@@ -37,8 +36,6 @@ import {
3736
chatUpdateNotificationType,
3837
listRulesRequestType,
3938
ruleClickRequestType,
40-
openFileDialogRequestType,
41-
OPEN_FILE_DIALOG_METHOD,
4239
} from '@aws/language-server-runtimes/protocol'
4340
import { v4 as uuidv4 } from 'uuid'
4441
import { Uri, Webview, WebviewView, commands, window } from 'vscode'
@@ -58,7 +55,8 @@ export function registerChat(
5855
extensionUri: Uri,
5956
encryptionKey?: Buffer,
6057
agenticMode?: boolean,
61-
modelSelectionEnabled?: boolean
58+
modelSelectionEnabled?: boolean,
59+
os?: string
6260
) {
6361
const webviewInitialized: Promise<Webview> = new Promise(resolveWebview => {
6462
const provider = {
@@ -71,8 +69,7 @@ export function registerChat(
7169
resolveWebview(webviewView.webview)
7270

7371
webviewView.webview.onDidReceiveMessage(async message => {
74-
languageClient.info(`[VSCode Client] Received ${JSON.stringify(message)} from chat`)
75-
72+
languageClient.info(`[VSCode Client] Received ${JSON.stringify(message)}`)
7673
switch (message.command) {
7774
case COPY_TO_CLIPBOARD:
7875
languageClient.info('[VSCode Client] Copy to clipboard event received')
@@ -301,7 +298,8 @@ export function registerChat(
301298
webviewView.webview,
302299
extensionUri,
303300
!!agenticMode,
304-
!!modelSelectionEnabled
301+
!!modelSelectionEnabled,
302+
os!
305303
)
306304

307305
registerGenericCommand('aws.sample-vscode-ext-amazonq.explainCode', 'Explain', webviewView.webview)
@@ -435,7 +433,13 @@ async function handleRequest(
435433
})
436434
}
437435

438-
function getWebviewContent(webView: Webview, extensionUri: Uri, agenticMode: boolean, modelSelectionEnabled: boolean) {
436+
function getWebviewContent(
437+
webView: Webview,
438+
extensionUri: Uri,
439+
agenticMode: boolean,
440+
modelSelectionEnabled: boolean,
441+
os: string
442+
) {
439443
return `
440444
<!DOCTYPE html>
441445
<html lang="en">
@@ -446,7 +450,7 @@ function getWebviewContent(webView: Webview, extensionUri: Uri, agenticMode: boo
446450
${generateCss()}
447451
</head>
448452
<body>
449-
${generateJS(webView, extensionUri, agenticMode, modelSelectionEnabled)}
453+
${generateJS(webView, extensionUri, agenticMode, modelSelectionEnabled, os)}
450454
</body>
451455
</html>`
452456
}
@@ -467,7 +471,13 @@ function generateCss() {
467471
</style>`
468472
}
469473

470-
function generateJS(webView: Webview, extensionUri: Uri, agenticMode: boolean, modelSelectionEnabled: boolean): string {
474+
function generateJS(
475+
webView: Webview,
476+
extensionUri: Uri,
477+
agenticMode: boolean,
478+
modelSelectionEnabled: boolean,
479+
os: string
480+
): string {
471481
const assetsPath = Uri.joinPath(extensionUri)
472482
const chatUri = Uri.joinPath(assetsPath, 'build', 'amazonq-ui.js')
473483

@@ -486,7 +496,7 @@ function generateJS(webView: Webview, extensionUri: Uri, agenticMode: boolean, m
486496
<script type="text/javascript">
487497
const init = () => {
488498
amazonQChat.createChat(acquireVsCodeApi(),
489-
{disclaimerAcknowledged: false, agenticMode: ${!!agenticMode}, modelSelectionEnabled: ${!!modelSelectionEnabled}},
499+
{disclaimerAcknowledged: false, agenticMode: ${!!agenticMode}, modelSelectionEnabled: ${!!modelSelectionEnabled}, os: "${os}"},
490500
undefined,
491501
JSON.stringify(${stringifiedContextCommands})
492502
);

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2250,7 +2250,7 @@ export class AgenticChatController implements ChatHandlers {
22502250

22512251
const initialHeader: ChatMessage['header'] = {
22522252
body: 'shell',
2253-
buttons: [{ id: BUTTON_STOP_SHELL_COMMAND, text: 'Stop', icon: 'stop' }],
2253+
buttons: [this.#renderStopShellCommandButton()],
22542254
}
22552255

22562256
const completedHeader: ChatMessage['header'] = {
@@ -2327,7 +2327,7 @@ export class AgenticChatController implements ChatHandlers {
23272327
text: 'Rejected',
23282328
},
23292329
}),
2330-
buttons: isAccept ? [{ id: BUTTON_STOP_SHELL_COMMAND, text: 'Stop', icon: 'stop' }] : [],
2330+
buttons: isAccept ? [this.#renderStopShellCommandButton()] : [],
23312331
},
23322332
}
23332333
}
@@ -2503,7 +2503,7 @@ export class AgenticChatController implements ChatHandlers {
25032503
#renderStopShellCommandButton() {
25042504
const stopKey = this.#getKeyBinding('aws.amazonq.stopCmdExecution')
25052505
return {
2506-
id: 'stop-shell-command',
2506+
id: BUTTON_STOP_SHELL_COMMAND,
25072507
text: 'Stop',
25082508
icon: 'stop',
25092509
...(stopKey ? { description: `Stop: ${stopKey}` } : {}),
@@ -2574,14 +2574,28 @@ export class AgenticChatController implements ChatHandlers {
25742574
switch (toolName) {
25752575
case EXECUTE_BASH: {
25762576
const commandString = (toolUse.input as unknown as ExecuteBashParams).command
2577+
// get feature flag
2578+
const shortcut =
2579+
this.#features.lsp.getClientInitializeParams()?.initializationOptions?.aws?.awsClientCapabilities?.q
2580+
?.shortcut
2581+
2582+
const runKey = this.#getKeyBinding('aws.amazonq.runCmdExecution')
2583+
const rejectKey = this.#getKeyBinding('aws.amazonq.rejectCmdExecution')
2584+
25772585
buttons = requiresAcceptance
25782586
? [
2579-
{ id: BUTTON_RUN_SHELL_COMMAND, text: 'Run', icon: 'play' },
2587+
{
2588+
id: BUTTON_RUN_SHELL_COMMAND,
2589+
text: 'Run',
2590+
icon: 'play',
2591+
...(runKey ? { description: `Run: ${runKey}` } : {}),
2592+
},
25802593
{
25812594
id: BUTTON_REJECT_SHELL_COMMAND,
25822595
status: 'dimmed-clear' as Status,
25832596
text: 'Reject',
25842597
icon: 'cancel',
2598+
...(rejectKey ? { description: `Reject: ${rejectKey}` } : {}),
25852599
},
25862600
]
25872601
: []

0 commit comments

Comments
 (0)