From 30166162f3749eae76754269627e79b096d28c71 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Mon, 17 Feb 2025 12:49:42 -0800 Subject: [PATCH 01/40] feat(amazonq): add context command selection to Q chat (#5) * local ws working * beta_2 * rename * update * code format * apply context cmd to all tabs * context to the api call * minimize changes * update * update * format code * update to lsp * update context commands * update context commands * clean up * beta5 * update * use lsp to resolve additionalContext --- package-lock.json | 7 +- packages/core/package.json | 2 +- packages/core/src/amazonq/index.ts | 2 +- packages/core/src/amazonq/lsp/lspClient.ts | 98 ++++++++++++++++- packages/core/src/amazonq/lsp/types.ts | 39 +++++++ .../webview/messages/messageDispatcher.ts | 2 + .../amazonq/webview/ui/apps/baseConnector.ts | 4 +- .../webview/ui/apps/cwChatConnector.ts | 16 ++- .../core/src/amazonq/webview/ui/connector.ts | 4 + packages/core/src/amazonq/webview/ui/main.ts | 20 +++- .../amazonq/webview/ui/messages/handler.ts | 2 +- .../core/src/codewhisperer/util/gitUtil.ts | 3 +- packages/core/src/codewhispererChat/app.ts | 7 ++ .../commands/registerCommands.ts | 3 + .../controllers/chat/chatRequest/converter.ts | 2 +- .../controllers/chat/controller.ts | 103 ++++++++++++++++++ .../controllers/chat/messenger/messenger.ts | 6 + .../controllers/chat/model.ts | 6 +- .../view/connector/connector.ts | 14 +++ .../view/messages/messageListener.ts | 9 ++ 20 files changed, 331 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72722618604..4fb9f8a7d81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4935,9 +4935,10 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.22.1", + "version": "4.23.0-beta.6", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.6.tgz", + "integrity": "sha512-wiMKJKhx9BdI4LWXCchjBNZaIFrccZqvjQ7XXFUZ5iWrZMOzTS6soeJ7J/S/3KLIHDfZmR3/v68RvzDIlgG2sw==", "hasInstallScript": true, - "license": "Apache License 2.0", "dependencies": { "escape-html": "^1.0.3", "highlight.js": "^11.11.0", @@ -18922,7 +18923,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.22.1", + "@aws/mynah-ui": "^4.23.0-beta.5", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index 993c14a218f..6b68badb3b6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -510,7 +510,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.22.1", + "@aws/mynah-ui": "^4.23.0-beta.5", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index b42a0c20a39..9ca9af7687c 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -43,7 +43,7 @@ export { ExtensionMessage } from '../amazonq/webview/ui/commands' export { CodeReference } from '../codewhispererChat/view/connector/connector' export { extractAuthFollowUp } from './util/authUtils' export { Messenger } from './commons/connector/baseMessenger' -import { FeatureContext } from '../shared/featureConfig' +import { FeatureContext } from '../shared' /** * main from createMynahUI is a purely browser dependency. Due to this diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index b5b506818d8..7797f5eb960 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -29,12 +29,18 @@ import { QueryRepomapIndexRequestType, GetRepomapIndexJSONRequestType, Usage, + GetContextCommandItemsRequestType, + ContextCommandItem, + GetIndexSequenceNumberRequestType, + GetContextCommandPromptRequestType, + AdditionalContextPrompt, } from './types' import { Writable } from 'stream' import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings' import { fs } from '../../shared/fs/fs' import { getLogger } from '../../shared/logger/logger' import globals from '../../shared/extensionGlobals' +import { waitUntil } from '../../shared' const localize = nls.loadMessageBundle() @@ -168,6 +174,66 @@ export class LspClient { throw e } } + + async getContextCommandItems(): Promise { + try { + const workspaceFolders = vscode.workspace.workspaceFolders || [] + const request = JSON.stringify({ + workspaceFolders: workspaceFolders.map((it) => it.uri.fsPath), + }) + const resp: any = await this.client?.sendRequest( + GetContextCommandItemsRequestType, + await this.encrypt(request) + ) + return resp + } catch (e) { + getLogger().error(`LspClient: getContextCommandItems error: ${e}`) + throw e + } + } + + async getContextCommandPrompt(contextCommandItems: ContextCommandItem[]): Promise { + try { + const request = JSON.stringify({ + contextCommands: contextCommandItems, + }) + const resp: any = await this.client?.sendRequest( + GetContextCommandPromptRequestType, + await this.encrypt(request) + ) + return resp + } catch (e) { + getLogger().error(`LspClient: getContextCommandPrompt error: ${e}`) + throw e + } + } + + async getIndexSequenceNumber(): Promise { + try { + const request = JSON.stringify({}) + const resp: any = await this.client?.sendRequest( + GetIndexSequenceNumberRequestType, + await this.encrypt(request) + ) + return resp + } catch (e) { + getLogger().error(`LspClient: getIndexSequenceNumber error: ${e}`) + throw e + } + } + + async waitUtilReady() { + return waitUntil( + async () => { + if (this.client === undefined) { + return false + } + await this.client.onReady() + return true + }, + { interval: 500, timeout: 60_000 * 3, truthy: true } + ) + } } /** * Activates the language server, this will start LSP server running over IPC protocol. @@ -261,17 +327,41 @@ export async function activate(extensionContext: ExtensionContext) { void LspClient.instance.updateIndex([savedDocument.fsPath], 'update') } }), - vscode.workspace.onDidCreateFiles((e) => { - void LspClient.instance.updateIndex( + vscode.workspace.onDidCreateFiles(async (e) => { + const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() + await LspClient.instance.updateIndex( e.files.map((f) => f.fsPath), 'add' ) + await waitUntil( + async () => { + const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() + if (newIndexSeqNum > indexSeqNum) { + await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) + return true + } + return false + }, + { interval: 500, timeout: 10_000, truthy: true } + ) }), - vscode.workspace.onDidDeleteFiles((e) => { - void LspClient.instance.updateIndex( + vscode.workspace.onDidDeleteFiles(async (e) => { + const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() + await LspClient.instance.updateIndex( e.files.map((f) => f.fsPath), 'remove' ) + await waitUntil( + async () => { + const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() + if (newIndexSeqNum > indexSeqNum) { + await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) + return true + } + return false + }, + { interval: 500, timeout: 10_000, truthy: true } + ) }) ) diff --git a/packages/core/src/amazonq/lsp/types.ts b/packages/core/src/amazonq/lsp/types.ts index 3af943cb97d..1020dd24ebc 100644 --- a/packages/core/src/amazonq/lsp/types.ts +++ b/packages/core/src/amazonq/lsp/types.ts @@ -76,3 +76,42 @@ export type GetRepomapIndexJSONRequest = string export const GetRepomapIndexJSONRequestType: RequestType = new RequestType( 'lsp/getRepomapIndexJSON' ) + +export type GetContextCommandItemsRequestPayload = { workspaceFolders: string[] } +export type GetContextCommandItemsRequest = string +export const GetContextCommandItemsRequestType: RequestType = new RequestType( + 'lsp/getContextCommandItems' +) + +export type GetIndexSequenceNumberRequestPayload = {} +export type GetIndexSequenceNumberRequest = string +export const GetIndexSequenceNumberRequestType: RequestType = new RequestType( + 'lsp/getIndexSequenceNumber' +) + +export type ContextCommandItemType = 'file' | 'folder' + +export interface ContextCommandItem { + workspaceFolder: string + type: ContextCommandItemType + relativePath: string +} + +export type GetContextCommandPromptRequestPayload = { + contextCommands: { + workspaceFolder: string + type: 'file' | 'folder' + relativePath: string + }[] +} +export type GetContextCommandPromptRequest = string +export const GetContextCommandPromptRequestType: RequestType = + new RequestType('lsp/getContextCommandPrompt') + +export interface AdditionalContextPrompt { + content: string + name: string + description: string + startLine: number + endLine: number +} diff --git a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts index 483b4525298..cf24a6a2324 100644 --- a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts +++ b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts @@ -36,6 +36,8 @@ export function dispatchWebViewMessagesToApps( }) performance.clearMarks(amazonqMark.uiReady) performance.clearMarks(amazonqMark.open) + // let cwcController know the ui is ready + webViewToAppsMessagePublishers.get('cwc')?.publish(msg) return } case 'start-chat-message-telemetry': { diff --git a/packages/core/src/amazonq/webview/ui/apps/baseConnector.ts b/packages/core/src/amazonq/webview/ui/apps/baseConnector.ts index e28c599d910..3d5cdd71473 100644 --- a/packages/core/src/amazonq/webview/ui/apps/baseConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/baseConnector.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItem, ChatItemAction, ChatItemType, FeedbackPayload } from '@aws/mynah-ui' +import { ChatItem, ChatItemAction, ChatItemType, FeedbackPayload, QuickActionCommand } from '@aws/mynah-ui' import { ExtensionMessage } from '../commands' import { CodeReference } from './amazonqCommonsConnector' import { TabOpenType, TabsStorage, TabType } from '../storages/tabsStorage' @@ -13,6 +13,7 @@ import { CWCChatItem } from '../connector' interface ChatPayload { chatMessage: string chatCommand?: string + chatContext?: string[] | QuickActionCommand[] } export interface BaseConnectorProps { @@ -212,6 +213,7 @@ export abstract class BaseConnector { command: 'chat-prompt', chatMessage: payload.chatMessage, chatCommand: payload.chatCommand, + chatContext: payload.chatContext, tabType: this.getTabType(), }) }) diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 58f525857a8..bb3dd3327b5 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -3,17 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemType } from '@aws/mynah-ui' +import { ChatItemType, MynahUIDataModel } from '@aws/mynah-ui' import { TabType } from '../storages/tabsStorage' import { CWCChatItem } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' export interface ConnectorProps extends BaseConnectorProps { onCWCContextCommandMessage: (message: CWCChatItem, command?: string) => string | undefined + onContextCommandDataReceived: (data: MynahUIDataModel['contextCommands']) => void } export class Connector extends BaseConnector { private readonly onCWCContextCommandMessage + private readonly onContextCommandDataReceived override getTabType(): TabType { return 'cwc' @@ -22,6 +24,7 @@ export class Connector extends BaseConnector { constructor(props: ConnectorProps) { super(props) this.onCWCContextCommandMessage = props.onCWCContextCommandMessage + this.onContextCommandDataReceived = props.onContextCommandDataReceived } onSourceLinkClick = (tabID: string, messageId: string, link: string): void => { @@ -131,6 +134,12 @@ export class Connector extends BaseConnector { } } + processContextCommandData(messageData: any) { + if (messageData.data) { + this.onContextCommandDataReceived(messageData.data) + } + } + handleMessageReceive = async (messageData: any): Promise => { if (messageData.type === 'chatMessage') { await this.processChatMessage(messageData) @@ -141,7 +150,10 @@ export class Connector extends BaseConnector { await this.processEditorContextCommandMessage(messageData) return } - + if (messageData.type === 'contextCommandData') { + await this.processContextCommandData(messageData) + return + } // For other message types, call the base class handleMessageReceive await this.baseHandleMessageReceive(messageData) } diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 900c9a6de96..3ac0a4c0f55 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -12,6 +12,8 @@ import { ProgressField, ReferenceTrackerInformation, ChatPrompt, + MynahUIDataModel, + QuickActionCommand, } from '@aws/mynah-ui' import { Connector as CWChatConnector } from './apps/cwChatConnector' import { Connector as FeatureDevChatConnector } from './apps/featureDevChatConnector' @@ -50,6 +52,7 @@ export interface UploadHistory { export interface ChatPayload { chatMessage: string chatCommand?: string + chatContext?: string[] | QuickActionCommand[] | undefined } // Adding userIntent param by extending ChatItem to send userIntent as part of amazonq_interactWithMessage telemetry event @@ -87,6 +90,7 @@ export interface ConnectorProps { onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void handleCommand: (chatPrompt: ChatPrompt, tabId: string) => void sendStaticMessages: (tabID: string, messages: ChatItem[]) => void + onContextCommandDataReceived: (message: MynahUIDataModel['contextCommands']) => void tabsStorage: TabsStorage } diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 1c940c6bea1..0a93dbf310f 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -55,6 +55,8 @@ export const createMynahUI = ( // Store the mapping between messageId and messageUserIntent for amazonq_interactWithMessage telemetry const responseMetadata = new Map() + let savedContextCommands: MynahUIDataModel['contextCommands'] = [] + window.addEventListener('error', (e) => { const { error, message } = e ideApi.postMessage({ @@ -503,7 +505,6 @@ export const createMynahUI = ( if (!newTabID) { return } - tabsStorage.updateTabTypeFromUnknown(newTabID, tabType) connector.onKnownTabOpen(newTabID) connector.onUpdateTabType(newTabID) @@ -557,6 +558,17 @@ export const createMynahUI = ( mynahUI.addChatItem(tabID, message) } }, + onContextCommandDataReceived(data: MynahUIDataModel['contextCommands']) { + savedContextCommands = data + for (const tabID in mynahUI.getAllTabs()) { + const tabType = tabsStorage.getTab(tabID)?.type || '' + if (['cwc', 'unknown', 'welcome'].includes(tabType)) { + mynahUI.updateStore(tabID, { + contextCommands: savedContextCommands, + }) + } + } + }, }) mynahUI = new MynahUI({ @@ -587,6 +599,12 @@ export const createMynahUI = ( quickActionCommands: tabDataGenerator.quickActionsGenerator.generateForTab('unknown'), ...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}), }) + // add the cached context commands for file, folder, etc selection + if (savedContextCommands && savedContextCommands.length > 0) { + mynahUI.updateStore(tabID, { + contextCommands: savedContextCommands, + }) + } connector.onTabAdd(tabID) }, onTabRemove: connector.onTabRemove, diff --git a/packages/core/src/amazonq/webview/ui/messages/handler.ts b/packages/core/src/amazonq/webview/ui/messages/handler.ts index d85774d23f6..e704e08f145 100644 --- a/packages/core/src/amazonq/webview/ui/messages/handler.ts +++ b/packages/core/src/amazonq/webview/ui/messages/handler.ts @@ -41,11 +41,11 @@ export class TextMessageHandler { }) this.tabsStorage.updateTabStatus(tabID, 'busy') - void this.connector .requestGenerativeAIAnswer(tabID, eventID, { chatMessage: chatPrompt.prompt ?? '', chatCommand: chatPrompt.command, + chatContext: chatPrompt.context, }) .then(() => {}) } diff --git a/packages/core/src/codewhisperer/util/gitUtil.ts b/packages/core/src/codewhisperer/util/gitUtil.ts index 09cd9a6116f..752c16ba6bf 100644 --- a/packages/core/src/codewhisperer/util/gitUtil.ts +++ b/packages/core/src/codewhisperer/util/gitUtil.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { removeAnsi } from '../../shared/utilities/textUtilities' -import { getLogger } from '../../shared/logger/logger' +import { getLogger, removeAnsi } from '../../shared' import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' import { Uri } from 'vscode' diff --git a/packages/core/src/codewhispererChat/app.ts b/packages/core/src/codewhispererChat/app.ts index 6781cde30e5..346550aae87 100644 --- a/packages/core/src/codewhispererChat/app.ts +++ b/packages/core/src/codewhispererChat/app.ts @@ -48,6 +48,7 @@ export function init(appContext: AmazonQAppInitContext) { processSourceLinkClick: new EventEmitter(), processResponseBodyLinkClick: new EventEmitter(), processFooterInfoLinkClick: new EventEmitter(), + processContextCommandUpdateMessage: new EventEmitter(), } const cwChatControllerMessageListeners = { @@ -96,6 +97,9 @@ export function init(appContext: AmazonQAppInitContext) { processFooterInfoLinkClick: new MessageListener( cwChatControllerEventEmitters.processFooterInfoLinkClick ), + processContextCommandUpdateMessage: new MessageListener( + cwChatControllerEventEmitters.processContextCommandUpdateMessage + ), } const cwChatControllerMessagePublishers = { @@ -146,6 +150,9 @@ export function init(appContext: AmazonQAppInitContext) { processFooterInfoLinkClick: new MessagePublisher( cwChatControllerEventEmitters.processFooterInfoLinkClick ), + processContextCommandUpdateMessage: new MessagePublisher( + cwChatControllerEventEmitters.processContextCommandUpdateMessage + ), } new CwChatController( diff --git a/packages/core/src/codewhispererChat/commands/registerCommands.ts b/packages/core/src/codewhispererChat/commands/registerCommands.ts index abf6c6fa07c..da91f48ebea 100644 --- a/packages/core/src/codewhispererChat/commands/registerCommands.ts +++ b/packages/core/src/codewhispererChat/commands/registerCommands.ts @@ -102,6 +102,9 @@ export function registerCommands(controllerPublishers: ChatControllerMessagePubl }) }) }) + Commands.register('aws.amazonq.updateContextCommandItems', () => { + controllerPublishers.processContextCommandUpdateMessage.publish() + }) } export type EditorContextBaseCommandType = diff --git a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts index c30d5aeb706..3fac65e2d70 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts @@ -101,7 +101,6 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c // service will throw validation exception if string is empty const customizationArn: string | undefined = undefinedIfEmpty(triggerPayload.customization.arn) const chatTriggerType = triggerPayload.trigger === ChatTriggerType.InlineChatMessage ? 'INLINE_CHAT' : 'MANUAL' - return { conversationState: { currentMessage: { @@ -116,6 +115,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c relevantDocuments, useRelevantDocuments, }, + additionalContext: triggerPayload.additionalContents, }, userIntent: triggerPayload.userIntent, }, diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index e0ee9e0924b..48d803b5f43 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -2,6 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ +import * as path from 'path' import { Event as VSCodeEvent, Uri } from 'vscode' import { EditorContextExtractor } from '../../editor/context/extractor' import { ChatSessionStorage } from '../../storages/chatSession' @@ -55,6 +56,9 @@ import { inspect } from '../../../shared/utilities/collectionUtils' import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' import { waitUntil } from '../../../shared/utilities/timeoutUtils' +import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' +import { LspClient } from '../../../amazonq' +import { ContextCommandItem } from '../../../amazonq/lsp/types' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher @@ -74,6 +78,7 @@ export interface ChatControllerMessagePublishers { readonly processSourceLinkClick: MessagePublisher readonly processResponseBodyLinkClick: MessagePublisher readonly processFooterInfoLinkClick: MessagePublisher + readonly processContextCommandUpdateMessage: MessagePublisher } export interface ChatControllerMessageListeners { @@ -94,6 +99,7 @@ export interface ChatControllerMessageListeners { readonly processSourceLinkClick: MessageListener readonly processResponseBodyLinkClick: MessageListener readonly processFooterInfoLinkClick: MessageListener + readonly processContextCommandUpdateMessage: MessageListener } export class ChatController { @@ -211,6 +217,9 @@ export class ChatController { this.chatControllerMessageListeners.processFooterInfoLinkClick.onMessage((data) => { return this.processFooterInfoLinkClick(data) }) + this.chatControllerMessageListeners.processContextCommandUpdateMessage.onMessage(() => { + return this.processContextCommandUpdateMessage() + }) } private processFooterInfoLinkClick(click: FooterInfoLinkClick) { @@ -351,6 +360,66 @@ export class ChatController { } } + private async processContextCommandUpdateMessage() { + // when UI is ready, refresh the context commands + const contextCommand: MynahUIDataModel['contextCommands'] = [ + { + commands: [ + { + command: '@workspace', + description: 'Reference all code in workspace.', + }, + { + command: 'folder', + children: [ + { + groupName: 'Folders', + commands: [], + }, + ], + description: 'All files within a specific folder', + icon: 'folder' as MynahIconsType, + }, + { + command: 'file', + children: [ + { + groupName: 'Files', + commands: [], + }, + ], + description: 'File', + icon: 'file' as MynahIconsType, + }, + ], + }, + ] + await LspClient.instance.waitUtilReady() + const contextCommandItems = await LspClient.instance.getContextCommandItems() + const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] + const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] + + for (const contextCommandItem of contextCommandItems) { + const wsFolderName = path.basename(contextCommandItem.workspaceFolder) + if (contextCommandItem.type === 'file') { + filesCmd.children?.[0].commands.push({ + command: path.basename(contextCommandItem.relativePath), + description: path.join(wsFolderName, contextCommandItem.relativePath), + route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + icon: 'file' as MynahIconsType, + }) + } else { + folderCmd.children?.[0].commands.push({ + command: path.basename(contextCommandItem.relativePath), + description: path.join(wsFolderName, contextCommandItem.relativePath), + route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + icon: 'folder' as MynahIconsType, + }) + } + } + this.messenger.sendContextCommandData(contextCommand) + } + private processException(e: any, tabID: string) { let errorMessage = '' let requestID = undefined @@ -554,6 +623,7 @@ export class ChatController { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromPromptChatMessage(message), customization: getSelectedCustomization(), + context: message.context, }, triggerID ) @@ -595,6 +665,36 @@ export class ChatController { this.messenger.sendStaticTextResponse(responseType, triggerID, tabID) } + private async resolveContextCommandPayload(triggerPayload: TriggerPayload) { + if (triggerPayload.context === undefined || triggerPayload.context.length === 0) { + return + } + const contextCommands: ContextCommandItem[] = [] + for (const context of triggerPayload.context) { + if (typeof context !== 'string' && context.route && context.route.length === 2) { + contextCommands.push({ + workspaceFolder: context.route?.[0] || '', + type: context.icon === 'folder' ? 'folder' : 'file', + relativePath: context.route?.[1] || '', + }) + } + } + const prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) + if (prompts.length > 0) { + triggerPayload.additionalContents = [] + for (const prompt of prompts) { + triggerPayload.additionalContents.push({ + name: prompt.name, + description: prompt.description, + innerContext: prompt.content, + }) + } + getLogger().info( + `Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} ` + ) + } + } + private async generateResponse( triggerPayload: TriggerPayload & { projectContextQueryLatencyMs?: number }, triggerID: string @@ -627,6 +727,9 @@ export class ChatController { await this.messenger.sendAuthNeededExceptionMessage(credentialsState, tabID, triggerID) return } + + await this.resolveContextCommandPayload(triggerPayload) + // TODO: resolve the context into real context up to 90k triggerPayload.useRelevantDocuments = false if (triggerPayload.message) { triggerPayload.useRelevantDocuments = triggerPayload.message.includes(`@workspace`) diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 6604fd7bb21..b5935e8f644 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -8,6 +8,7 @@ import { AppToWebViewMessageDispatcher, AuthNeededException, CodeReference, + ContextCommandData, EditorContextCommandMessage, OpenSettingsMessage, QuickActionMessage, @@ -35,6 +36,7 @@ import { LspController } from '../../../../amazonq/lsp/lspController' import { extractCodeBlockLanguage } from '../../../../shared/markdown' import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils' import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants' +import { MynahUIDataModel } from '@aws/mynah-ui' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -482,4 +484,8 @@ export class Messenger { public sendOpenSettingsMessage(triggerId: string, tabID: string) { this.dispatcher.sendOpenSettingsMessage(new OpenSettingsMessage(tabID)) } + + public sendContextCommandData(contextCommands: MynahUIDataModel['contextCommands']) { + this.dispatcher.sendContextCommandData(new ContextCommandData(contextCommands)) + } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index f79510acacb..f6baeda8fa8 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -4,12 +4,13 @@ */ import * as vscode from 'vscode' -import { RelevantTextDocument, UserIntent } from '@amzn/codewhisperer-streaming' +import { AdditionalContentEntry, RelevantTextDocument, UserIntent } from '@amzn/codewhisperer-streaming' import { MatchPolicy, CodeQuery } from '../../clients/chat/v0/model' import { Selection } from 'vscode' import { TabOpenType } from '../../../amazonq/webview/ui/storages/tabsStorage' import { CodeReference } from '../../view/connector/connector' import { Customization } from '../../../codewhisperer/client/codewhispereruserclient' +import { QuickActionCommand } from '@aws/mynah-ui' export interface TriggerTabIDReceived { tabID: string @@ -102,6 +103,7 @@ export interface PromptMessage { command: ChatPromptCommandType | undefined userIntent: UserIntent | undefined tabID: string + context?: string[] | QuickActionCommand[] } export interface PromptAnswer { @@ -171,7 +173,9 @@ export interface TriggerPayload { readonly codeQuery: CodeQuery | undefined readonly userIntent: UserIntent | undefined readonly customization: Customization + readonly context?: string[] | QuickActionCommand[] relevantTextDocuments?: RelevantTextDocument[] + additionalContents?: AdditionalContentEntry[] useRelevantDocuments?: boolean traceId?: string } diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index 02794af5fb3..8b0de515f4e 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -7,6 +7,7 @@ import { Timestamp } from 'aws-sdk/clients/apigateway' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' +import { MynahUIDataModel } from '@aws/mynah-ui' class UiMessage { readonly time: number = Date.now() @@ -132,6 +133,15 @@ export class OpenSettingsMessage extends UiMessage { override type = 'openSettingsMessage' } +export class ContextCommandData extends UiMessage { + readonly data: MynahUIDataModel['contextCommands'] + override type = 'contextCommandData' + constructor(data: MynahUIDataModel['contextCommands']) { + super('tab-1') + this.data = data + } +} + export interface ChatMessageProps { readonly message: string | undefined readonly messageType: ChatMessageType @@ -243,4 +253,8 @@ export class AppToWebViewMessageDispatcher { public sendOpenSettingsMessage(message: OpenSettingsMessage) { this.appsToWebViewMessagePublisher.publish(message) } + + public sendContextCommandData(message: ContextCommandData) { + this.appsToWebViewMessagePublisher.publish(message) + } } diff --git a/packages/core/src/codewhispererChat/view/messages/messageListener.ts b/packages/core/src/codewhispererChat/view/messages/messageListener.ts index 9c1dcb4b095..0074717aa32 100644 --- a/packages/core/src/codewhispererChat/view/messages/messageListener.ts +++ b/packages/core/src/codewhispererChat/view/messages/messageListener.ts @@ -103,9 +103,17 @@ export class UIMessageListener { break case 'open-settings': this.processOpenSettings(msg) + break + case 'ui-is-ready': + this.processUIIsReady() + break } } + private processUIIsReady() { + this.chatControllerMessagePublishers.processContextCommandUpdateMessage.publish() + } + private processOpenSettings(msg: any) { void openSettingsId(`amazonQ.workspaceIndex`) } @@ -228,6 +236,7 @@ export class UIMessageListener { tabID: msg.tabID, messageId: msg.messageId, userIntent: msg.userIntent !== '' ? msg.userIntent : undefined, + context: msg.chatContext, }) } From db88beb64298e15f583dc5232e1d87f92ba1d08f Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Mon, 17 Feb 2025 13:33:28 -0800 Subject: [PATCH 02/40] fix cyclic dependencies; minimize code changes; reformat code --- packages/core/src/amazonq/index.ts | 2 +- packages/core/src/amazonq/lsp/lspClient.ts | 2 +- packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts | 1 + packages/core/src/amazonq/webview/ui/main.ts | 1 + packages/core/src/amazonq/webview/ui/messages/handler.ts | 1 + packages/core/src/codewhisperer/util/gitUtil.ts | 3 ++- .../controllers/chat/chatRequest/converter.ts | 1 + 7 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 9ca9af7687c..b42a0c20a39 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -43,7 +43,7 @@ export { ExtensionMessage } from '../amazonq/webview/ui/commands' export { CodeReference } from '../codewhispererChat/view/connector/connector' export { extractAuthFollowUp } from './util/authUtils' export { Messenger } from './commons/connector/baseMessenger' -import { FeatureContext } from '../shared' +import { FeatureContext } from '../shared/featureConfig' /** * main from createMynahUI is a purely browser dependency. Due to this diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index 7797f5eb960..7916cbba7ec 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -40,7 +40,7 @@ import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSet import { fs } from '../../shared/fs/fs' import { getLogger } from '../../shared/logger/logger' import globals from '../../shared/extensionGlobals' -import { waitUntil } from '../../shared' +import { waitUntil } from '../../shared/utilities/timeoutUtils' const localize = nls.loadMessageBundle() diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index bb3dd3327b5..0c382638d7f 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -150,6 +150,7 @@ export class Connector extends BaseConnector { await this.processEditorContextCommandMessage(messageData) return } + if (messageData.type === 'contextCommandData') { await this.processContextCommandData(messageData) return diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 0a93dbf310f..8eab1a16b1d 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -505,6 +505,7 @@ export const createMynahUI = ( if (!newTabID) { return } + tabsStorage.updateTabTypeFromUnknown(newTabID, tabType) connector.onKnownTabOpen(newTabID) connector.onUpdateTabType(newTabID) diff --git a/packages/core/src/amazonq/webview/ui/messages/handler.ts b/packages/core/src/amazonq/webview/ui/messages/handler.ts index e704e08f145..92a96d1c2da 100644 --- a/packages/core/src/amazonq/webview/ui/messages/handler.ts +++ b/packages/core/src/amazonq/webview/ui/messages/handler.ts @@ -41,6 +41,7 @@ export class TextMessageHandler { }) this.tabsStorage.updateTabStatus(tabID, 'busy') + void this.connector .requestGenerativeAIAnswer(tabID, eventID, { chatMessage: chatPrompt.prompt ?? '', diff --git a/packages/core/src/codewhisperer/util/gitUtil.ts b/packages/core/src/codewhisperer/util/gitUtil.ts index 752c16ba6bf..09cd9a6116f 100644 --- a/packages/core/src/codewhisperer/util/gitUtil.ts +++ b/packages/core/src/codewhisperer/util/gitUtil.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { getLogger, removeAnsi } from '../../shared' +import { removeAnsi } from '../../shared/utilities/textUtilities' +import { getLogger } from '../../shared/logger/logger' import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' import { Uri } from 'vscode' diff --git a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts index 3fac65e2d70..0a34463058e 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts @@ -101,6 +101,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c // service will throw validation exception if string is empty const customizationArn: string | undefined = undefinedIfEmpty(triggerPayload.customization.arn) const chatTriggerType = triggerPayload.trigger === ChatTriggerType.InlineChatMessage ? 'INLINE_CHAT' : 'MANUAL' + return { conversationState: { currentMessage: { From 6c6e284c8b221264bdc0fc096ee394f977b91efb Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Mon, 17 Feb 2025 13:48:02 -0800 Subject: [PATCH 03/40] further fix cyclic dependency --- .../core/src/codewhispererChat/controllers/chat/controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 48d803b5f43..8711e9a569a 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -57,7 +57,7 @@ import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' import { waitUntil } from '../../../shared/utilities/timeoutUtils' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' -import { LspClient } from '../../../amazonq' +import { LspClient } from '../../../amazonq/lsp/lspClient' import { ContextCommandItem } from '../../../amazonq/lsp/types' export interface ChatControllerMessagePublishers { From 9e94095c55af3eb131d388c6c2977af56427b1d8 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:19:13 -0800 Subject: [PATCH 04/40] update lsp for falcon (#6) --- packages/amazonq/package.json | 2 +- packages/core/src/amazonq/lsp/lspClient.ts | 11 +++++++++-- packages/core/src/amazonq/lsp/lspController.ts | 4 ++-- .../codewhispererChat/controllers/chat/controller.ts | 5 ++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 3d582d324d8..1e53ae80656 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.48.0-SNAPSHOT", + "version": "1.48.0-FALCON", "extensionKind": [ "workspace" ], diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index 7916cbba7ec..dbf3d1a20f5 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -40,7 +40,7 @@ import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSet import { fs } from '../../shared/fs/fs' import { getLogger } from '../../shared/logger/logger' import globals from '../../shared/extensionGlobals' -import { waitUntil } from '../../shared/utilities/timeoutUtils' +import { sleep, waitUntil } from '../../shared/utilities/timeoutUtils' const localize = nls.loadMessageBundle() @@ -222,9 +222,16 @@ export class LspClient { } } - async waitUtilReady() { + async waitUntilReady() { return waitUntil( async () => { + for (let i = 0; i < 5; i++) { + if (this.client !== undefined) { + break + } else { + await sleep(5_000) + } + } if (this.client === undefined) { return false } diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 37afed70db0..2f5aa6dcc38 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -58,9 +58,9 @@ export interface Manifest { targets: Target[] }[] } -const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' +const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.36.json' // this LSP client in Q extension is only going to work with these LSP server versions -const supportedLspServerVersions = ['0.1.35'] +const supportedLspServerVersions = ['0.1.36'] const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node' diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 8711e9a569a..0840215013d 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -394,7 +394,10 @@ export class ChatController { ], }, ] - await LspClient.instance.waitUtilReady() + const lspClientReady = await LspClient.instance.waitUntilReady() + if (!lspClientReady) { + return + } const contextCommandItems = await LspClient.instance.getContextCommandItems() const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] From 2726c7ad93e16d060958502fa144614c9e19514e Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Mon, 17 Feb 2025 15:20:45 -0800 Subject: [PATCH 05/40] fix minor bug when context cmd list is empty --- .../core/src/codewhispererChat/controllers/chat/controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 0840215013d..338ab16464a 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -682,6 +682,9 @@ export class ChatController { }) } } + if (contextCommands.length === 0) { + return + } const prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) if (prompts.length > 0) { triggerPayload.additionalContents = [] From 79e531b77192fb50b4b479064d2bb132705bdae4 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:33:56 -0500 Subject: [PATCH 06/40] feat(amazonq): saved prompts (#7) * feat(amazonq): saved prompts * fix: remove unecessary change --- .../webview/ui/apps/cwChatConnector.ts | 59 ++++++++- .../core/src/amazonq/webview/ui/commands.ts | 1 + .../core/src/amazonq/webview/ui/connector.ts | 19 +++ packages/core/src/amazonq/webview/ui/main.ts | 12 ++ .../src/amazonq/webview/ui/tabs/constants.ts | 2 +- packages/core/src/codewhispererChat/app.ts | 16 +++ .../controllers/chat/controller.ts | 125 +++++++++++++++++- .../controllers/chat/messenger/messenger.ts | 15 ++- .../controllers/chat/model.ts | 6 + .../view/connector/connector.ts | 51 ++++++- .../view/messages/messageListener.ts | 18 +++ 11 files changed, 314 insertions(+), 10 deletions(-) diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 0c382638d7f..089e5f69e65 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemType, MynahUIDataModel } from '@aws/mynah-ui' +import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel } from '@aws/mynah-ui' import { TabType } from '../storages/tabsStorage' import { CWCChatItem } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' @@ -11,11 +11,19 @@ import { BaseConnector, BaseConnectorProps } from './baseConnector' export interface ConnectorProps extends BaseConnectorProps { onCWCContextCommandMessage: (message: CWCChatItem, command?: string) => string | undefined onContextCommandDataReceived: (data: MynahUIDataModel['contextCommands']) => void + onShowCustomForm: ( + tabId: string, + formItems?: ChatItemFormItem[], + buttons?: ChatItemButton[], + title?: string, + description?: string + ) => void } export class Connector extends BaseConnector { private readonly onCWCContextCommandMessage private readonly onContextCommandDataReceived + private readonly onShowCustomForm override getTabType(): TabType { return 'cwc' @@ -25,6 +33,7 @@ export class Connector extends BaseConnector { super(props) this.onCWCContextCommandMessage = props.onCWCContextCommandMessage this.onContextCommandDataReceived = props.onContextCommandDataReceived + this.onShowCustomForm = props.onShowCustomForm } onSourceLinkClick = (tabID: string, messageId: string, link: string): void => { @@ -140,6 +149,16 @@ export class Connector extends BaseConnector { } } + private showCustomFormMessage = (messageData: any) => { + this.onShowCustomForm( + messageData.tabID, + messageData.formItems, + messageData.buttons, + messageData.title, + messageData.description + ) + } + handleMessageReceive = async (messageData: any): Promise => { if (messageData.type === 'chatMessage') { await this.processChatMessage(messageData) @@ -155,7 +174,45 @@ export class Connector extends BaseConnector { await this.processContextCommandData(messageData) return } + if (messageData.type === 'showCustomFormMessage') { + this.showCustomFormMessage(messageData) + return + } + + if (messageData.type === 'customFormActionMessage') { + this.onCustomFormAction(messageData.tabID, messageData.action) + return + } // For other message types, call the base class handleMessageReceive await this.baseHandleMessageReceive(messageData) } + + onQuickCommandGroupActionClick = (tabID: string, action: { id: string }) => { + this.sendMessageToExtension({ + command: 'quick-command-group-action-click', + actionId: action.id, + tabID, + tabType: this.getTabType(), + }) + } + + onCustomFormAction( + tabId: string, + action: { + id: string + text?: string | undefined + formItemValues?: Record | undefined + } + ) { + if (action === undefined) { + return + } + + this.sendMessageToExtension({ + command: 'form-action-click', + action: action, + tabType: this.getTabType(), + tabID: tabId, + }) + } } diff --git a/packages/core/src/amazonq/webview/ui/commands.ts b/packages/core/src/amazonq/webview/ui/commands.ts index bdf2490b3a1..bf59f427cc9 100644 --- a/packages/core/src/amazonq/webview/ui/commands.ts +++ b/packages/core/src/amazonq/webview/ui/commands.ts @@ -42,5 +42,6 @@ type MessageCommand = | 'open-link' | 'send-telemetry' | 'update-welcome-count' + | 'quick-command-group-action-click' export type ExtensionMessage = Record & { command: MessageCommand } diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 3ac0a4c0f55..94dfd1be6da 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -14,6 +14,8 @@ import { ChatPrompt, MynahUIDataModel, QuickActionCommand, + ChatItemFormItem, + ChatItemButton, } from '@aws/mynah-ui' import { Connector as CWChatConnector } from './apps/cwChatConnector' import { Connector as FeatureDevChatConnector } from './apps/featureDevChatConnector' @@ -91,6 +93,13 @@ export interface ConnectorProps { handleCommand: (chatPrompt: ChatPrompt, tabId: string) => void sendStaticMessages: (tabID: string, messages: ChatItem[]) => void onContextCommandDataReceived: (message: MynahUIDataModel['contextCommands']) => void + onShowCustomForm: ( + tabId: string, + formItems?: ChatItemFormItem[], + buttons?: ChatItemButton[], + title?: string, + description?: string + ) => void tabsStorage: TabsStorage } @@ -612,6 +621,14 @@ export class Connector { } } + onQuickCommandGroupActionClick = (tabId: string, action: { id: string }) => { + switch (this.tabsStorage.getTab(tabId)?.type) { + case 'cwc': + this.cwChatConnector.onQuickCommandGroupActionClick(tabId, action) + break + } + } + onChatItemVoted = (tabId: string, messageId: string, vote: 'upvote' | 'downvote'): void | undefined => { switch (this.tabsStorage.getTab(tabId)?.type) { case 'cwc': @@ -655,6 +672,8 @@ export class Connector { type: '', tabType: 'cwc', }) + } else { + this.cwChatConnector.onCustomFormAction(tabId, action) } break case 'agentWalkthrough': { diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 8eab1a16b1d..b813f11179f 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -13,6 +13,8 @@ import { NotificationType, ReferenceTrackerInformation, ProgressField, + ChatItemButton, + ChatItemFormItem, } from '@aws/mynah-ui' import { ChatPrompt } from '@aws/mynah-ui/dist/static' import { TabsStorage, TabType } from './storages/tabsStorage' @@ -570,6 +572,15 @@ export const createMynahUI = ( } } }, + onShowCustomForm( + tabId: string, + formItems?: ChatItemFormItem[], + buttons?: ChatItemButton[], + title?: string, + description?: string + ) { + mynahUI.showCustomForm(tabId, formItems, buttons, title, description) + }, }) mynahUI = new MynahUI({ @@ -670,6 +681,7 @@ export const createMynahUI = ( // handler for the cwc panel textMessageHandler.handle(prompt, tabID, eventId as string) }, + onQuickCommandGroupActionClick: connector.onQuickCommandGroupActionClick, onVote: connector.onChatItemVoted, onInBodyButtonClicked: (tabId, messageId, action, eventId) => { switch (action.id) { diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index efbf700b91d..cb21c4da242 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -13,7 +13,7 @@ export type TabTypeData = { contextCommands?: QuickActionCommandGroup[] } -const workspaceCommand: QuickActionCommandGroup = { +export const workspaceCommand: QuickActionCommandGroup = { groupName: 'Mention code', commands: [ { diff --git a/packages/core/src/codewhispererChat/app.ts b/packages/core/src/codewhispererChat/app.ts index 346550aae87..3a9f2816a30 100644 --- a/packages/core/src/codewhispererChat/app.ts +++ b/packages/core/src/codewhispererChat/app.ts @@ -26,8 +26,10 @@ import { TriggerTabIDReceived, UIFocusMessage, AcceptDiff, + QuickCommandGroupActionClick, } from './controllers/chat/model' import { EditorContextCommand, registerCommands } from './commands/registerCommands' +import { CustomFormActionMessage } from './view/connector/connector' export function init(appContext: AmazonQAppInitContext) { const cwChatControllerEventEmitters = { @@ -49,6 +51,8 @@ export function init(appContext: AmazonQAppInitContext) { processResponseBodyLinkClick: new EventEmitter(), processFooterInfoLinkClick: new EventEmitter(), processContextCommandUpdateMessage: new EventEmitter(), + processQuickCommandGroupActionClicked: new EventEmitter(), + processCustomFormAction: new EventEmitter(), } const cwChatControllerMessageListeners = { @@ -100,6 +104,12 @@ export function init(appContext: AmazonQAppInitContext) { processContextCommandUpdateMessage: new MessageListener( cwChatControllerEventEmitters.processContextCommandUpdateMessage ), + processQuickCommandGroupActionClicked: new MessageListener( + cwChatControllerEventEmitters.processQuickCommandGroupActionClicked + ), + processCustomFormAction: new MessageListener( + cwChatControllerEventEmitters.processCustomFormAction + ), } const cwChatControllerMessagePublishers = { @@ -153,6 +163,12 @@ export function init(appContext: AmazonQAppInitContext) { processContextCommandUpdateMessage: new MessagePublisher( cwChatControllerEventEmitters.processContextCommandUpdateMessage ), + processQuickCommandGroupActionClicked: new MessagePublisher( + cwChatControllerEventEmitters.processQuickCommandGroupActionClicked + ), + processCustomFormAction: new MessagePublisher( + cwChatControllerEventEmitters.processCustomFormAction + ), } new CwChatController( diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 338ab16464a..61fe823de82 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -27,8 +27,9 @@ import { FooterInfoLinkClick, ViewDiff, AcceptDiff, + QuickCommandGroupActionClick, } from './model' -import { AppToWebViewMessageDispatcher } from '../../view/connector/connector' +import { AppToWebViewMessageDispatcher, CustomFormActionMessage } from '../../view/connector/connector' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { MessageListener } from '../../../amazonq/messages/messageListener' import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' @@ -59,6 +60,9 @@ import { waitUntil } from '../../../shared/utilities/timeoutUtils' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' import { LspClient } from '../../../amazonq/lsp/lspClient' import { ContextCommandItem } from '../../../amazonq/lsp/types' +import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' +import fs from '../../../shared/fs/fs' +import * as vscode from 'vscode' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher @@ -79,6 +83,8 @@ export interface ChatControllerMessagePublishers { readonly processResponseBodyLinkClick: MessagePublisher readonly processFooterInfoLinkClick: MessagePublisher readonly processContextCommandUpdateMessage: MessagePublisher + readonly processQuickCommandGroupActionClicked: MessagePublisher + readonly processCustomFormAction: MessagePublisher } export interface ChatControllerMessageListeners { @@ -100,6 +106,8 @@ export interface ChatControllerMessageListeners { readonly processResponseBodyLinkClick: MessageListener readonly processFooterInfoLinkClick: MessageListener readonly processContextCommandUpdateMessage: MessageListener + readonly processQuickCommandGroupActionClicked: MessageListener + readonly processCustomFormAction: MessageListener } export class ChatController { @@ -220,6 +228,12 @@ export class ChatController { this.chatControllerMessageListeners.processContextCommandUpdateMessage.onMessage(() => { return this.processContextCommandUpdateMessage() }) + this.chatControllerMessageListeners.processQuickCommandGroupActionClicked.onMessage((data) => { + return this.processQuickCommandGroupActionClicked(data) + }) + this.chatControllerMessageListeners.processCustomFormAction.onMessage((data) => { + return this.processCustomFormAction(data) + }) } private processFooterInfoLinkClick(click: FooterInfoLinkClick) { @@ -365,10 +379,7 @@ export class ChatController { const contextCommand: MynahUIDataModel['contextCommands'] = [ { commands: [ - { - command: '@workspace', - description: 'Reference all code in workspace.', - }, + ...workspaceCommand.commands, { command: 'folder', children: [ @@ -391,6 +402,25 @@ export class ChatController { description: 'File', icon: 'file' as MynahIconsType, }, + { + command: 'prompts', + children: [ + { + groupName: 'Prompts', + actions: [ + { + id: 'create-prompt', + icon: 'plus', + text: 'Create', + description: 'Create new prompt', + }, + ], + commands: [], + }, + ], + description: 'Prompts', + icon: 'magic' as MynahIconsType, + }, ], }, ] @@ -401,6 +431,7 @@ export class ChatController { const contextCommandItems = await LspClient.instance.getContextCommandItems() const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] + const promptsCmd: QuickActionCommand = contextCommand[0].commands?.[3] for (const contextCommandItem of contextCommandItems) { const wsFolderName = path.basename(contextCommandItem.workspaceFolder) @@ -411,6 +442,15 @@ export class ChatController { route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], icon: 'file' as MynahIconsType, }) + + // If file is a .prompt type, add to prompts list + if (contextCommandItem.relativePath.endsWith('.prompt')) { + promptsCmd.children?.[0].commands.push({ + command: path.basename(contextCommandItem.relativePath, '.prompt'), + route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + icon: 'magic' as MynahIconsType, + }) + } } else { folderCmd.children?.[0].commands.push({ command: path.basename(contextCommandItem.relativePath), @@ -420,9 +460,84 @@ export class ChatController { }) } } + + // Check ~/.aws/prompts for saved prompts + try { + const systemPromptsDirectory = path.join(fs.getUserHomeDir(), '.aws', 'prompts') + const systemPromptFiles = await fs.readdir(systemPromptsDirectory) + promptsCmd.children?.[0].commands.push( + ...systemPromptFiles + .filter(([name]) => name.endsWith('.prompt')) + .map(([name]) => ({ + command: name.replace(/\.prompt$/, ''), + icon: 'magic' as MynahIconsType, + route: [systemPromptsDirectory, name], + })) + ) + } catch (e) { + getLogger().verbose(`Could not read prompts from ~/.aws/prompts: ${e}`) + } + this.messenger.sendContextCommandData(contextCommand) } + private async processQuickCommandGroupActionClicked(message: QuickCommandGroupActionClick) { + if (message.actionId === 'create-prompt') { + this.messenger.showCustomForm( + message.tabID, + [ + { + id: 'prompt-name', + type: 'textinput', + mandatory: true, + title: 'Prompt name', + placeholder: 'Enter prompt name', + }, + { + id: 'shared-scope', + type: 'select', + title: 'Save globally for all projects?', + mandatory: true, + value: 'system', + options: [ + { value: 'project', label: 'No' }, + { value: 'system', label: 'Yes' }, + ], + }, + ], + [ + { id: 'cancel-create-prompt', text: 'Cancel', status: 'clear' }, + { id: 'submit-create-prompt', text: 'Create', status: 'main' }, + ], + `Create a saved prompt`, + 'Use this prompt by typing `@` followed by the prompt name.' + ) + } + } + + private async processCustomFormAction(message: CustomFormActionMessage) { + if (message.tabID) { + if (message.action.id === 'submit-create-prompt') { + let promptsDirectory = path.join(fs.getUserHomeDir(), '.aws', 'prompts') + if ( + vscode.workspace.workspaceFolders?.[0] && + message.action.formItemValues?.['shared-scope'] === 'project' + ) { + const workspaceUri = vscode.workspace.workspaceFolders[0].uri + promptsDirectory = vscode.Uri.joinPath(workspaceUri, '.aws', 'prompts').fsPath + } + + const title = message.action.formItemValues?.['prompt-name'] + const newFilePath = path.join(promptsDirectory, title ? `${title}.prompt` : 'default.prompt') + const newFileContent = new Uint8Array(Buffer.from('')) + await fs.writeFile(newFilePath, newFileContent) + const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) + await vscode.window.showTextDocument(newFileDoc) + // TO-DO: Trigger regeneration of prompt list in context menu + } + } + } + private processException(e: any, tabID: string) { let errorMessage = '' let requestID = undefined diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index b5935e8f644..79589657b94 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -12,6 +12,7 @@ import { EditorContextCommandMessage, OpenSettingsMessage, QuickActionMessage, + ShowCustomFormMessage, } from '../../../view/connector/connector' import { EditorContextCommandType } from '../../../commands/registerCommands' import { ChatResponseStream as qdevChatResponseStream } from '@amzn/amazon-q-developer-streaming-client' @@ -36,7 +37,7 @@ import { LspController } from '../../../../amazonq/lsp/lspController' import { extractCodeBlockLanguage } from '../../../../shared/markdown' import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils' import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants' -import { MynahUIDataModel } from '@aws/mynah-ui' +import { ChatItemButton, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -488,4 +489,16 @@ export class Messenger { public sendContextCommandData(contextCommands: MynahUIDataModel['contextCommands']) { this.dispatcher.sendContextCommandData(new ContextCommandData(contextCommands)) } + + public showCustomForm( + tabID: string, + formItems?: ChatItemFormItem[], + buttons?: ChatItemButton[], + title?: string, + description?: string + ) { + this.dispatcher.sendShowCustomFormMessage( + new ShowCustomFormMessage(tabID, formItems, buttons, title, description) + ) + } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index f6baeda8fa8..464e56f09f0 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -141,6 +141,12 @@ export interface FooterInfoLinkClick { link: string } +export interface QuickCommandGroupActionClick { + command: string + actionId: string + tabID: string +} + export interface ChatItemVotedMessage { tabID: string command: string diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index 8b0de515f4e..548e95cbbf9 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -7,14 +7,14 @@ import { Timestamp } from 'aws-sdk/clients/apigateway' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' -import { MynahUIDataModel } from '@aws/mynah-ui' +import { ChatItemButton, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui' class UiMessage { readonly time: number = Date.now() readonly sender: string = 'CWChat' readonly type: string = '' - public constructor(protected tabID: string | undefined) {} + public constructor(public tabID: string | undefined) {} } export class ErrorMessage extends UiMessage { @@ -142,6 +142,49 @@ export class ContextCommandData extends UiMessage { } } +export class CustomFormActionMessage extends UiMessage { + override type = 'customFormActionMessage' + readonly action: { + id: string + text?: string | undefined + formItemValues?: Record | undefined + } + + constructor( + tabID: string, + action: { + id: string + text?: string | undefined + formItemValues?: Record | undefined + } + ) { + super(tabID) + this.action = action + } +} + +export class ShowCustomFormMessage extends UiMessage { + override type = 'showCustomFormMessage' + readonly formItems?: ChatItemFormItem[] + readonly buttons?: ChatItemButton[] + readonly title?: string + readonly description?: string + + constructor( + tabID: string, + formItems?: ChatItemFormItem[], + buttons?: ChatItemButton[], + title?: string, + description?: string + ) { + super(tabID) + this.formItems = formItems + this.buttons = buttons + this.title = title + this.description = description + } +} + export interface ChatMessageProps { readonly message: string | undefined readonly messageType: ChatMessageType @@ -257,4 +300,8 @@ export class AppToWebViewMessageDispatcher { public sendContextCommandData(message: ContextCommandData) { this.appsToWebViewMessagePublisher.publish(message) } + + public sendShowCustomFormMessage(message: ShowCustomFormMessage) { + this.appsToWebViewMessagePublisher.publish(message) + } } diff --git a/packages/core/src/codewhispererChat/view/messages/messageListener.ts b/packages/core/src/codewhispererChat/view/messages/messageListener.ts index 0074717aa32..7d8f8dc408e 100644 --- a/packages/core/src/codewhispererChat/view/messages/messageListener.ts +++ b/packages/core/src/codewhispererChat/view/messages/messageListener.ts @@ -107,6 +107,12 @@ export class UIMessageListener { case 'ui-is-ready': this.processUIIsReady() break + case 'quick-command-group-action-click': + this.quickCommandGroupActionClicked(msg) + break + case 'form-action-click': + this.processCustomFormAction(msg) + break } } @@ -114,6 +120,18 @@ export class UIMessageListener { this.chatControllerMessagePublishers.processContextCommandUpdateMessage.publish() } + private processCustomFormAction(msg: any) { + this.chatControllerMessagePublishers.processCustomFormAction.publish({ tabID: msg.tabID, ...msg }) + } + + private quickCommandGroupActionClicked(msg: any) { + this.chatControllerMessagePublishers.processQuickCommandGroupActionClicked.publish({ + tabID: msg.tabID, + actionId: msg.actionId, + command: 'quick-command-group-action-click', + }) + } + private processOpenSettings(msg: any) { void openSettingsId(`amazonQ.workspaceIndex`) } From e2b54e8dedae38e5823103694fb62bc615f643fb Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:33:55 -0500 Subject: [PATCH 07/40] Add 'create a new prompt' context to bottom of prompts list (#8) * feat(amazonq): saved prompts * fix: remove unecessary change * feat(amazonq): saved prompts * fix: remove unecessary change * feat(amazonq): add Create a new prompt button to context --- package-lock.json | 10 +-- packages/core/package.json | 2 +- .../webview/ui/apps/cwChatConnector.ts | 19 +++- .../core/src/amazonq/webview/ui/commands.ts | 1 + .../core/src/amazonq/webview/ui/connector.ts | 15 ++++ packages/core/src/amazonq/webview/ui/main.ts | 1 + .../src/amazonq/webview/ui/tabs/constants.ts | 2 + packages/core/src/codewhispererChat/app.ts | 9 +- .../controllers/chat/controller.ts | 89 ++++++++++++------- .../view/connector/connector.ts | 12 ++- .../view/messages/messageListener.ts | 6 ++ 11 files changed, 122 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fb9f8a7d81..b24b5841df9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4935,9 +4935,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.23.0-beta.6", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.6.tgz", - "integrity": "sha512-wiMKJKhx9BdI4LWXCchjBNZaIFrccZqvjQ7XXFUZ5iWrZMOzTS6soeJ7J/S/3KLIHDfZmR3/v68RvzDIlgG2sw==", + "version": "4.23.0-beta.7", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.7.tgz", + "integrity": "sha512-qRwy8bP8inhbTb94xv9tJvqrmvv+PDcGyOKuPE9vx4+cwymNtf8HVp5IcIjJ/oTWV5bWA7M3o0mm9qEFFYdB6w==", "hasInstallScript": true, "dependencies": { "escape-html": "^1.0.3", @@ -18891,7 +18891,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.48.0-SNAPSHOT", + "version": "1.48.0-FALCON", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -18923,7 +18923,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.5", + "@aws/mynah-ui": "^4.23.0-beta.7", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index 6b68badb3b6..9e84a82ed3d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -510,7 +510,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.5", + "@aws/mynah-ui": "^4.23.0-beta.7", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 089e5f69e65..ed8872f825b 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -3,10 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel } from '@aws/mynah-ui' +import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' import { TabType } from '../storages/tabsStorage' import { CWCChatItem } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' +import { createPromptCommand } from '../tabs/constants' export interface ConnectorProps extends BaseConnectorProps { onCWCContextCommandMessage: (message: CWCChatItem, command?: string) => string | undefined @@ -171,7 +172,7 @@ export class Connector extends BaseConnector { } if (messageData.type === 'contextCommandData') { - await this.processContextCommandData(messageData) + this.processContextCommandData(messageData) return } if (messageData.type === 'showCustomFormMessage') { @@ -196,6 +197,20 @@ export class Connector extends BaseConnector { }) } + onContextSelected = (tabID: string, contextItem: QuickActionCommand) => { + this.sendMessageToExtension({ + command: 'context-selected', + contextItem, + tabID, + tabType: this.getTabType(), + }) + + if (contextItem.command === createPromptCommand) { + return false + } + return true + } + onCustomFormAction( tabId: string, action: { diff --git a/packages/core/src/amazonq/webview/ui/commands.ts b/packages/core/src/amazonq/webview/ui/commands.ts index bf59f427cc9..5b79549e53e 100644 --- a/packages/core/src/amazonq/webview/ui/commands.ts +++ b/packages/core/src/amazonq/webview/ui/commands.ts @@ -43,5 +43,6 @@ type MessageCommand = | 'send-telemetry' | 'update-welcome-count' | 'quick-command-group-action-click' + | 'context-selected' export type ExtensionMessage = Record & { command: MessageCommand } diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 94dfd1be6da..ae7878b93b4 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -623,12 +623,27 @@ export class Connector { onQuickCommandGroupActionClick = (tabId: string, action: { id: string }) => { switch (this.tabsStorage.getTab(tabId)?.type) { + case 'welcome': + case 'unknown': case 'cwc': + this.tabsStorage.updateTabTypeFromUnknown(tabId, 'cwc') this.cwChatConnector.onQuickCommandGroupActionClick(tabId, action) break } } + onContextSelected = (contextItem: QuickActionCommand, tabId: string) => { + switch (this.tabsStorage.getTab(tabId)?.type) { + case 'welcome': + case 'unknown': + case 'cwc': + this.tabsStorage.updateTabTypeFromUnknown(tabId, 'cwc') + return this.cwChatConnector.onContextSelected(tabId, contextItem) + default: + return true + } + } + onChatItemVoted = (tabId: string, messageId: string, vote: 'upvote' | 'downvote'): void | undefined => { switch (this.tabsStorage.getTab(tabId)?.type) { case 'cwc': diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index b813f11179f..7d77c34bd76 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -682,6 +682,7 @@ export const createMynahUI = ( textMessageHandler.handle(prompt, tabID, eventId as string) }, onQuickCommandGroupActionClick: connector.onQuickCommandGroupActionClick, + onContextSelected: connector.onContextSelected, onVote: connector.onChatItemVoted, onInBodyButtonClicked: (tabId, messageId, action, eventId) => { switch (action.id) { diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index cb21c4da242..49199be94f1 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -13,6 +13,8 @@ export type TabTypeData = { contextCommands?: QuickActionCommandGroup[] } +export const createPromptCommand = 'Create a new prompt' + export const workspaceCommand: QuickActionCommandGroup = { groupName: 'Mention code', commands: [ diff --git a/packages/core/src/codewhispererChat/app.ts b/packages/core/src/codewhispererChat/app.ts index 3a9f2816a30..2fe73c04a04 100644 --- a/packages/core/src/codewhispererChat/app.ts +++ b/packages/core/src/codewhispererChat/app.ts @@ -29,7 +29,7 @@ import { QuickCommandGroupActionClick, } from './controllers/chat/model' import { EditorContextCommand, registerCommands } from './commands/registerCommands' -import { CustomFormActionMessage } from './view/connector/connector' +import { ContextSelectedMessage, CustomFormActionMessage } from './view/connector/connector' export function init(appContext: AmazonQAppInitContext) { const cwChatControllerEventEmitters = { @@ -53,6 +53,7 @@ export function init(appContext: AmazonQAppInitContext) { processContextCommandUpdateMessage: new EventEmitter(), processQuickCommandGroupActionClicked: new EventEmitter(), processCustomFormAction: new EventEmitter(), + processContextSelected: new EventEmitter(), } const cwChatControllerMessageListeners = { @@ -110,6 +111,9 @@ export function init(appContext: AmazonQAppInitContext) { processCustomFormAction: new MessageListener( cwChatControllerEventEmitters.processCustomFormAction ), + processContextSelected: new MessageListener( + cwChatControllerEventEmitters.processContextSelected + ), } const cwChatControllerMessagePublishers = { @@ -169,6 +173,9 @@ export function init(appContext: AmazonQAppInitContext) { processCustomFormAction: new MessagePublisher( cwChatControllerEventEmitters.processCustomFormAction ), + processContextSelected: new MessagePublisher( + cwChatControllerEventEmitters.processContextSelected + ), } new CwChatController( diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 61fe823de82..cb3b0ef0231 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -29,7 +29,11 @@ import { AcceptDiff, QuickCommandGroupActionClick, } from './model' -import { AppToWebViewMessageDispatcher, CustomFormActionMessage } from '../../view/connector/connector' +import { + AppToWebViewMessageDispatcher, + ContextSelectedMessage, + CustomFormActionMessage, +} from '../../view/connector/connector' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { MessageListener } from '../../../amazonq/messages/messageListener' import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' @@ -60,7 +64,7 @@ import { waitUntil } from '../../../shared/utilities/timeoutUtils' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' import { LspClient } from '../../../amazonq/lsp/lspClient' import { ContextCommandItem } from '../../../amazonq/lsp/types' -import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' +import { createPromptCommand, workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import * as vscode from 'vscode' @@ -85,6 +89,7 @@ export interface ChatControllerMessagePublishers { readonly processContextCommandUpdateMessage: MessagePublisher readonly processQuickCommandGroupActionClicked: MessagePublisher readonly processCustomFormAction: MessagePublisher + readonly processContextSelected: MessagePublisher } export interface ChatControllerMessageListeners { @@ -108,6 +113,7 @@ export interface ChatControllerMessageListeners { readonly processContextCommandUpdateMessage: MessageListener readonly processQuickCommandGroupActionClicked: MessageListener readonly processCustomFormAction: MessageListener + readonly processContextSelected: MessageListener } export class ChatController { @@ -234,6 +240,9 @@ export class ChatController { this.chatControllerMessageListeners.processCustomFormAction.onMessage((data) => { return this.processCustomFormAction(data) }) + this.chatControllerMessageListeners.processContextSelected.onMessage((data) => { + return this.processContextSelected(data) + }) } private processFooterInfoLinkClick(click: FooterInfoLinkClick) { @@ -411,7 +420,6 @@ export class ChatController { { id: 'create-prompt', icon: 'plus', - text: 'Create', description: 'Create new prompt', }, ], @@ -478,40 +486,47 @@ export class ChatController { getLogger().verbose(`Could not read prompts from ~/.aws/prompts: ${e}`) } + // Add create prompt button to the bottom of the prompts list + promptsCmd.children?.[0].commands.push({ command: createPromptCommand, icon: 'list-add' as MynahIconsType }) + this.messenger.sendContextCommandData(contextCommand) } - private async processQuickCommandGroupActionClicked(message: QuickCommandGroupActionClick) { + private handlePromptCreate(tabID: string) { + this.messenger.showCustomForm( + tabID, + [ + { + id: 'prompt-name', + type: 'textinput', + mandatory: true, + title: 'Prompt name', + placeholder: 'Enter prompt name', + description: 'Use this prompt in the chat by typing `@` followed by the prompt name.', + }, + { + id: 'shared-scope', + type: 'select', + title: 'Save globally for all projects?', + mandatory: true, + value: 'system', + description: 'If yes is selected, .prompt file will be saved in ~/.aws/prompts.', + options: [ + { value: 'project', label: 'No' }, + { value: 'system', label: 'Yes' }, + ], + }, + ], + [ + { id: 'cancel-create-prompt', text: 'Cancel', status: 'clear' }, + { id: 'submit-create-prompt', text: 'Create', status: 'main' }, + ], + `Create a saved prompt` + ) + } + private processQuickCommandGroupActionClicked(message: QuickCommandGroupActionClick) { if (message.actionId === 'create-prompt') { - this.messenger.showCustomForm( - message.tabID, - [ - { - id: 'prompt-name', - type: 'textinput', - mandatory: true, - title: 'Prompt name', - placeholder: 'Enter prompt name', - }, - { - id: 'shared-scope', - type: 'select', - title: 'Save globally for all projects?', - mandatory: true, - value: 'system', - options: [ - { value: 'project', label: 'No' }, - { value: 'system', label: 'Yes' }, - ], - }, - ], - [ - { id: 'cancel-create-prompt', text: 'Cancel', status: 'clear' }, - { id: 'submit-create-prompt', text: 'Create', status: 'main' }, - ], - `Create a saved prompt`, - 'Use this prompt by typing `@` followed by the prompt name.' - ) + this.handlePromptCreate(message.tabID) } } @@ -533,11 +548,17 @@ export class ChatController { await fs.writeFile(newFilePath, newFileContent) const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) await vscode.window.showTextDocument(newFileDoc) - // TO-DO: Trigger regeneration of prompt list in context menu + await this.processContextCommandUpdateMessage() } } } + private async processContextSelected(message: ContextSelectedMessage) { + if (message.tabID && message.contextItem.command === createPromptCommand) { + this.handlePromptCreate(message.tabID) + } + } + private processException(e: any, tabID: string) { let errorMessage = '' let requestID = undefined diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index 548e95cbbf9..927c1880772 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -7,7 +7,7 @@ import { Timestamp } from 'aws-sdk/clients/apigateway' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' -import { ChatItemButton, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui' +import { ChatItemButton, ChatItemFormItem, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' class UiMessage { readonly time: number = Date.now() @@ -185,6 +185,16 @@ export class ShowCustomFormMessage extends UiMessage { } } +export class ContextSelectedMessage extends UiMessage { + override type = 'contextSelectedMessage' + readonly contextItem: QuickActionCommand + + constructor(tabID: string, contextItem: QuickActionCommand) { + super(tabID) + this.contextItem = contextItem + } +} + export interface ChatMessageProps { readonly message: string | undefined readonly messageType: ChatMessageType diff --git a/packages/core/src/codewhispererChat/view/messages/messageListener.ts b/packages/core/src/codewhispererChat/view/messages/messageListener.ts index 7d8f8dc408e..8bc5e1e2d09 100644 --- a/packages/core/src/codewhispererChat/view/messages/messageListener.ts +++ b/packages/core/src/codewhispererChat/view/messages/messageListener.ts @@ -113,6 +113,8 @@ export class UIMessageListener { case 'form-action-click': this.processCustomFormAction(msg) break + case 'context-selected': + this.processContextSelected(msg) } } @@ -124,6 +126,10 @@ export class UIMessageListener { this.chatControllerMessagePublishers.processCustomFormAction.publish({ tabID: msg.tabID, ...msg }) } + private processContextSelected(msg: any) { + this.chatControllerMessagePublishers.processContextSelected.publish({ tabID: msg.tabID, ...msg }) + } + private quickCommandGroupActionClicked(msg: any) { this.chatControllerMessagePublishers.processQuickCommandGroupActionClicked.publish({ tabID: msg.tabID, From 450d789e456ffe744817a8be9ce19afdc05b0cc2 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Tue, 18 Feb 2025 11:19:46 -0800 Subject: [PATCH 08/40] dev lsp 0.1.37 --- packages/core/src/amazonq/lsp/lspController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 2f5aa6dcc38..b38696d0b10 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -58,9 +58,9 @@ export interface Manifest { targets: Target[] }[] } -const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.36.json' +const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.37.json' // this LSP client in Q extension is only going to work with these LSP server versions -const supportedLspServerVersions = ['0.1.36'] +const supportedLspServerVersions = ['0.1.37'] const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node' From a082f075c949ca5151f2b243fc505f111f0e61a1 Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:41:40 -0800 Subject: [PATCH 09/40] Add context transparency feature to Q chat @workspace (#9) * Add context transparency feature to Q chat @workspace * remove console log * remove console log * remove unused FileClickMessage --- .../core/src/amazonq/lsp/lspController.ts | 6 ++ .../webview/ui/apps/cwChatConnector.ts | 11 ++ .../core/src/amazonq/webview/ui/connector.ts | 9 ++ packages/core/src/amazonq/webview/ui/main.ts | 24 +++++ .../codewhispererruntime-2022-11-11.json | 6 ++ .../codewhisperer/client/user-service-2.json | 6 ++ packages/core/src/codewhispererChat/app.ts | 8 ++ .../codewhispererChat/clients/chat/v0/chat.ts | 2 + .../controllers/chat/controller.ts | 102 +++++++++++++++++- .../controllers/chat/messenger/messenger.ts | 14 ++- .../controllers/chat/model.ts | 13 +++ .../view/connector/connector.ts | 4 + .../view/messages/messageListener.ts | 13 +++ .../src/models/models_0.ts | 2 + 14 files changed, 214 insertions(+), 6 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index b38696d0b10..6e7d2aee640 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -31,6 +31,8 @@ export interface Chunk { readonly context?: string readonly relativePath?: string readonly programmingLanguage?: string + readonly startLine?: number + readonly endLine?: number } export interface Content { @@ -292,11 +294,15 @@ export class LspController { programmingLanguage: { languageName: chunk.programmingLanguage, }, + startLine: chunk.startLine ?? -1, + endLine: chunk.endLine ?? -1, }) } else { resp.push({ text: text, relativeFilePath: chunk.relativePath ? chunk.relativePath : path.basename(chunk.filePath), + startLine: chunk.startLine ?? -1, + endLine: chunk.endLine ?? -1, }) } } diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index ed8872f825b..63eced89654 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -96,6 +96,7 @@ export class Connector extends BaseConnector { codeReference: messageData.codeReference, userIntent: messageData.userIntent, codeBlockLanguage: messageData.codeBlockLanguage, + contextList: messageData.contextList, } // If it is not there we will not set it @@ -230,4 +231,14 @@ export class Connector extends BaseConnector { tabID: tabId, }) } + + onFileClick = (tabID: string, filePath: string, messageId?: string) => { + this.sendMessageToExtension({ + command: 'file-click', + tabID, + messageId, + filePath, + tabType: 'cwc', + }) + } } diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index ae7878b93b4..0c1fd264338 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -62,6 +62,12 @@ export interface CWCChatItem extends ChatItem { traceId?: string userIntent?: UserIntent codeBlockLanguage?: string + contextList?: Context[] +} + +export interface Context { + relativeFilePath: string + lineRanges: Array<{ first: number; second: number }> // List of [startLine, endLine] tuples } export interface ConnectorProps { @@ -604,6 +610,9 @@ export class Connector { case 'doc': this.docChatConnector.onOpenDiff(tabID, filePath, deleted) break + case 'cwc': + this.cwChatConnector.onFileClick(tabID, filePath, messageId) + break } } diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 7d77c34bd76..452f087fc84 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -368,6 +368,30 @@ export const createMynahUI = ( return } + if (item.contextList !== undefined && item.contextList.length > 0) { + item.header = { + fileList: { + fileTreeTitle: '', + filePaths: item.contextList.map((file) => file.relativeFilePath), + rootFolderTitle: 'Context', + collapsedByDefault: true, + hideFileCount: true, + details: Object.fromEntries( + item.contextList.map((file) => [ + file.relativeFilePath, + { + label: file.lineRanges + .map((range) => `line ${range.first} - ${range.second}`) + .join(', '), + description: file.relativeFilePath, + clickable: true, + }, + ]) + ), + }, + } + } + if ( item.body !== undefined || item.relatedContent !== undefined || diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json index ff9670742ec..43537b6df2d 100644 --- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json +++ b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json @@ -2587,6 +2587,12 @@ }, "documentSymbols": { "shape": "DocumentSymbols" + }, + "startLine": { + "shape": "Integer" + }, + "endLine": { + "shape": "Integer" } } }, diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 1f33cb8c98c..eef28444f53 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -1907,6 +1907,12 @@ "documentSymbols": { "shape": "DocumentSymbols", "documentation": "

DocumentSymbols parsed from a text document

" + }, + "startLine": { + "shape": "Integer" + }, + "endLine": { + "shape": "Integer" } }, "documentation": "

Represents an IDE retrieved relevant Text Document / File

" diff --git a/packages/core/src/codewhispererChat/app.ts b/packages/core/src/codewhispererChat/app.ts index 2fe73c04a04..51e1a2184df 100644 --- a/packages/core/src/codewhispererChat/app.ts +++ b/packages/core/src/codewhispererChat/app.ts @@ -27,6 +27,7 @@ import { UIFocusMessage, AcceptDiff, QuickCommandGroupActionClick, + FileClick, } from './controllers/chat/model' import { EditorContextCommand, registerCommands } from './commands/registerCommands' import { ContextSelectedMessage, CustomFormActionMessage } from './view/connector/connector' @@ -54,6 +55,7 @@ export function init(appContext: AmazonQAppInitContext) { processQuickCommandGroupActionClicked: new EventEmitter(), processCustomFormAction: new EventEmitter(), processContextSelected: new EventEmitter(), + processFileClick: new EventEmitter(), } const cwChatControllerMessageListeners = { @@ -114,6 +116,9 @@ export function init(appContext: AmazonQAppInitContext) { processContextSelected: new MessageListener( cwChatControllerEventEmitters.processContextSelected ), + processFileClick: new MessageListener( + cwChatControllerEventEmitters.processFileClick + ), } const cwChatControllerMessagePublishers = { @@ -176,6 +181,9 @@ export function init(appContext: AmazonQAppInitContext) { processContextSelected: new MessagePublisher( cwChatControllerEventEmitters.processContextSelected ), + processFileClick: new MessagePublisher( + cwChatControllerEventEmitters.processFileClick + ), } new CwChatController( diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index b849b328bac..70235923ecc 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -14,6 +14,8 @@ import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWr export class ChatSession { private sessionId?: string + contexts: Map> = new Map() + currentContextId: number = 0 public get sessionIdentifier(): string | undefined { return this.sessionId } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index cb3b0ef0231..08d30f6e3f9 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as path from 'path' -import { Event as VSCodeEvent, Uri } from 'vscode' +import { Event as VSCodeEvent, Uri, workspace, window, ViewColumn, Position, Selection } from 'vscode' import { EditorContextExtractor } from '../../editor/context/extractor' import { ChatSessionStorage } from '../../storages/chatSession' import { Messenger, MessengerResponseType, StaticTextResponseType } from './messenger/messenger' @@ -28,6 +28,8 @@ import { ViewDiff, AcceptDiff, QuickCommandGroupActionClick, + MergedRelevantDocument, + FileClick, } from './model' import { AppToWebViewMessageDispatcher, @@ -41,7 +43,7 @@ import { EditorContextCommand } from '../../commands/registerCommands' import { PromptsGenerator } from './prompts/promptsGenerator' import { TriggerEventsStorage } from '../../storages/triggerEvents' import { SendMessageRequest } from '@amzn/amazon-q-developer-streaming-client' -import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' +import { CodeWhispererStreamingServiceException, RelevantTextDocument } from '@amzn/codewhisperer-streaming' import { UserIntentRecognizer } from './userIntent/userIntentRecognizer' import { CWCTelemetryHelper, recordTelemetryChatRunCommand } from './telemetryHelper' import { CodeWhispererTracker } from '../../../codewhisperer/tracker/codewhispererTracker' @@ -90,6 +92,7 @@ export interface ChatControllerMessagePublishers { readonly processQuickCommandGroupActionClicked: MessagePublisher readonly processCustomFormAction: MessagePublisher readonly processContextSelected: MessagePublisher + readonly processFileClick: MessagePublisher } export interface ChatControllerMessageListeners { @@ -114,6 +117,7 @@ export interface ChatControllerMessageListeners { readonly processQuickCommandGroupActionClicked: MessageListener readonly processCustomFormAction: MessageListener readonly processContextSelected: MessageListener + readonly processFileClick: MessageListener } export class ChatController { @@ -243,6 +247,9 @@ export class ChatController { this.chatControllerMessageListeners.processContextSelected.onMessage((data) => { return this.processContextSelected(data) }) + this.chatControllerMessageListeners.processFileClick.onMessage((data) => { + return this.processFileClickMessage(data) + }) } private processFooterInfoLinkClick(click: FooterInfoLinkClick) { @@ -524,6 +531,7 @@ export class ChatController { `Create a saved prompt` ) } + private processQuickCommandGroupActionClicked(message: QuickCommandGroupActionClick) { if (message.actionId === 'create-prompt') { this.handlePromptCreate(message.tabID) @@ -558,6 +566,36 @@ export class ChatController { this.handlePromptCreate(message.tabID) } } + private async processFileClickMessage(message: FileClick) { + let session = this.sessionStorage.getSession(message.tabID) + // TODO remove currentContextId but use messageID to track context for each answer message + const lineRanges = session.contexts.get(session.currentContextId)?.get(message.filePath) + + if (!lineRanges) { + return + } + const projectRoot = workspace.workspaceFolders?.[0]?.uri.fsPath + if (!projectRoot) { + return + } + + const absoluteFilePath = path.join(projectRoot, message.filePath) + + // Open the file in VSCode + const document = await workspace.openTextDocument(absoluteFilePath) + const editor = await window.showTextDocument(document, ViewColumn.Active) + + // Create multiple selections based on line ranges + const selections: Selection[] = lineRanges.map(({ first, second }) => { + const startPosition = new Position(first - 1, 0) // Convert 1-based to 0-based + const endPosition = new Position(second - 1, document.lineAt(second - 1).range.end.character) + return new Selection(startPosition.line, startPosition.character, endPosition.line, endPosition.character) + }) + + // Apply multiple selections to the editor using the new API + editor.selection = selections[0] // Set the first selection as active + editor.selections = selections // Apply multiple selections + } private processException(e: any, tabID: string) { let errorMessage = '' @@ -880,9 +918,12 @@ export class ChatController { if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { const start = performance.now() triggerPayload.relevantTextDocuments = await LspController.instance.query(triggerPayload.message) + triggerPayload.mergedRelevantDocuments = this.mergeRelevantTextDocuments( + triggerPayload.relevantTextDocuments + ) for (const doc of triggerPayload.relevantTextDocuments) { getLogger().info( - `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}` + `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}, start line: ${doc.startLine}, end line: ${doc.endLine}` ) } triggerPayload.projectContextQueryLatencyMs = performance.now() - start @@ -904,12 +945,25 @@ export class ChatController { }, { timeout: 500, interval: 200, truthy: false } ) + triggerPayload.mergedRelevantDocuments = this.mergeRelevantTextDocuments( + triggerPayload.relevantTextDocuments + ) triggerPayload.projectContextQueryLatencyMs = performance.now() - start } } const request = triggerPayloadToChatRequest(triggerPayload) const session = this.sessionStorage.getSession(tabID) + + session.currentContextId++ + session.contexts.set(session.currentContextId, new Map()) + triggerPayload.mergedRelevantDocuments?.forEach((doc) => { + const currentContext = session.contexts.get(session.currentContextId) + if (currentContext) { + currentContext.set(doc.relativeFilePath, doc.lineRanges) + } + }) + getLogger().info( `request from tab: ${tabID} conversationID: ${session.sessionIdentifier} request: ${inspect(request, { depth: 12, @@ -918,7 +972,7 @@ export class ChatController { let response: MessengerResponseType | undefined = undefined session.createNewTokenSource() try { - this.messenger.sendInitalStream(tabID, triggerID) + this.messenger.sendInitalStream(tabID, triggerID, triggerPayload.mergedRelevantDocuments) this.telemetryHelper.setConversationStreamStartTime(tabID) if (isSsoConnection(AuthUtil.instance.conn)) { const { $metadata, generateAssistantResponseResponse } = await session.chatSso(request) @@ -948,4 +1002,44 @@ export class ChatController { this.processException(e, tabID) } } + + private mergeRelevantTextDocuments( + documents: RelevantTextDocument[] | undefined + ): MergedRelevantDocument[] | undefined { + if (documents === undefined) { + return undefined + } + return Object.entries( + documents.reduce>((acc, doc) => { + if (!doc.relativeFilePath || doc.startLine === undefined || doc.endLine === undefined) { + return acc // Skip invalid documents + } + + if (!acc[doc.relativeFilePath]) { + acc[doc.relativeFilePath] = [] + } + acc[doc.relativeFilePath].push({ first: doc.startLine, second: doc.endLine }) + return acc + }, {}) + ).map(([filePath, ranges]) => { + // Sort by startLine + const sortedRanges = ranges.sort((a, b) => a.first - b.first) + + const mergedRanges: { first: number; second: number }[] = [] + for (const { first, second } of sortedRanges) { + if (mergedRanges.length === 0 || mergedRanges[mergedRanges.length - 1].second < first - 1) { + // If no overlap, add new range + mergedRanges.push({ first, second }) + } else { + // Merge overlapping or consecutive ranges + mergedRanges[mergedRanges.length - 1].second = Math.max( + mergedRanges[mergedRanges.length - 1].second, + second + ) + } + } + + return { relativeFilePath: filePath, lineRanges: mergedRanges } + }) + } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 79589657b94..dc4a5074fc4 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -25,7 +25,7 @@ import { ChatMessage, ErrorMessage, FollowUp, Suggestion } from '../../../view/c import { ChatSession } from '../../../clients/chat/v0/chat' import { ChatException } from './model' import { CWCTelemetryHelper } from '../telemetryHelper' -import { ChatPromptCommandType, TriggerPayload } from '../model' +import { ChatPromptCommandType, MergedRelevantDocument, TriggerPayload } from '../model' import { getHttpStatusCode, getRequestId, ToolkitError } from '../../../../shared/errors' import { keys } from '../../../../shared/utilities/tsUtils' import { getLogger } from '../../../../shared/logger/logger' @@ -66,7 +66,11 @@ export class Messenger { ) } - public sendInitalStream(tabID: string, triggerID: string) { + public sendInitalStream( + tabID: string, + triggerID: string, + mergedRelevantDocuments: MergedRelevantDocument[] | undefined + ) { this.dispatcher.sendChatMessage( new ChatMessage( { @@ -79,6 +83,7 @@ export class Messenger { messageID: '', userIntent: undefined, codeBlockLanguage: undefined, + contextList: mergedRelevantDocuments, }, tabID ) @@ -191,6 +196,7 @@ export class Messenger { messageID, userIntent: triggerPayload.userIntent, codeBlockLanguage: codeBlockLanguage, + contextList: undefined, }, tabID ) @@ -269,6 +275,7 @@ export class Messenger { messageID, userIntent: triggerPayload.userIntent, codeBlockLanguage: codeBlockLanguage, + contextList: undefined, }, tabID ) @@ -288,6 +295,7 @@ export class Messenger { messageID, userIntent: triggerPayload.userIntent, codeBlockLanguage: undefined, + contextList: undefined, }, tabID ) @@ -306,6 +314,7 @@ export class Messenger { messageID, userIntent: triggerPayload.userIntent, codeBlockLanguage: undefined, + contextList: undefined, }, tabID ) @@ -404,6 +413,7 @@ export class Messenger { messageID: 'static_message_' + triggerID, userIntent: undefined, codeBlockLanguage: undefined, + contextList: undefined, }, tabID ) diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 464e56f09f0..422c9d0beb4 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -147,6 +147,13 @@ export interface QuickCommandGroupActionClick { tabID: string } +export interface FileClick { + command: string + tabID: string + messageId: string + filePath: string +} + export interface ChatItemVotedMessage { tabID: string command: string @@ -182,10 +189,16 @@ export interface TriggerPayload { readonly context?: string[] | QuickActionCommand[] relevantTextDocuments?: RelevantTextDocument[] additionalContents?: AdditionalContentEntry[] + mergedRelevantDocuments?: MergedRelevantDocument[] useRelevantDocuments?: boolean traceId?: string } +export interface MergedRelevantDocument { + readonly relativeFilePath: string + readonly lineRanges: Array<{ first: number; second: number }> +} + export interface InsertedCode { readonly conversationID: string readonly messageID: string diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index 927c1880772..175cfbd17ad 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -8,6 +8,7 @@ import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' import { ChatItemButton, ChatItemFormItem, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' +import { MergedRelevantDocument } from '../../controllers/chat/model' class UiMessage { readonly time: number = Date.now() @@ -206,6 +207,7 @@ export interface ChatMessageProps { readonly messageID: string readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined + readonly contextList: MergedRelevantDocument[] | undefined } export class ChatMessage extends UiMessage { @@ -220,6 +222,7 @@ export class ChatMessage extends UiMessage { readonly messageID: string | undefined readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined + readonly contextList: MergedRelevantDocument[] | undefined override type = 'chatMessage' constructor(props: ChatMessageProps, tabID: string) { @@ -234,6 +237,7 @@ export class ChatMessage extends UiMessage { this.messageID = props.messageID this.userIntent = props.userIntent this.codeBlockLanguage = props.codeBlockLanguage + this.contextList = props.contextList } } diff --git a/packages/core/src/codewhispererChat/view/messages/messageListener.ts b/packages/core/src/codewhispererChat/view/messages/messageListener.ts index 8bc5e1e2d09..bb2871957c8 100644 --- a/packages/core/src/codewhispererChat/view/messages/messageListener.ts +++ b/packages/core/src/codewhispererChat/view/messages/messageListener.ts @@ -115,6 +115,10 @@ export class UIMessageListener { break case 'context-selected': this.processContextSelected(msg) + break + case 'file-click': + this.fileClick(msg) + break } } @@ -288,4 +292,13 @@ export class UIMessageListener { comment: msg.comment, }) } + + private fileClick(msg: any) { + this.chatControllerMessagePublishers.processFileClick.publish({ + messageId: msg.messageId, + tabID: msg.tabID, + command: msg.command, + filePath: msg.filePath, + }) + } } diff --git a/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts b/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts index 07e2794da2d..79ce76ec283 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts @@ -1019,6 +1019,8 @@ export interface RelevantTextDocument { * @public */ documentSymbols?: (DocumentSymbol)[] | undefined; + startLine?: number; + endLine?: number; } /** From 43156189eaaafae61affbb4637b2ed95de1ceba6 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:44:36 -0500 Subject: [PATCH 10/40] fix: add fs exists check for system prompts directory (#10) * feat(amazonq): saved prompts * fix: remove unecessary change * feat(amazonq): saved prompts * fix: remove unecessary change * fix: add fs exists check for ~/.aws/prompts * fix: whitespace --- .../controllers/chat/controller.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 08d30f6e3f9..815dc56a7b3 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -479,16 +479,19 @@ export class ChatController { // Check ~/.aws/prompts for saved prompts try { const systemPromptsDirectory = path.join(fs.getUserHomeDir(), '.aws', 'prompts') - const systemPromptFiles = await fs.readdir(systemPromptsDirectory) - promptsCmd.children?.[0].commands.push( - ...systemPromptFiles - .filter(([name]) => name.endsWith('.prompt')) - .map(([name]) => ({ - command: name.replace(/\.prompt$/, ''), - icon: 'magic' as MynahIconsType, - route: [systemPromptsDirectory, name], - })) - ) + const directoryExists = await fs.exists(systemPromptsDirectory) + if (directoryExists) { + const systemPromptFiles = await fs.readdir(systemPromptsDirectory) + promptsCmd.children?.[0].commands.push( + ...systemPromptFiles + .filter(([name]) => name.endsWith('.prompt')) + .map(([name]) => ({ + command: name.replace(/\.prompt$/, ''), + icon: 'magic' as MynahIconsType, + route: [systemPromptsDirectory, name], + })) + ) + } } catch (e) { getLogger().verbose(`Could not read prompts from ~/.aws/prompts: ${e}`) } From d0af6fe3329cf8ce3491eb064a5b621284fa1063 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:52:22 -0500 Subject: [PATCH 11/40] fix: @workspace command (#11) * fix: @workspace command * remove first instance of workspace from message --- .../controllers/chat/controller.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 815dc56a7b3..0bf82648f36 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -570,7 +570,7 @@ export class ChatController { } } private async processFileClickMessage(message: FileClick) { - let session = this.sessionStorage.getSession(message.tabID) + const session = this.sessionStorage.getSession(message.tabID) // TODO remove currentContextId but use messageID to track context for each answer message const lineRanges = session.contexts.get(session.currentContextId)?.get(message.filePath) @@ -915,9 +915,11 @@ export class ChatController { // TODO: resolve the context into real context up to 90k triggerPayload.useRelevantDocuments = false if (triggerPayload.message) { - triggerPayload.useRelevantDocuments = triggerPayload.message.includes(`@workspace`) + triggerPayload.useRelevantDocuments = triggerPayload.context?.some( + (context) => typeof context !== 'string' && context.command === '@workspace' + ) if (triggerPayload.useRelevantDocuments) { - triggerPayload.message = triggerPayload.message.replace(/@workspace/g, '') + triggerPayload.message = triggerPayload.message.replace(/workspace/, '') if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { const start = performance.now() triggerPayload.relevantTextDocuments = await LspController.instance.query(triggerPayload.message) @@ -960,12 +962,14 @@ export class ChatController { session.currentContextId++ session.contexts.set(session.currentContextId, new Map()) - triggerPayload.mergedRelevantDocuments?.forEach((doc) => { - const currentContext = session.contexts.get(session.currentContextId) - if (currentContext) { - currentContext.set(doc.relativeFilePath, doc.lineRanges) + if (triggerPayload.mergedRelevantDocuments) { + for (const doc of triggerPayload.mergedRelevantDocuments) { + const currentContext = session.contexts.get(session.currentContextId) + if (currentContext) { + currentContext.set(doc.relativeFilePath, doc.lineRanges) + } } - }) + } getLogger().info( `request from tab: ${tabID} conversationID: ${session.sessionIdentifier} request: ${inspect(request, { From 98078fe41472c92b261dc42ef5e24678dddf0eae Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:26:05 -0800 Subject: [PATCH 12/40] speed up loading the context commands (#12) --- packages/core/src/amazonq/lsp/lspClient.ts | 9 +-------- packages/core/src/amazonq/lsp/lspController.ts | 4 +++- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index dbf3d1a20f5..080ede81462 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -40,7 +40,7 @@ import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSet import { fs } from '../../shared/fs/fs' import { getLogger } from '../../shared/logger/logger' import globals from '../../shared/extensionGlobals' -import { sleep, waitUntil } from '../../shared/utilities/timeoutUtils' +import { waitUntil } from '../../shared/utilities/timeoutUtils' const localize = nls.loadMessageBundle() @@ -225,13 +225,6 @@ export class LspClient { async waitUntilReady() { return waitUntil( async () => { - for (let i = 0; i < 5; i++) { - if (this.client !== undefined) { - break - } else { - await sleep(5_000) - } - } if (this.client === undefined) { return false } diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 6e7d2aee640..96271d70788 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -24,6 +24,7 @@ import { ToolkitError } from '../../shared/errors' import { isWeb } from '../../shared/extensionGlobals' import { getUserAgent } from '../../shared/telemetry/util' import { isAmazonInternalOs } from '../../shared/vscode/env' +import { sleep } from '../../shared/utilities/timeoutUtils' export interface Chunk { readonly filePath: string @@ -399,7 +400,8 @@ export class LspController { try { await activateLsp(context) getLogger().info('LspController: LSP activated') - void LspController.instance.buildIndex(buildIndexConfig) + await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) + await sleep(5_000).then(void LspController.instance.buildIndex(buildIndexConfig)) // log the LSP server CPU and Memory usage per 30 minutes. globals.clock.setInterval( async () => { From 6ef5f35ce1020928bd3212fd0dcbfe5365a09ae9 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Tue, 18 Feb 2025 15:42:28 -0800 Subject: [PATCH 13/40] update lsp version --- packages/core/src/amazonq/lsp/lspController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 96271d70788..78eac99d676 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -61,9 +61,9 @@ export interface Manifest { targets: Target[] }[] } -const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.37.json' +const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.38.json' // this LSP client in Q extension is only going to work with these LSP server versions -const supportedLspServerVersions = ['0.1.37'] +const supportedLspServerVersions = ['0.1.38'] const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node' From 7bcbaf10ad9c276e7b42ad4924eabaa321692a7e Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Tue, 18 Feb 2025 20:23:35 -0500 Subject: [PATCH 14/40] fix: add feature flag command to context (#13) * fix: add feature flag command to context * fix: move into additional commands --- .../codewhispererChat/controllers/chat/controller.ts | 12 ++++++++++++ packages/core/src/shared/featureConfig.ts | 1 + 2 files changed, 13 insertions(+) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 0bf82648f36..edc6f5aa551 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -69,6 +69,7 @@ import { ContextCommandItem } from '../../../amazonq/lsp/types' import { createPromptCommand, workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import * as vscode from 'vscode' +import { FeatureConfigProvider, Features } from '../../../shared/featureConfig' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher @@ -439,6 +440,17 @@ export class ChatController { ], }, ] + + const feature = FeatureConfigProvider.getFeature(Features.highlightCommand) + const commandName = feature?.value.stringValue + if (commandName) { + const commandDescription = feature.variation + contextCommand.push({ + groupName: 'Additional Commands', + commands: [{ command: commandName, description: commandDescription }], + }) + } + const lspClientReady = await LspClient.instance.waitUntilReady() if (!lspClientReady) { return diff --git a/packages/core/src/shared/featureConfig.ts b/packages/core/src/shared/featureConfig.ts index dd8bbb901e5..4af806491e2 100644 --- a/packages/core/src/shared/featureConfig.ts +++ b/packages/core/src/shared/featureConfig.ts @@ -41,6 +41,7 @@ export const Features = { projectContextFeature: 'ProjectContextV2', workspaceContextFeature: 'WorkspaceContext', test: 'testFeature', + highlightCommand: 'highlightCommand', } as const export type FeatureName = (typeof Features)[keyof typeof Features] From 023c57a99e85305c5fd148d82a0e54bb05044136 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Tue, 18 Feb 2025 17:57:26 -0800 Subject: [PATCH 15/40] lsp version to 0.1.39 --- packages/core/src/amazonq/lsp/lspController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 78eac99d676..420cd5210d9 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -61,9 +61,9 @@ export interface Manifest { targets: Target[] }[] } -const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.38.json' +const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.39.json' // this LSP client in Q extension is only going to work with these LSP server versions -const supportedLspServerVersions = ['0.1.38'] +const supportedLspServerVersions = ['0.1.39'] const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node' From 276111e7d9ed90b4807de116eb013ab491178e00 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Tue, 18 Feb 2025 18:05:38 -0800 Subject: [PATCH 16/40] fix linter --- packages/core/src/amazonq/lsp/types.ts | 1 - packages/core/src/codewhispererChat/app.ts | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/core/src/amazonq/lsp/types.ts b/packages/core/src/amazonq/lsp/types.ts index 1020dd24ebc..59e1e069287 100644 --- a/packages/core/src/amazonq/lsp/types.ts +++ b/packages/core/src/amazonq/lsp/types.ts @@ -83,7 +83,6 @@ export const GetContextCommandItemsRequestType: RequestType = new RequestType( 'lsp/getIndexSequenceNumber' diff --git a/packages/core/src/codewhispererChat/app.ts b/packages/core/src/codewhispererChat/app.ts index 51e1a2184df..3ffa51b75eb 100644 --- a/packages/core/src/codewhispererChat/app.ts +++ b/packages/core/src/codewhispererChat/app.ts @@ -116,9 +116,7 @@ export function init(appContext: AmazonQAppInitContext) { processContextSelected: new MessageListener( cwChatControllerEventEmitters.processContextSelected ), - processFileClick: new MessageListener( - cwChatControllerEventEmitters.processFileClick - ), + processFileClick: new MessageListener(cwChatControllerEventEmitters.processFileClick), } const cwChatControllerMessagePublishers = { @@ -181,9 +179,7 @@ export function init(appContext: AmazonQAppInitContext) { processContextSelected: new MessagePublisher( cwChatControllerEventEmitters.processContextSelected ), - processFileClick: new MessagePublisher( - cwChatControllerEventEmitters.processFileClick - ), + processFileClick: new MessagePublisher(cwChatControllerEventEmitters.processFileClick), } new CwChatController( From 65d657c2a6b793bd0c2a2c1631f21e3004d0b906 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:28:54 -0500 Subject: [PATCH 17/40] fix: refresh prompt list when project prompt is added (#16) * fix: refresh prompt list when project prompt is added * fix: validate length and size of additionalContents * fix: truncate additionalContents based on api validation limits * fix: move constants --- .../core/src/amazonq/lsp/lspController.ts | 2 +- .../controllers/chat/controller.ts | 102 ++++++++++-------- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 420cd5210d9..774a464a1a8 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -61,7 +61,7 @@ export interface Manifest { targets: Target[] }[] } -const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.39.json' +const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions const supportedLspServerVersions = ['0.1.39'] diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index edc6f5aa551..63725a2ad1a 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -121,6 +121,12 @@ export interface ChatControllerMessageListeners { readonly processFileClick: MessageListener } +const promptFileExtension = '.prompt' + +const additionalContentInnerContextLimit = 8192 + +const aditionalContentNameLimit = 1024 + export class ChatController { private readonly sessionStorage: ChatSessionStorage private readonly triggerEventsStorage: TriggerEventsStorage @@ -450,45 +456,21 @@ export class ChatController { commands: [{ command: commandName, description: commandDescription }], }) } - - const lspClientReady = await LspClient.instance.waitUntilReady() - if (!lspClientReady) { - return - } - const contextCommandItems = await LspClient.instance.getContextCommandItems() - const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] - const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] const promptsCmd: QuickActionCommand = contextCommand[0].commands?.[3] - for (const contextCommandItem of contextCommandItems) { - const wsFolderName = path.basename(contextCommandItem.workspaceFolder) - if (contextCommandItem.type === 'file') { - filesCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - icon: 'file' as MynahIconsType, - }) + // Check .aws/prompts for prompt files in workspace + const workspacePromptFiles = await vscode.workspace.findFiles(`.aws/prompts/*${promptFileExtension}`) - // If file is a .prompt type, add to prompts list - if (contextCommandItem.relativePath.endsWith('.prompt')) { - promptsCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath, '.prompt'), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - icon: 'magic' as MynahIconsType, - }) - } - } else { - folderCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - icon: 'folder' as MynahIconsType, - }) - } + if (workspacePromptFiles.length > 0) { + promptsCmd.children?.[0].commands.push( + ...workspacePromptFiles.map((file) => ({ + command: path.basename(file.path, promptFileExtension), + icon: 'magic' as MynahIconsType, + route: [path.dirname(file.path), path.basename(file.path)], + })) + ) } - - // Check ~/.aws/prompts for saved prompts + // Check ~/.aws/prompts for global prompt files try { const systemPromptsDirectory = path.join(fs.getUserHomeDir(), '.aws', 'prompts') const directoryExists = await fs.exists(systemPromptsDirectory) @@ -496,9 +478,9 @@ export class ChatController { const systemPromptFiles = await fs.readdir(systemPromptsDirectory) promptsCmd.children?.[0].commands.push( ...systemPromptFiles - .filter(([name]) => name.endsWith('.prompt')) + .filter(([name]) => name.endsWith(promptFileExtension)) .map(([name]) => ({ - command: name.replace(/\.prompt$/, ''), + command: path.basename(name, promptFileExtension), icon: 'magic' as MynahIconsType, route: [systemPromptsDirectory, name], })) @@ -511,6 +493,32 @@ export class ChatController { // Add create prompt button to the bottom of the prompts list promptsCmd.children?.[0].commands.push({ command: createPromptCommand, icon: 'list-add' as MynahIconsType }) + const lspClientReady = await LspClient.instance.waitUntilReady() + if (lspClientReady) { + const contextCommandItems = await LspClient.instance.getContextCommandItems() + const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] + const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] + + for (const contextCommandItem of contextCommandItems) { + const wsFolderName = path.basename(contextCommandItem.workspaceFolder) + if (contextCommandItem.type === 'file') { + filesCmd.children?.[0].commands.push({ + command: path.basename(contextCommandItem.relativePath), + description: path.join(wsFolderName, contextCommandItem.relativePath), + route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + icon: 'file' as MynahIconsType, + }) + } else { + folderCmd.children?.[0].commands.push({ + command: path.basename(contextCommandItem.relativePath), + description: path.join(wsFolderName, contextCommandItem.relativePath), + route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + icon: 'folder' as MynahIconsType, + }) + } + } + } + this.messenger.sendContextCommandData(contextCommand) } @@ -532,7 +540,7 @@ export class ChatController { title: 'Save globally for all projects?', mandatory: true, value: 'system', - description: 'If yes is selected, .prompt file will be saved in ~/.aws/prompts.', + description: `If yes is selected, ${promptFileExtension} file will be saved in ~/.aws/prompts.`, options: [ { value: 'project', label: 'No' }, { value: 'system', label: 'Yes' }, @@ -566,7 +574,10 @@ export class ChatController { } const title = message.action.formItemValues?.['prompt-name'] - const newFilePath = path.join(promptsDirectory, title ? `${title}.prompt` : 'default.prompt') + const newFilePath = path.join( + promptsDirectory, + title ? `${title}${promptFileExtension}` : `default${promptFileExtension}` + ) const newFileContent = new Uint8Array(Buffer.from('')) await fs.writeFile(newFilePath, newFileContent) const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) @@ -878,11 +889,14 @@ export class ChatController { if (prompts.length > 0) { triggerPayload.additionalContents = [] for (const prompt of prompts) { - triggerPayload.additionalContents.push({ - name: prompt.name, - description: prompt.description, - innerContext: prompt.content, - }) + // Todo: add mechanism for sorting/prioritization of additional context + if (triggerPayload.additionalContents.length < 20) { + triggerPayload.additionalContents.push({ + name: prompt.name.substring(0, aditionalContentNameLimit), + description: prompt.description.substring(0, aditionalContentNameLimit), + innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), + }) + } } getLogger().info( `Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} ` From fbd40c8583751fa7bb047b04af77981291946f12 Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:09:51 -0800 Subject: [PATCH 18/40] context transparency multiple fixes (#17) * context transparency multiple fixes 1. add manually selected context from prompt into context list 2. fix -1 start/end line edge cases 3. Center selction after clicking on the file * remove console.log --- .../core/src/amazonq/lsp/lspController.ts | 6 +- packages/core/src/amazonq/webview/ui/main.ts | 7 +- .../codewhisperer/client/user-service-2.json | 6 -- .../controllers/chat/controller.ts | 65 ++++++++++++++----- .../controllers/chat/model.ts | 5 +- 5 files changed, 60 insertions(+), 29 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 774a464a1a8..162f7d6ff5c 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -13,7 +13,6 @@ import fetch from 'node-fetch' import request from '../../shared/request' import { LspClient } from './lspClient' import AdmZip from 'adm-zip' -import { RelevantTextDocument } from '@amzn/codewhisperer-streaming' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' import { activate as activateLsp } from './lspClient' import { telemetry } from '../../shared/telemetry/telemetry' @@ -25,6 +24,7 @@ import { isWeb } from '../../shared/extensionGlobals' import { getUserAgent } from '../../shared/telemetry/util' import { isAmazonInternalOs } from '../../shared/vscode/env' import { sleep } from '../../shared/utilities/timeoutUtils' +import { RelevantTextDocumentAddition } from '../../codewhispererChat/controllers/chat/model' export interface Chunk { readonly filePath: string @@ -282,9 +282,9 @@ export class LspController { } } - async query(s: string): Promise { + async query(s: string): Promise { const chunks: Chunk[] | undefined = await LspClient.instance.queryVectorIndex(s) - const resp: RelevantTextDocument[] = [] + const resp: RelevantTextDocumentAddition[] = [] if (chunks) { for (const chunk of chunks) { const text = chunk.context ? chunk.context : chunk.content diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 452f087fc84..2305f35c734 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -351,6 +351,7 @@ export const createMynahUI = ( ...(item.relatedContent !== undefined ? { relatedContent: item.relatedContent } : {}), ...(item.followUp !== undefined ? { followUp: item.followUp } : {}), ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), + ...(item.header !== undefined ? { header: item.header } : {}), }) if ( item.messageId !== undefined && @@ -381,7 +382,11 @@ export const createMynahUI = ( file.relativeFilePath, { label: file.lineRanges - .map((range) => `line ${range.first} - ${range.second}`) + .map((range) => + range.first === -1 || range.second === -1 + ? '' + : `line ${range.first} - ${range.second}` + ) .join(', '), description: file.relativeFilePath, clickable: true, diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index eef28444f53..1f33cb8c98c 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -1907,12 +1907,6 @@ "documentSymbols": { "shape": "DocumentSymbols", "documentation": "

DocumentSymbols parsed from a text document

" - }, - "startLine": { - "shape": "Integer" - }, - "endLine": { - "shape": "Integer" } }, "documentation": "

Represents an IDE retrieved relevant Text Document / File

" diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 63725a2ad1a..c31d822179c 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -30,6 +30,7 @@ import { QuickCommandGroupActionClick, MergedRelevantDocument, FileClick, + RelevantTextDocumentAddition, } from './model' import { AppToWebViewMessageDispatcher, @@ -43,7 +44,7 @@ import { EditorContextCommand } from '../../commands/registerCommands' import { PromptsGenerator } from './prompts/promptsGenerator' import { TriggerEventsStorage } from '../../storages/triggerEvents' import { SendMessageRequest } from '@amzn/amazon-q-developer-streaming-client' -import { CodeWhispererStreamingServiceException, RelevantTextDocument } from '@amzn/codewhisperer-streaming' +import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' import { UserIntentRecognizer } from './userIntent/userIntentRecognizer' import { CWCTelemetryHelper, recordTelemetryChatRunCommand } from './telemetryHelper' import { CodeWhispererTracker } from '../../../codewhisperer/tracker/codewhispererTracker' @@ -600,6 +601,8 @@ export class ChatController { if (!lineRanges) { return } + + // TODO: Fix for multiple workspace setup const projectRoot = workspace.workspaceFolders?.[0]?.uri.fsPath if (!projectRoot) { return @@ -612,15 +615,25 @@ export class ChatController { const editor = await window.showTextDocument(document, ViewColumn.Active) // Create multiple selections based on line ranges - const selections: Selection[] = lineRanges.map(({ first, second }) => { - const startPosition = new Position(first - 1, 0) // Convert 1-based to 0-based - const endPosition = new Position(second - 1, document.lineAt(second - 1).range.end.character) - return new Selection(startPosition.line, startPosition.character, endPosition.line, endPosition.character) - }) + const selections: Selection[] = lineRanges + .filter(({ first, second }) => first !== -1 && second !== -1) + .map(({ first, second }) => { + const startPosition = new Position(first - 1, 0) // Convert 1-based to 0-based + const endPosition = new Position(second - 1, document.lineAt(second - 1).range.end.character) + return new Selection( + startPosition.line, + startPosition.character, + endPosition.line, + endPosition.character + ) + }) // Apply multiple selections to the editor using the new API - editor.selection = selections[0] // Set the first selection as active - editor.selections = selections // Apply multiple selections + if (selections.length > 0) { + editor.selection = selections[0] // Set the first selection as active + editor.selections = selections // Apply multiple selections + editor.revealRange(selections[0], vscode.TextEditorRevealType.InCenter) + } } private processException(e: any, tabID: string) { @@ -868,11 +881,12 @@ export class ChatController { this.messenger.sendStaticTextResponse(responseType, triggerID, tabID) } - private async resolveContextCommandPayload(triggerPayload: TriggerPayload) { + private async resolveContextCommandPayload(triggerPayload: TriggerPayload): Promise { if (triggerPayload.context === undefined || triggerPayload.context.length === 0) { - return + return [] } const contextCommands: ContextCommandItem[] = [] + const relativePaths: string[] = [] for (const context of triggerPayload.context) { if (typeof context !== 'string' && context.route && context.route.length === 2) { contextCommands.push({ @@ -880,10 +894,11 @@ export class ChatController { type: context.icon === 'folder' ? 'folder' : 'file', relativePath: context.route?.[1] || '', }) + relativePaths.push(context.route[1]) } } if (contextCommands.length === 0) { - return + return [] } const prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) if (prompts.length > 0) { @@ -902,6 +917,7 @@ export class ChatController { `Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} ` ) } + return relativePaths } private async generateResponse( @@ -937,7 +953,7 @@ export class ChatController { return } - await this.resolveContextCommandPayload(triggerPayload) + const relativePaths = await this.resolveContextCommandPayload(triggerPayload) // TODO: resolve the context into real context up to 90k triggerPayload.useRelevantDocuments = false if (triggerPayload.message) { @@ -988,11 +1004,24 @@ export class ChatController { session.currentContextId++ session.contexts.set(session.currentContextId, new Map()) - if (triggerPayload.mergedRelevantDocuments) { - for (const doc of triggerPayload.mergedRelevantDocuments) { - const currentContext = session.contexts.get(session.currentContextId) - if (currentContext) { - currentContext.set(doc.relativeFilePath, doc.lineRanges) + if (triggerPayload.mergedRelevantDocuments !== undefined) { + const relativePathsOfMergedRelevantDocuments = triggerPayload.mergedRelevantDocuments.map( + (doc) => doc.relativeFilePath + ) + for (const relativePath of relativePaths) { + if (!relativePathsOfMergedRelevantDocuments.includes(relativePath)) { + triggerPayload.mergedRelevantDocuments.push({ + relativeFilePath: relativePath, + lineRanges: [{ first: -1, second: -1 }], + }) + } + } + if (triggerPayload.mergedRelevantDocuments) { + for (const doc of triggerPayload.mergedRelevantDocuments) { + const currentContext = session.contexts.get(session.currentContextId) + if (currentContext) { + currentContext.set(doc.relativeFilePath, doc.lineRanges) + } } } } @@ -1037,7 +1066,7 @@ export class ChatController { } private mergeRelevantTextDocuments( - documents: RelevantTextDocument[] | undefined + documents: RelevantTextDocumentAddition[] | undefined ): MergedRelevantDocument[] | undefined { if (documents === undefined) { return undefined diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 422c9d0beb4..72b27a1c43b 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -187,13 +187,16 @@ export interface TriggerPayload { readonly userIntent: UserIntent | undefined readonly customization: Customization readonly context?: string[] | QuickActionCommand[] - relevantTextDocuments?: RelevantTextDocument[] + relevantTextDocuments?: RelevantTextDocumentAddition[] additionalContents?: AdditionalContentEntry[] mergedRelevantDocuments?: MergedRelevantDocument[] useRelevantDocuments?: boolean traceId?: string } +// TODO move this to API definition (or just use this across the codebase) +export type RelevantTextDocumentAddition = RelevantTextDocument & { startLine: number; endLine: number } + export interface MergedRelevantDocument { readonly relativeFilePath: string readonly lineRanges: Array<{ first: number; second: number }> From dc13da9a231ef111e9be5ead175f8e894bd80259 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:26:05 -0800 Subject: [PATCH 19/40] disable ws (#19) --- .../controllers/chat/controller.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index c31d822179c..a570dbfd2a0 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -63,7 +63,6 @@ import { isSsoConnection } from '../../../auth/connection' import { inspect } from '../../../shared/utilities/collectionUtils' import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' -import { waitUntil } from '../../../shared/utilities/timeoutUtils' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' import { LspClient } from '../../../amazonq/lsp/lspClient' import { ContextCommandItem } from '../../../amazonq/lsp/types' @@ -978,24 +977,6 @@ export class ChatController { this.messenger.sendOpenSettingsMessage(triggerID, tabID) return } - } else if ( - !LspController.instance.isIndexingInProgress() && - CodeWhispererSettings.instance.isLocalIndexEnabled() - ) { - const start = performance.now() - triggerPayload.relevantTextDocuments = await waitUntil( - async function () { - if (triggerPayload.message) { - return await LspController.instance.query(triggerPayload.message) - } - return [] - }, - { timeout: 500, interval: 200, truthy: false } - ) - triggerPayload.mergedRelevantDocuments = this.mergeRelevantTextDocuments( - triggerPayload.relevantTextDocuments - ) - triggerPayload.projectContextQueryLatencyMs = performance.now() - start } } From d10327bce64a369bb6bbca23765c56fc80bd6ecb Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:07:42 -0800 Subject: [PATCH 20/40] enable context transparency for folder case (#20) --- .../core/src/amazonq/lsp/lspController.ts | 4 +- packages/core/src/amazonq/lsp/types.ts | 2 + .../controllers/chat/controller.ts | 56 ++++++++++--------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 162f7d6ff5c..7d1e49b9aba 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -61,9 +61,9 @@ export interface Manifest { targets: Target[] }[] } -const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' +const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.40.json' // this LSP client in Q extension is only going to work with these LSP server versions -const supportedLspServerVersions = ['0.1.39'] +const supportedLspServerVersions = ['0.1.40'] const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node' diff --git a/packages/core/src/amazonq/lsp/types.ts b/packages/core/src/amazonq/lsp/types.ts index 59e1e069287..1da8dfb00de 100644 --- a/packages/core/src/amazonq/lsp/types.ts +++ b/packages/core/src/amazonq/lsp/types.ts @@ -113,4 +113,6 @@ export interface AdditionalContextPrompt { description: string startLine: number endLine: number + filePath: string + relativePath: string } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index a570dbfd2a0..a23ee8739a9 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -609,30 +609,32 @@ export class ChatController { const absoluteFilePath = path.join(projectRoot, message.filePath) - // Open the file in VSCode - const document = await workspace.openTextDocument(absoluteFilePath) - const editor = await window.showTextDocument(document, ViewColumn.Active) - - // Create multiple selections based on line ranges - const selections: Selection[] = lineRanges - .filter(({ first, second }) => first !== -1 && second !== -1) - .map(({ first, second }) => { - const startPosition = new Position(first - 1, 0) // Convert 1-based to 0-based - const endPosition = new Position(second - 1, document.lineAt(second - 1).range.end.character) - return new Selection( - startPosition.line, - startPosition.character, - endPosition.line, - endPosition.character - ) - }) + try { + // Open the file in VSCode + const document = await workspace.openTextDocument(absoluteFilePath) + const editor = await window.showTextDocument(document, ViewColumn.Active) + + // Create multiple selections based on line ranges + const selections: Selection[] = lineRanges + .filter(({ first, second }) => first !== -1 && second !== -1) + .map(({ first, second }) => { + const startPosition = new Position(first - 1, 0) // Convert 1-based to 0-based + const endPosition = new Position(second - 1, document.lineAt(second - 1).range.end.character) + return new Selection( + startPosition.line, + startPosition.character, + endPosition.line, + endPosition.character + ) + }) - // Apply multiple selections to the editor using the new API - if (selections.length > 0) { - editor.selection = selections[0] // Set the first selection as active - editor.selections = selections // Apply multiple selections - editor.revealRange(selections[0], vscode.TextEditorRevealType.InCenter) - } + // Apply multiple selections to the editor + if (selections.length > 0) { + editor.selection = selections[0] // Set the first selection as active + editor.selections = selections // Apply multiple selections + editor.revealRange(selections[0], vscode.TextEditorRevealType.InCenter) + } + } catch (error) {} } private processException(e: any, tabID: string) { @@ -889,16 +891,16 @@ export class ChatController { for (const context of triggerPayload.context) { if (typeof context !== 'string' && context.route && context.route.length === 2) { contextCommands.push({ - workspaceFolder: context.route?.[0] || '', + workspaceFolder: context.route[0] || '', type: context.icon === 'folder' ? 'folder' : 'file', - relativePath: context.route?.[1] || '', + relativePath: context.route[1] || '', }) - relativePaths.push(context.route[1]) } } if (contextCommands.length === 0) { return [] } + const workspaceFolder = contextCommands[0].workspaceFolder const prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) if (prompts.length > 0) { triggerPayload.additionalContents = [] @@ -910,6 +912,8 @@ export class ChatController { description: prompt.description.substring(0, aditionalContentNameLimit), innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), }) + const relativePath = path.relative(workspaceFolder, prompt.filePath) + relativePaths.push(relativePath) } } getLogger().info( From 7d80fd3285df468864987c81837c4661b22eafc5 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Wed, 19 Feb 2025 15:13:40 -0800 Subject: [PATCH 21/40] promot version to 1.49.0 --- packages/amazonq/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 1e53ae80656..fe3d8b872be 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.48.0-FALCON", + "version": "1.49.0-FALCON", "extensionKind": [ "workspace" ], From aef05242d6fbba61fafa2db71ded86cd89903dca Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:32:41 -0800 Subject: [PATCH 22/40] fix no context without @workspace but with explicit mention (#21) --- package-lock.json | 2 +- .../core/src/codewhispererChat/controllers/chat/controller.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b24b5841df9..36348c7b29b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18891,7 +18891,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.48.0-FALCON", + "version": "1.49.0-FALCON", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index a23ee8739a9..1d88e5b14ac 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -959,6 +959,7 @@ export class ChatController { const relativePaths = await this.resolveContextCommandPayload(triggerPayload) // TODO: resolve the context into real context up to 90k triggerPayload.useRelevantDocuments = false + triggerPayload.mergedRelevantDocuments = [] if (triggerPayload.message) { triggerPayload.useRelevantDocuments = triggerPayload.context?.some( (context) => typeof context !== 'string' && context.command === '@workspace' From b030acfa280a55dfd76f3d83f5bbfb5c0cc58bc5 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:22:37 -0500 Subject: [PATCH 23/40] fix: clicking on workspace prompt files in context transparency (#22) --- .../controllers/chat/controller.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 1d88e5b14ac..dd4384810ba 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -463,11 +463,15 @@ export class ChatController { if (workspacePromptFiles.length > 0) { promptsCmd.children?.[0].commands.push( - ...workspacePromptFiles.map((file) => ({ - command: path.basename(file.path, promptFileExtension), - icon: 'magic' as MynahIconsType, - route: [path.dirname(file.path), path.basename(file.path)], - })) + ...workspacePromptFiles.map((file) => { + const workspacePath = vscode.workspace.getWorkspaceFolder(file)?.uri.path || path.dirname(file.path) + const relativePath = path.relative(workspacePath, file.path) + return { + command: path.basename(file.path, promptFileExtension), + icon: 'magic' as MynahIconsType, + route: [workspacePath, relativePath], + } + }) ) } // Check ~/.aws/prompts for global prompt files From 8c41430924498719ce920762ef94d0b201acda23 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:48:18 -0500 Subject: [PATCH 24/40] feat: add automatic workspace rules and fix clicking on user prompt (#23) * feat: add automatic workspace rules and fix clicking on user prompt in context transparency * add comments to code * fix: display os specific path in create prompt dialog --- .../controllers/chat/controller.ts | 113 +++++++++--------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index dd4384810ba..16e95237e38 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -65,7 +65,7 @@ import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' import { LspClient } from '../../../amazonq/lsp/lspClient' -import { ContextCommandItem } from '../../../amazonq/lsp/types' +import { ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types' import { createPromptCommand, workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import * as vscode from 'vscode' @@ -121,12 +121,16 @@ export interface ChatControllerMessageListeners { readonly processFileClick: MessageListener } -const promptFileExtension = '.prompt' +const promptFileExtension = '.prompt.md' const additionalContentInnerContextLimit = 8192 const aditionalContentNameLimit = 1024 +const getUserPromptsDirectory = () => { + return path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'prompts') +} + export class ChatController { private readonly sessionStorage: ChatSessionStorage private readonly triggerEventsStorage: TriggerEventsStorage @@ -458,35 +462,19 @@ export class ChatController { } const promptsCmd: QuickActionCommand = contextCommand[0].commands?.[3] - // Check .aws/prompts for prompt files in workspace - const workspacePromptFiles = await vscode.workspace.findFiles(`.aws/prompts/*${promptFileExtension}`) - - if (workspacePromptFiles.length > 0) { - promptsCmd.children?.[0].commands.push( - ...workspacePromptFiles.map((file) => { - const workspacePath = vscode.workspace.getWorkspaceFolder(file)?.uri.path || path.dirname(file.path) - const relativePath = path.relative(workspacePath, file.path) - return { - command: path.basename(file.path, promptFileExtension), - icon: 'magic' as MynahIconsType, - route: [workspacePath, relativePath], - } - }) - ) - } - // Check ~/.aws/prompts for global prompt files + // Check for user prompts try { - const systemPromptsDirectory = path.join(fs.getUserHomeDir(), '.aws', 'prompts') - const directoryExists = await fs.exists(systemPromptsDirectory) + const userPromptsDirectory = getUserPromptsDirectory() + const directoryExists = await fs.exists(userPromptsDirectory) if (directoryExists) { - const systemPromptFiles = await fs.readdir(systemPromptsDirectory) + const systemPromptFiles = await fs.readdir(userPromptsDirectory) promptsCmd.children?.[0].commands.push( ...systemPromptFiles .filter(([name]) => name.endsWith(promptFileExtension)) .map(([name]) => ({ command: path.basename(name, promptFileExtension), icon: 'magic' as MynahIconsType, - route: [systemPromptsDirectory, name], + route: [userPromptsDirectory, name], })) ) } @@ -536,19 +524,7 @@ export class ChatController { mandatory: true, title: 'Prompt name', placeholder: 'Enter prompt name', - description: 'Use this prompt in the chat by typing `@` followed by the prompt name.', - }, - { - id: 'shared-scope', - type: 'select', - title: 'Save globally for all projects?', - mandatory: true, - value: 'system', - description: `If yes is selected, ${promptFileExtension} file will be saved in ~/.aws/prompts.`, - options: [ - { value: 'project', label: 'No' }, - { value: 'system', label: 'Yes' }, - ], + description: `Use this prompt by typing \`@\` followed by the prompt name. Prompt will be saved in ${getUserPromptsDirectory()}.`, }, ], [ @@ -568,18 +544,11 @@ export class ChatController { private async processCustomFormAction(message: CustomFormActionMessage) { if (message.tabID) { if (message.action.id === 'submit-create-prompt') { - let promptsDirectory = path.join(fs.getUserHomeDir(), '.aws', 'prompts') - if ( - vscode.workspace.workspaceFolders?.[0] && - message.action.formItemValues?.['shared-scope'] === 'project' - ) { - const workspaceUri = vscode.workspace.workspaceFolders[0].uri - promptsDirectory = vscode.Uri.joinPath(workspaceUri, '.aws', 'prompts').fsPath - } + const userPromptsDirectory = getUserPromptsDirectory() const title = message.action.formItemValues?.['prompt-name'] const newFilePath = path.join( - promptsDirectory, + userPromptsDirectory, title ? `${title}${promptFileExtension}` : `default${promptFileExtension}` ) const newFileContent = new Uint8Array(Buffer.from('')) @@ -610,8 +579,16 @@ export class ChatController { if (!projectRoot) { return } - - const absoluteFilePath = path.join(projectRoot, message.filePath) + let absoluteFilePath = path.join(projectRoot, message.filePath) + + // Handle clicking on a user prompt outside the workspace + if (message.filePath.endsWith(promptFileExtension)) { + try { + await vscode.workspace.fs.stat(vscode.Uri.file(absoluteFilePath)) + } catch { + absoluteFilePath = path.join(getUserPromptsDirectory(), message.filePath) + } + } try { // Open the file in VSCode @@ -887,18 +864,36 @@ export class ChatController { } private async resolveContextCommandPayload(triggerPayload: TriggerPayload): Promise { - if (triggerPayload.context === undefined || triggerPayload.context.length === 0) { - return [] - } const contextCommands: ContextCommandItem[] = [] const relativePaths: string[] = [] - for (const context of triggerPayload.context) { - if (typeof context !== 'string' && context.route && context.route.length === 2) { - contextCommands.push({ - workspaceFolder: context.route[0] || '', - type: context.icon === 'folder' ? 'folder' : 'file', - relativePath: context.route[1] || '', + + // Check for workspace rules to add to context + const workspaceRules = await vscode.workspace.findFiles(`.amazonq/rules/*${promptFileExtension}`) + if (workspaceRules.length > 0) { + contextCommands.push( + ...workspaceRules.map((rule) => { + const workspaceFolderPath = vscode.workspace.getWorkspaceFolder(rule)?.uri?.path || '' + return { + workspaceFolder: workspaceFolderPath, + // todo: add 'prompt' type to LSP model + type: 'file' as ContextCommandItemType, + relativePath: path.relative(workspaceFolderPath, rule.path), + } }) + ) + } + + // Add context commands added by user to context + if (triggerPayload.context !== undefined && triggerPayload.context.length > 0) { + for (const context of triggerPayload.context) { + // todo: add handling of 'prompt' type (dependent on LSP changes) + if (typeof context !== 'string' && context.route && context.route.length === 2) { + contextCommands.push({ + workspaceFolder: context.route[0] || '', + type: context.icon === 'folder' ? 'folder' : 'file', + relativePath: context.route[1] || '', + }) + } } } if (contextCommands.length === 0) { @@ -916,7 +911,11 @@ export class ChatController { description: prompt.description.substring(0, aditionalContentNameLimit), innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), }) - const relativePath = path.relative(workspaceFolder, prompt.filePath) + let relativePath = path.relative(workspaceFolder, prompt.filePath) + // Handle user prompts outside the workspace + if (prompt.filePath.startsWith(getUserPromptsDirectory())) { + relativePath = path.basename(prompt.filePath) + } relativePaths.push(relativePath) } } From 584207bd31173d41b1d9a9703ee024c262caa9af Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:57:19 -0500 Subject: [PATCH 25/40] feat: add watcher for user prompts (#24) --- .../controllers/chat/controller.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 16e95237e38..d45fef09b91 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -140,6 +140,7 @@ export class ChatController { private readonly promptGenerator: PromptsGenerator private readonly userIntentRecognizer: UserIntentRecognizer private readonly telemetryHelper: CWCTelemetryHelper + private userPromptsWatcher: vscode.FileSystemWatcher | undefined public constructor( private readonly chatControllerMessageListeners: ChatControllerMessageListeners, @@ -263,6 +264,21 @@ export class ChatController { }) } + private registerUserPromptsWatcher() { + if (this.userPromptsWatcher) { + return + } + this.userPromptsWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(vscode.Uri.file(getUserPromptsDirectory()), `*${promptFileExtension}`), + false, + true, + false + ) + this.userPromptsWatcher.onDidCreate(() => this.processContextCommandUpdateMessage()) + this.userPromptsWatcher.onDidDelete(() => this.processContextCommandUpdateMessage()) + globals.context.subscriptions.push(this.userPromptsWatcher) + } + private processFooterInfoLinkClick(click: FooterInfoLinkClick) { this.openLinkInExternalBrowser(click) } @@ -403,6 +419,7 @@ export class ChatController { private async processContextCommandUpdateMessage() { // when UI is ready, refresh the context commands + this.registerUserPromptsWatcher() const contextCommand: MynahUIDataModel['contextCommands'] = [ { commands: [ @@ -555,7 +572,6 @@ export class ChatController { await fs.writeFile(newFilePath, newFileContent) const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) await vscode.window.showTextDocument(newFileDoc) - await this.processContextCommandUpdateMessage() } } } From d87152c515f587fd7d5c7cbc9d17bbc383020b55 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 21 Feb 2025 11:13:17 -0800 Subject: [PATCH 26/40] fix on feedback --- packages/amazonq/package.json | 1 - packages/core/src/amazonq/lsp/lspController.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 115bd38e845..5b54baffe0d 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,6 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.49.0-SNAPSHOT", "extensionKind": [ "workspace" diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index 7d1e49b9aba..e67d8b5ddf3 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -23,7 +23,6 @@ import { ToolkitError } from '../../shared/errors' import { isWeb } from '../../shared/extensionGlobals' import { getUserAgent } from '../../shared/telemetry/util' import { isAmazonInternalOs } from '../../shared/vscode/env' -import { sleep } from '../../shared/utilities/timeoutUtils' import { RelevantTextDocumentAddition } from '../../codewhispererChat/controllers/chat/model' export interface Chunk { @@ -401,7 +400,7 @@ export class LspController { await activateLsp(context) getLogger().info('LspController: LSP activated') await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - await sleep(5_000).then(void LspController.instance.buildIndex(buildIndexConfig)) + void LspController.instance.buildIndex(buildIndexConfig) // log the LSP server CPU and Memory usage per 30 minutes. globals.clock.setInterval( async () => { From f76964648ef8bb3b874ac8df5450a4eed5e30675 Mon Sep 17 00:00:00 2001 From: Avi Alpert Date: Fri, 21 Feb 2025 14:29:05 -0500 Subject: [PATCH 27/40] fix: remove unecessary conditional check --- .../controllers/chat/controller.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index d45fef09b91..5e87aded2ef 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -559,20 +559,18 @@ export class ChatController { } private async processCustomFormAction(message: CustomFormActionMessage) { - if (message.tabID) { - if (message.action.id === 'submit-create-prompt') { - const userPromptsDirectory = getUserPromptsDirectory() - - const title = message.action.formItemValues?.['prompt-name'] - const newFilePath = path.join( - userPromptsDirectory, - title ? `${title}${promptFileExtension}` : `default${promptFileExtension}` - ) - const newFileContent = new Uint8Array(Buffer.from('')) - await fs.writeFile(newFilePath, newFileContent) - const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) - await vscode.window.showTextDocument(newFileDoc) - } + if (message.action.id === 'submit-create-prompt') { + const userPromptsDirectory = getUserPromptsDirectory() + + const title = message.action.formItemValues?.['prompt-name'] + const newFilePath = path.join( + userPromptsDirectory, + title ? `${title}${promptFileExtension}` : `default${promptFileExtension}` + ) + const newFileContent = new Uint8Array(Buffer.from('')) + await fs.writeFile(newFilePath, newFileContent) + const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) + await vscode.window.showTextDocument(newFileDoc) } } From 23fed8cc4e4318ca8d8d7c67dfb274396671bdd4 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:15:12 -0800 Subject: [PATCH 28/40] config(amazonq): update LSP endpoint to prod (#6663) ## Problem ## Solution update LSP endpoint to prod --- - 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. --- packages/core/src/amazonq/lsp/lspController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index e67d8b5ddf3..f843a2bc670 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -60,7 +60,7 @@ export interface Manifest { targets: Target[] }[] } -const manifestUrl = 'https://ducvaeoffl85c.cloudfront.net/manifest-0.1.40.json' +const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions const supportedLspServerVersions = ['0.1.40'] From 3130c683d92670381f72dca94276707770ab152b Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:55:41 -0800 Subject: [PATCH 29/40] refactor(amazonq): remove unnecessary code (#6665) ## Problem Some code was accidentally checked in to src.gen ## Solution Remove these unnecessary code --- - 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. --- src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts b/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts index 79ce76ec283..07e2794da2d 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts @@ -1019,8 +1019,6 @@ export interface RelevantTextDocument { * @public */ documentSymbols?: (DocumentSymbol)[] | undefined; - startLine?: number; - endLine?: number; } /** From ae20598f35d75fd149a6726d1e570af536d55043 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:50:41 -0800 Subject: [PATCH 30/40] refactor(cw): Rename the mergedTextDocument field. (#6666) ## Problem The mergedRelevantTextDocument should have a better name. It is actually the documents that are referenced. ## Solution --- - 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. --- .../controllers/chat/controller.ts | 66 +++++++++---------- .../controllers/chat/messenger/messenger.ts | 4 +- .../controllers/chat/model.ts | 6 +- .../view/connector/connector.ts | 6 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index a8b602944dd..16c37cabe97 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -29,7 +29,7 @@ import { ViewDiff, AcceptDiff, QuickCommandGroupActionClick, - MergedRelevantDocument, + DocumentReference, FileClick, RelevantTextDocumentAddition, } from './model' @@ -974,53 +974,53 @@ export class ChatController { } const relativePaths = await this.resolveContextCommandPayload(triggerPayload) - // TODO: resolve the context into real context up to 90k triggerPayload.useRelevantDocuments = false - triggerPayload.mergedRelevantDocuments = [] - if (triggerPayload.message) { - triggerPayload.useRelevantDocuments = triggerPayload.context?.some( - (context) => typeof context !== 'string' && context.command === '@workspace' - ) - if (triggerPayload.useRelevantDocuments) { - triggerPayload.message = triggerPayload.message.replace(/workspace/, '') - if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { - const start = performance.now() - triggerPayload.relevantTextDocuments = await LspController.instance.query(triggerPayload.message) - triggerPayload.mergedRelevantDocuments = this.mergeRelevantTextDocuments( - triggerPayload.relevantTextDocuments + triggerPayload.documentReferences = [] + triggerPayload.useRelevantDocuments = triggerPayload.context?.some( + (context) => typeof context !== 'string' && context.command === '@workspace' + ) + if (triggerPayload.useRelevantDocuments && triggerPayload.message) { + triggerPayload.message = triggerPayload.message.replace(/workspace/, '') + if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { + const start = performance.now() + triggerPayload.relevantTextDocuments = await LspController.instance.query(triggerPayload.message) + + for (const doc of triggerPayload.relevantTextDocuments) { + getLogger().info( + `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}, start line: ${doc.startLine}, end line: ${doc.endLine}` ) - for (const doc of triggerPayload.relevantTextDocuments) { - getLogger().info( - `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}, start line: ${doc.startLine}, end line: ${doc.endLine}` - ) - } - triggerPayload.projectContextQueryLatencyMs = performance.now() - start - } else { - this.messenger.sendOpenSettingsMessage(triggerID, tabID) - return } + triggerPayload.projectContextQueryLatencyMs = performance.now() - start + } else { + this.messenger.sendOpenSettingsMessage(triggerID, tabID) + return } } + triggerPayload.documentReferences = this.mergeRelevantTextDocuments(triggerPayload.relevantTextDocuments || []) + + // TODO: make sure the user input + current focused document + relevantDocument + additionalContext + // combined does not exceed 100k characters before generating the request payload. + // Do truncation and make sure triggerPayload.documentReferences is up-to-date after truncation const request = triggerPayloadToChatRequest(triggerPayload) const session = this.sessionStorage.getSession(tabID) session.currentContextId++ session.contexts.set(session.currentContextId, new Map()) - if (triggerPayload.mergedRelevantDocuments !== undefined) { - const relativePathsOfMergedRelevantDocuments = triggerPayload.mergedRelevantDocuments.map( + if (triggerPayload.documentReferences !== undefined) { + const relativePathsOfMergedRelevantDocuments = triggerPayload.documentReferences.map( (doc) => doc.relativeFilePath ) for (const relativePath of relativePaths) { if (!relativePathsOfMergedRelevantDocuments.includes(relativePath)) { - triggerPayload.mergedRelevantDocuments.push({ + triggerPayload.documentReferences.push({ relativeFilePath: relativePath, lineRanges: [{ first: -1, second: -1 }], }) } } - if (triggerPayload.mergedRelevantDocuments) { - for (const doc of triggerPayload.mergedRelevantDocuments) { + if (triggerPayload.documentReferences) { + for (const doc of triggerPayload.documentReferences) { const currentContext = session.contexts.get(session.currentContextId) if (currentContext) { currentContext.set(doc.relativeFilePath, doc.lineRanges) @@ -1037,7 +1037,7 @@ export class ChatController { let response: MessengerResponseType | undefined = undefined session.createNewTokenSource() try { - this.messenger.sendInitalStream(tabID, triggerID, triggerPayload.mergedRelevantDocuments) + this.messenger.sendInitalStream(tabID, triggerID, triggerPayload.documentReferences) this.telemetryHelper.setConversationStreamStartTime(tabID) if (isSsoConnection(AuthUtil.instance.conn)) { const { $metadata, generateAssistantResponseResponse } = await session.chatSso(request) @@ -1068,11 +1068,9 @@ export class ChatController { } } - private mergeRelevantTextDocuments( - documents: RelevantTextDocumentAddition[] | undefined - ): MergedRelevantDocument[] | undefined { - if (documents === undefined) { - return undefined + private mergeRelevantTextDocuments(documents: RelevantTextDocumentAddition[]): DocumentReference[] { + if (documents.length === 0) { + return [] } return Object.entries( documents.reduce>((acc, doc) => { diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index dc4a5074fc4..0f434c6dc6d 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -25,7 +25,7 @@ import { ChatMessage, ErrorMessage, FollowUp, Suggestion } from '../../../view/c import { ChatSession } from '../../../clients/chat/v0/chat' import { ChatException } from './model' import { CWCTelemetryHelper } from '../telemetryHelper' -import { ChatPromptCommandType, MergedRelevantDocument, TriggerPayload } from '../model' +import { ChatPromptCommandType, DocumentReference, TriggerPayload } from '../model' import { getHttpStatusCode, getRequestId, ToolkitError } from '../../../../shared/errors' import { keys } from '../../../../shared/utilities/tsUtils' import { getLogger } from '../../../../shared/logger/logger' @@ -69,7 +69,7 @@ export class Messenger { public sendInitalStream( tabID: string, triggerID: string, - mergedRelevantDocuments: MergedRelevantDocument[] | undefined + mergedRelevantDocuments: DocumentReference[] | undefined ) { this.dispatcher.sendChatMessage( new ChatMessage( diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 72b27a1c43b..f8033649599 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -189,7 +189,9 @@ export interface TriggerPayload { readonly context?: string[] | QuickActionCommand[] relevantTextDocuments?: RelevantTextDocumentAddition[] additionalContents?: AdditionalContentEntry[] - mergedRelevantDocuments?: MergedRelevantDocument[] + // a reference to all documents used in chat payload + // for providing better context transparency + documentReferences?: DocumentReference[] useRelevantDocuments?: boolean traceId?: string } @@ -197,7 +199,7 @@ export interface TriggerPayload { // TODO move this to API definition (or just use this across the codebase) export type RelevantTextDocumentAddition = RelevantTextDocument & { startLine: number; endLine: number } -export interface MergedRelevantDocument { +export interface DocumentReference { readonly relativeFilePath: string readonly lineRanges: Array<{ first: number; second: number }> } diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index 175cfbd17ad..0b2b29498c4 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -8,7 +8,7 @@ import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' import { ChatItemButton, ChatItemFormItem, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' -import { MergedRelevantDocument } from '../../controllers/chat/model' +import { DocumentReference } from '../../controllers/chat/model' class UiMessage { readonly time: number = Date.now() @@ -207,7 +207,7 @@ export interface ChatMessageProps { readonly messageID: string readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined - readonly contextList: MergedRelevantDocument[] | undefined + readonly contextList: DocumentReference[] | undefined } export class ChatMessage extends UiMessage { @@ -222,7 +222,7 @@ export class ChatMessage extends UiMessage { readonly messageID: string | undefined readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined - readonly contextList: MergedRelevantDocument[] | undefined + readonly contextList: DocumentReference[] | undefined override type = 'chatMessage' constructor(props: ChatMessageProps, tabID: string) { From a4e566afa6dc96a085d74bf37186cce3b6eca7d4 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:45:37 -0800 Subject: [PATCH 31/40] fix(amazonq): Set context limit to 40k characters (#6677) ## Problem This is a band-aid to temporarily set the size of `relevantTextDocuments` and `additionalContentEntry[]` combined to be lower than 40k characters, due to limitations on the service side. This PR will fix the issue when the API returns validation exception. ## Solution 1. Truncation is performed at chunk level. 2. Prioritize `additionalContentEntry[]` over `relevantTextDocuments` 3. Added TODO for a total character count indicator. --- - 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. --- .../controllers/chat/controller.ts | 92 ++++++++++++------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 16c37cabe97..1629ed6fc69 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -127,6 +127,9 @@ const additionalContentInnerContextLimit = 8192 const aditionalContentNameLimit = 1024 +// temporary limit for @workspace and @file combined context length +const contextMaxLength = 40_000 + const getUserPromptsDirectory = () => { return path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'prompts') } @@ -915,28 +918,38 @@ export class ChatController { } const workspaceFolder = contextCommands[0].workspaceFolder const prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) - if (prompts.length > 0) { - triggerPayload.additionalContents = [] - for (const prompt of prompts) { - // Todo: add mechanism for sorting/prioritization of additional context - if (triggerPayload.additionalContents.length < 20) { - triggerPayload.additionalContents.push({ - name: prompt.name.substring(0, aditionalContentNameLimit), - description: prompt.description.substring(0, aditionalContentNameLimit), - innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), - }) - let relativePath = path.relative(workspaceFolder, prompt.filePath) - // Handle user prompts outside the workspace - if (prompt.filePath.startsWith(getUserPromptsDirectory())) { - relativePath = path.basename(prompt.filePath) - } - relativePaths.push(relativePath) - } + if (prompts.length === 0) { + return [] + } + + let currentContextLength = 0 + triggerPayload.additionalContents = [] + for (const prompt of prompts.slice(0, 20)) { + // Todo: add mechanism for sorting/prioritization of additional context + const entry = { + name: prompt.name.substring(0, aditionalContentNameLimit), + description: prompt.description.substring(0, aditionalContentNameLimit), + innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), } - getLogger().info( - `Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} ` - ) + // make sure the relevantDocument + additionalContext + // combined does not exceed 40k characters before generating the request payload. + // Do truncation and make sure triggerPayload.documentReferences is up-to-date after truncation + // TODO: Use a context length indicator + if (currentContextLength + entry.innerContext.length > contextMaxLength) { + getLogger().warn(`Selected context exceeds context size limit: ${entry.description} `) + break + } + triggerPayload.additionalContents.push(entry) + currentContextLength += entry.innerContext.length + let relativePath = path.relative(workspaceFolder, prompt.filePath) + // Handle user prompts outside the workspace + if (prompt.filePath.startsWith(getUserPromptsDirectory())) { + relativePath = path.basename(prompt.filePath) + } + relativePaths.push(relativePath) } + getLogger().info(`Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} `) + return relativePaths } @@ -973,17 +986,37 @@ export class ChatController { return } - const relativePaths = await this.resolveContextCommandPayload(triggerPayload) - triggerPayload.useRelevantDocuments = false + const relativePathsOfContextCommandFiles = await this.resolveContextCommandPayload(triggerPayload) + triggerPayload.useRelevantDocuments = + triggerPayload.context?.some( + (context) => typeof context !== 'string' && context.command === '@workspace' + ) || false triggerPayload.documentReferences = [] - triggerPayload.useRelevantDocuments = triggerPayload.context?.some( - (context) => typeof context !== 'string' && context.command === '@workspace' - ) if (triggerPayload.useRelevantDocuments && triggerPayload.message) { triggerPayload.message = triggerPayload.message.replace(/workspace/, '') if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { const start = performance.now() - triggerPayload.relevantTextDocuments = await LspController.instance.query(triggerPayload.message) + let remainingContextLength = contextMaxLength + for (const additionalContent of triggerPayload.additionalContents || []) { + if (additionalContent.innerContext) { + remainingContextLength -= additionalContent.innerContext.length + } + } + triggerPayload.relevantTextDocuments = [] + const relevantTextDocuments = await LspController.instance.query(triggerPayload.message) + for (const relevantDocument of relevantTextDocuments) { + if (relevantDocument.text !== undefined && relevantDocument.text.length > 0) { + if (remainingContextLength > relevantDocument.text.length) { + triggerPayload.relevantTextDocuments.push(relevantDocument) + remainingContextLength -= relevantDocument.text.length + } else { + getLogger().warn( + `Retrieved context exceeds context size limit: ${relevantDocument.relativeFilePath} ` + ) + break + } + } + } for (const doc of triggerPayload.relevantTextDocuments) { getLogger().info( @@ -996,11 +1029,8 @@ export class ChatController { return } } - triggerPayload.documentReferences = this.mergeRelevantTextDocuments(triggerPayload.relevantTextDocuments || []) - // TODO: make sure the user input + current focused document + relevantDocument + additionalContext - // combined does not exceed 100k characters before generating the request payload. - // Do truncation and make sure triggerPayload.documentReferences is up-to-date after truncation + triggerPayload.documentReferences = this.mergeRelevantTextDocuments(triggerPayload.relevantTextDocuments || []) const request = triggerPayloadToChatRequest(triggerPayload) const session = this.sessionStorage.getSession(tabID) @@ -1011,7 +1041,7 @@ export class ChatController { const relativePathsOfMergedRelevantDocuments = triggerPayload.documentReferences.map( (doc) => doc.relativeFilePath ) - for (const relativePath of relativePaths) { + for (const relativePath of relativePathsOfContextCommandFiles) { if (!relativePathsOfMergedRelevantDocuments.includes(relativePath)) { triggerPayload.documentReferences.push({ relativeFilePath: relativePath, From 0ac8a28e8f7f25d74c373c839a3a31cf7f6bb0b7 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:29:22 -0800 Subject: [PATCH 32/40] deps(amazonq): update mynahUI to beta 11 (#6683) ## Problem ## Solution --- - 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. --- package-lock.json | 8 ++++---- packages/core/package.json | 2 +- packages/core/src/amazonq/webview/ui/main.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 679848c43db..1b6007d946b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4935,9 +4935,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.23.0-beta.7", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.7.tgz", - "integrity": "sha512-qRwy8bP8inhbTb94xv9tJvqrmvv+PDcGyOKuPE9vx4+cwymNtf8HVp5IcIjJ/oTWV5bWA7M3o0mm9qEFFYdB6w==", + "version": "4.23.0-beta.11", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.11.tgz", + "integrity": "sha512-3ZwOSM7UZ1aNO/SyGRhBdrr7Fj2MOcqmT5zzbTCRKrfNmQXMJBCm+lFh54bc2B0S4OskzE2vcvrI16+dBm7WGg==", "hasInstallScript": true, "dependencies": { "escape-html": "^1.0.3", @@ -18925,7 +18925,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.7", + "@aws/mynah-ui": "^4.23.0-beta.11", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index c86b4f0fd1a..5ec69a7e1c6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -510,7 +510,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.7", + "@aws/mynah-ui": "^4.23.0-beta.11", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 2305f35c734..be3adbd2592 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -375,7 +375,7 @@ export const createMynahUI = ( fileTreeTitle: '', filePaths: item.contextList.map((file) => file.relativeFilePath), rootFolderTitle: 'Context', - collapsedByDefault: true, + collapsed: true, hideFileCount: true, details: Object.fromEntries( item.contextList.map((file) => [ From 6618b719ab29e0aac772f85a1d5951161b4b1780 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:17:10 -0500 Subject: [PATCH 33/40] fix(amazonq): remove hardcoded strings, update placeholder (#6670) ## Problem Strings were hardcoded in .ts and test files. Input placeholder text did not include information about using @ to insert context ## Solution Move strings to language file, update placeholder text to match figma. --- - 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. --- .../amazonq/test/e2e/amazonq/chat.test.ts | 4 +-- .../e2e/amazonq/framework/jsdomInjector.ts | 3 ++ packages/core/package.nls.json | 5 +++ packages/core/src/amazonq/index.ts | 1 + .../webview/ui/apps/cwChatConnector.ts | 4 +-- .../src/amazonq/webview/ui/tabs/constants.ts | 6 ++-- .../controllers/chat/controller.ts | 31 ++++++++++++------- 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/amazonq/test/e2e/amazonq/chat.test.ts b/packages/amazonq/test/e2e/amazonq/chat.test.ts index 3021be28782..52122c8e904 100644 --- a/packages/amazonq/test/e2e/amazonq/chat.test.ts +++ b/packages/amazonq/test/e2e/amazonq/chat.test.ts @@ -11,7 +11,7 @@ import { MynahUIDataModel } from '@aws/mynah-ui' import { assertContextCommands, assertQuickActions } from './assert' import { registerAuthHook, using } from 'aws-core-vscode/test' import { loginToIdC } from './utils/setup' -import { webviewConstants } from 'aws-core-vscode/amazonq' +import { webviewConstants, webviewTabConstants } from 'aws-core-vscode/amazonq' describe('Amazon Q Chat', function () { let framework: qTestingFramework @@ -60,7 +60,7 @@ describe('Amazon Q Chat', function () { }) it('Shows placeholder', () => { - assert.deepStrictEqual(store.promptInputPlaceholder, 'Ask a question or enter "/" for quick actions') + assert.deepStrictEqual(store.promptInputPlaceholder, webviewTabConstants.commonTabData.placeholder) }) it('Sends message', async () => { diff --git a/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts b/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts index ce8309c1039..f2aa5a283c9 100644 --- a/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts +++ b/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts @@ -40,6 +40,9 @@ export function injectJSDOM() { get() { return this.textContent }, + set(value) { + this.textContent = value + }, }) // jsdom doesn't have support for structuredClone. See https://github.com/jsdom/jsdom/issues/3363 diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 23fed064a06..2cb2a1ba005 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -309,6 +309,11 @@ "AWS.codewhisperer.customization.notification.new_customizations.learn_more": "Learn More", "AWS.amazonq.title": "Amazon Q", "AWS.amazonq.chat": "Chat", + "AWS.amazonq.savedPrompts.title": "Prompt name", + "AWS.amazonq.savedPrompts.create": "Create", + "AWS.amazonq.savedPrompts.action": "Create a new prompt", + "AWS.amazonq.savedPrompts.placeholder": "Enter prompt name", + "AWS.amazonq.savedPrompts.description": "Use this prompt by typing '@' followed by the prompt name.", "AWS.amazonq.chat.workspacecontext.enable.message": "Amazon Q: Workspace index is now enabled. You can disable it from Amazon Q settings.", "AWS.amazonq.security": "Code Issues", "AWS.amazonq.login": "Login", diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 2eee9e7d4b2..59a53ddf658 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -26,6 +26,7 @@ export { init as testChatAppInit } from '../amazonqTest/app' export { init as docChatAppInit } from '../amazonqDoc/app' export { amazonQHelpUrl } from '../shared/constants' export * as webviewConstants from './webview/ui/texts/constants' +export * as webviewTabConstants from './webview/ui/tabs/constants' export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/statusBarMenu' export { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands' export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens' diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 63eced89654..995061dfd51 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -7,7 +7,6 @@ import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel, Quick import { TabType } from '../storages/tabsStorage' import { CWCChatItem } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' -import { createPromptCommand } from '../tabs/constants' export interface ConnectorProps extends BaseConnectorProps { onCWCContextCommandMessage: (message: CWCChatItem, command?: string) => string | undefined @@ -205,8 +204,7 @@ export class Connector extends BaseConnector { tabID, tabType: this.getTabType(), }) - - if (contextItem.command === createPromptCommand) { + if (contextItem.id === 'create-saved-prompt') { return false } return true diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index 49199be94f1..5675439f8c4 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -13,8 +13,6 @@ export type TabTypeData = { contextCommands?: QuickActionCommandGroup[] } -export const createPromptCommand = 'Create a new prompt' - export const workspaceCommand: QuickActionCommandGroup = { groupName: 'Mention code', commands: [ @@ -25,9 +23,9 @@ export const workspaceCommand: QuickActionCommandGroup = { ], } -const commonTabData: TabTypeData = { +export const commonTabData: TabTypeData = { title: 'Chat', - placeholder: 'Ask a question or enter "/" for quick actions', + placeholder: 'Ask a question. Use @ to add context, / for quick actions', welcome: `Hi, I'm Amazon Q. I can answer your software development questions. Ask me to explain, debug, or optimize your code. You can enter \`/\` to see a list of quick actions. Add @workspace to the beginning of your message to include your entire workspace as context.`, diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 1629ed6fc69..a8e2b8a347f 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -67,9 +67,10 @@ import globals from '../../../shared/extensionGlobals' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' import { LspClient } from '../../../amazonq/lsp/lspClient' import { ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types' -import { createPromptCommand, workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' +import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import { FeatureConfigProvider, Features } from '../../../shared/featureConfig' +import { i18n } from '../../../shared/i18n-helper' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher @@ -121,7 +122,7 @@ export interface ChatControllerMessageListeners { readonly processFileClick: MessageListener } -const promptFileExtension = '.prompt.md' +const promptFileExtension = '.md' const additionalContentInnerContextLimit = 8192 @@ -134,6 +135,8 @@ const getUserPromptsDirectory = () => { return path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'prompts') } +const createSavedPromptCommandId = 'create-saved-prompt' + export class ChatController { private readonly sessionStorage: ChatSessionStorage private readonly triggerEventsStorage: TriggerEventsStorage @@ -456,9 +459,9 @@ export class ChatController { groupName: 'Prompts', actions: [ { - id: 'create-prompt', + id: createSavedPromptCommandId, icon: 'plus', - description: 'Create new prompt', + description: i18n('AWS.amazonq.savedPrompts.action'), }, ], commands: [], @@ -503,7 +506,11 @@ export class ChatController { } // Add create prompt button to the bottom of the prompts list - promptsCmd.children?.[0].commands.push({ command: createPromptCommand, icon: 'list-add' as MynahIconsType }) + promptsCmd.children?.[0].commands.push({ + command: i18n('AWS.amazonq.savedPrompts.action'), + id: createSavedPromptCommandId, + icon: 'list-add' as MynahIconsType, + }) const lspClientReady = await LspClient.instance.waitUntilReady() if (lspClientReady) { @@ -542,21 +549,21 @@ export class ChatController { id: 'prompt-name', type: 'textinput', mandatory: true, - title: 'Prompt name', - placeholder: 'Enter prompt name', - description: `Use this prompt by typing \`@\` followed by the prompt name. Prompt will be saved in ${getUserPromptsDirectory()}.`, + title: i18n('AWS.amazonq.savedPrompts.title'), + placeholder: i18n('AWS.amazonq.savedPrompts.placeholder'), + description: i18n('AWS.amazonq.savedPrompts.description'), }, ], [ - { id: 'cancel-create-prompt', text: 'Cancel', status: 'clear' }, - { id: 'submit-create-prompt', text: 'Create', status: 'main' }, + { id: 'cancel-create-prompt', text: i18n('AWS.generic.cancel'), status: 'clear' }, + { id: 'submit-create-prompt', text: i18n('AWS.amazonq.savedPrompts.create'), status: 'main' }, ], `Create a saved prompt` ) } private processQuickCommandGroupActionClicked(message: QuickCommandGroupActionClick) { - if (message.actionId === 'create-prompt') { + if (message.actionId === createSavedPromptCommandId) { this.handlePromptCreate(message.tabID) } } @@ -578,7 +585,7 @@ export class ChatController { } private async processContextSelected(message: ContextSelectedMessage) { - if (message.tabID && message.contextItem.command === createPromptCommand) { + if (message.tabID && message.contextItem.id === createSavedPromptCommandId) { this.handlePromptCreate(message.tabID) } } From 36caa769c27ae8aa02350674475f3784024dddc5 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Thu, 27 Feb 2025 04:06:08 -0800 Subject: [PATCH 34/40] fix(amazonq): Fix edge cases of context command updates (#6686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem 1. If user create a file using the File>New File buttons, the onCreateFile listener does not capture it. Screenshot 2025-02-26 at 2 46 31 PM 2. Renaming file or folders is not listened and updated. ## Solution 1. Listen to new files created by File>New File buttons 2. Listen to renaming file or folders --- - 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. --- packages/core/src/amazonq/lsp/lspClient.ts | 76 +++++++++++++--------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/core/src/amazonq/lsp/lspClient.ts b/packages/core/src/amazonq/lsp/lspClient.ts index 080ede81462..5d96650ebf8 100644 --- a/packages/core/src/amazonq/lsp/lspClient.ts +++ b/packages/core/src/amazonq/lsp/lspClient.ts @@ -315,6 +315,37 @@ export async function activate(extensionContext: ExtensionContext) { let savedDocument: vscode.Uri | undefined = undefined + const onAdd = async (filePaths: string[]) => { + const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() + await LspClient.instance.updateIndex(filePaths, 'add') + await waitUntil( + async () => { + const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() + if (newIndexSeqNum > indexSeqNum) { + await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) + return true + } + return false + }, + { interval: 500, timeout: 5_000, truthy: true } + ) + } + const onRemove = async (filePaths: string[]) => { + const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() + await LspClient.instance.updateIndex(filePaths, 'remove') + await waitUntil( + async () => { + const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() + if (newIndexSeqNum > indexSeqNum) { + await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) + return true + } + return false + }, + { interval: 500, timeout: 5_000, truthy: true } + ) + } + toDispose.push( vscode.workspace.onDidSaveTextDocument((document) => { if (document.uri.scheme !== 'file') { @@ -326,42 +357,23 @@ export async function activate(extensionContext: ExtensionContext) { if (savedDocument && editor && editor.document.uri.fsPath !== savedDocument.fsPath) { void LspClient.instance.updateIndex([savedDocument.fsPath], 'update') } + // user created a new empty file using File -> New File + // these events will not be captured by vscode.workspace.onDidCreateFiles + // because it was created by File Explorer(Win) or Finder(MacOS) + // TODO: consider using a high performance fs watcher + if (editor?.document.getText().length === 0) { + void onAdd([editor.document.uri.fsPath]) + } }), vscode.workspace.onDidCreateFiles(async (e) => { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex( - e.files.map((f) => f.fsPath), - 'add' - ) - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 500, timeout: 10_000, truthy: true } - ) + await onAdd(e.files.map((f) => f.fsPath)) }), vscode.workspace.onDidDeleteFiles(async (e) => { - const indexSeqNum = await LspClient.instance.getIndexSequenceNumber() - await LspClient.instance.updateIndex( - e.files.map((f) => f.fsPath), - 'remove' - ) - await waitUntil( - async () => { - const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber() - if (newIndexSeqNum > indexSeqNum) { - await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`) - return true - } - return false - }, - { interval: 500, timeout: 10_000, truthy: true } - ) + await onRemove(e.files.map((f) => f.fsPath)) + }), + vscode.workspace.onDidRenameFiles(async (e) => { + await onRemove(e.files.map((f) => f.oldUri.fsPath)) + await onAdd(e.files.map((f) => f.newUri.fsPath)) }) ) From e14f31eb065cfc53eb8a57c41a59e0a59dd7dd86 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:44:12 -0800 Subject: [PATCH 35/40] fix(amazonq): update lsp artifact to 0.1.42 (#6689) ## Problem In certain rare cases, when the changed file has no dot in filename, this file was misread as folders in the older LSP. Now this bug is fixed in 0.1.42 lsp. ## Solution update lsp artifact to 0.1.42 --- - 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. --- packages/core/src/amazonq/lsp/lspController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index f843a2bc670..774392337ba 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -62,7 +62,7 @@ export interface Manifest { } const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions -const supportedLspServerVersions = ['0.1.40'] +const supportedLspServerVersions = ['0.1.42'] const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node' From 314c94960b98b816c806b1babd93a896665afe91 Mon Sep 17 00:00:00 2001 From: Lei Gao <97199248+leigaol@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:44:50 -0800 Subject: [PATCH 36/40] deps(amazonq): upgrade to mynahui beta 12 (#6688) ## Problem We need to update to mynahui beta 12 ## Solution --- - 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. --- package-lock.json | 8 ++++---- packages/core/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b6007d946b..0eac79a454a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4935,9 +4935,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.23.0-beta.11", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.11.tgz", - "integrity": "sha512-3ZwOSM7UZ1aNO/SyGRhBdrr7Fj2MOcqmT5zzbTCRKrfNmQXMJBCm+lFh54bc2B0S4OskzE2vcvrI16+dBm7WGg==", + "version": "4.23.0-beta.12", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.12.tgz", + "integrity": "sha512-/koL2JR31dccP1NgWbbHCBK3RwlC84+PbasEW6hSbjLOi5K3h9N5kcqLCn8vSIljMORS5XDnqErg7ZCcDkjXDg==", "hasInstallScript": true, "dependencies": { "escape-html": "^1.0.3", @@ -18925,7 +18925,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.11", + "@aws/mynah-ui": "^4.23.0-beta.12", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index 5ec69a7e1c6..9cacaa0bc07 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -510,7 +510,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.11", + "@aws/mynah-ui": "^4.23.0-beta.12", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", From 9a37abe6362c4f2a403294c099a7da5d871cef91 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:23:44 -0500 Subject: [PATCH 37/40] fix(amazonq): file list flicker (#6697) ## Problem File list in header flickers as message is rendering (see screen recording) https://github.com/user-attachments/assets/485c665f-cde1-4ed6-a316-8cea2ac63a49 ## Solution Update to latest mynah beta. Pass `undefined` to `header` in subsequent messages (as instructed by Mynah UI team) Note: no fix in changelog needed since feature hasnt yet been released --- - 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. --- package-lock.json | 8 ++++---- packages/core/package.json | 2 +- packages/core/src/amazonq/webview/ui/main.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index b383fd2aed0..d345cda465c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4935,9 +4935,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.23.0-beta.12", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.12.tgz", - "integrity": "sha512-/koL2JR31dccP1NgWbbHCBK3RwlC84+PbasEW6hSbjLOi5K3h9N5kcqLCn8vSIljMORS5XDnqErg7ZCcDkjXDg==", + "version": "4.23.0-beta.14", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.14.tgz", + "integrity": "sha512-agmqB6ilxhgFi2eToEWYbmfzvUBOQcYUYdVtJ0/q1kjQj2UoJcRsLjulnsHd7Ml1pJlvSSh/LmxyNKVhiBwVQg==", "hasInstallScript": true, "dependencies": { "escape-html": "^1.0.3", @@ -18925,7 +18925,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.12", + "@aws/mynah-ui": "^4.23.0-beta.14", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index 9cacaa0bc07..83d721d4235 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -510,7 +510,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.12", + "@aws/mynah-ui": "^4.23.0-beta.14", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index be3adbd2592..bbd78dbfb4a 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -351,7 +351,7 @@ export const createMynahUI = ( ...(item.relatedContent !== undefined ? { relatedContent: item.relatedContent } : {}), ...(item.followUp !== undefined ? { followUp: item.followUp } : {}), ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), - ...(item.header !== undefined ? { header: item.header } : {}), + ...(item.header !== undefined ? { header: item.header } : { header: undefined }), }) if ( item.messageId !== undefined && From 64f6132ec8b05e2395b22d7fe1f23837c6484a7e Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:26:51 -0500 Subject: [PATCH 38/40] telemetry(amazonq): add telemetry for falcon features (#6691) This PR adds the following telemetry for falcon features: **New metric**: `amazonq_createSavedPrompt` - tracks when user creates a saved prompt using UX. Adding to commons in this [PR](https://github.com/aws/aws-toolkit-common/pull/986) **Additing to existing metric**: `amazonq_addMessage` (does not exist in toolkit common): - `cwsprChatHasContextList` - true if context list is displayed to user - `cwsprChatPromptContextCount` - # of saved prompts manually added to context - `cwsprChatPromptContextLength` - Total length of saved prompts added to context - `cwsprChatPromptContextTruncatedLength` - Truncated length of saved prompts added to context - `cwsprChatRuleContextCount` - # of workspace rules automatically added to context - `cwsprChatRuleContextLength` - Total length of workspace rules added to context - `cwsprChatRuleContextTruncatedLength` - Truncated length of workspace rules added to context - `cwsprChatFileContextCount` - # of files manually added to context - `cwsprChatFileContextLength` - Total length of files added to context - `cwsprChatFileContextTruncatedLength` - Truncated length of files added to context - `cwsprChatFolderContextCount` - # of folders manually added to context Telemetry doc: https://quip-amazon.com/XRRfA9b8Ux24/Falcon-Telemetry-Toolkits-telemetry --- - 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. --- .../core/src/codewhispererChat/constants.ts | 21 ++++ .../controllers/chat/controller.ts | 44 ++++++--- .../controllers/chat/model.ts | 9 ++ .../controllers/chat/telemetryHelper.ts | 77 +++++++++++++++ .../src/shared/telemetry/vscodeTelemetry.json | 99 +++++++++++++++++++ 5 files changed, 235 insertions(+), 15 deletions(-) create mode 100644 packages/core/src/codewhispererChat/constants.ts diff --git a/packages/core/src/codewhispererChat/constants.ts b/packages/core/src/codewhispererChat/constants.ts new file mode 100644 index 00000000000..4566d14ec64 --- /dev/null +++ b/packages/core/src/codewhispererChat/constants.ts @@ -0,0 +1,21 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as path from 'path' +import fs from '../shared/fs/fs' + +export const promptFileExtension = '.md' + +export const additionalContentInnerContextLimit = 8192 + +export const aditionalContentNameLimit = 1024 + +// temporary limit for @workspace and @file combined context length +export const contextMaxLength = 40_000 + +export const getUserPromptsDirectory = () => { + return path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'prompts') +} + +export const createSavedPromptCommandId = 'create-saved-prompt' diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index a8e2b8a347f..4ce8bfc1a4e 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -71,6 +71,14 @@ import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import { FeatureConfigProvider, Features } from '../../../shared/featureConfig' import { i18n } from '../../../shared/i18n-helper' +import { + getUserPromptsDirectory, + promptFileExtension, + createSavedPromptCommandId, + aditionalContentNameLimit, + additionalContentInnerContextLimit, + contextMaxLength, +} from '../../constants' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher @@ -122,21 +130,6 @@ export interface ChatControllerMessageListeners { readonly processFileClick: MessageListener } -const promptFileExtension = '.md' - -const additionalContentInnerContextLimit = 8192 - -const aditionalContentNameLimit = 1024 - -// temporary limit for @workspace and @file combined context length -const contextMaxLength = 40_000 - -const getUserPromptsDirectory = () => { - return path.join(fs.getUserHomeDir(), '.aws', 'amazonq', 'prompts') -} - -const createSavedPromptCommandId = 'create-saved-prompt' - export class ChatController { private readonly sessionStorage: ChatSessionStorage private readonly triggerEventsStorage: TriggerEventsStorage @@ -497,6 +490,7 @@ export class ChatController { .map(([name]) => ({ command: path.basename(name, promptFileExtension), icon: 'magic' as MynahIconsType, + id: 'prompt', route: [userPromptsDirectory, name], })) ) @@ -525,6 +519,7 @@ export class ChatController { command: path.basename(contextCommandItem.relativePath), description: path.join(wsFolderName, contextCommandItem.relativePath), route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + id: 'file', icon: 'file' as MynahIconsType, }) } else { @@ -532,6 +527,7 @@ export class ChatController { command: path.basename(contextCommandItem.relativePath), description: path.join(wsFolderName, contextCommandItem.relativePath), route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], + id: 'folder', icon: 'folder' as MynahIconsType, }) } @@ -581,6 +577,7 @@ export class ChatController { await fs.writeFile(newFilePath, newFileContent) const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) await vscode.window.showTextDocument(newFileDoc) + telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' }) } } @@ -906,6 +903,7 @@ export class ChatController { }) ) } + triggerPayload.workspaceRulesCount = workspaceRules.length // Add context commands added by user to context if (triggerPayload.context !== undefined && triggerPayload.context.length > 0) { @@ -931,6 +929,12 @@ export class ChatController { let currentContextLength = 0 triggerPayload.additionalContents = [] + triggerPayload.additionalContextLengths = this.telemetryHelper.getContextLengths(prompts) + triggerPayload.truncatedAdditionalContextLengths = { + fileContextLength: 0, + promptContextLength: 0, + ruleContextLength: 0, + } for (const prompt of prompts.slice(0, 20)) { // Todo: add mechanism for sorting/prioritization of additional context const entry = { @@ -946,6 +950,16 @@ export class ChatController { getLogger().warn(`Selected context exceeds context size limit: ${entry.description} `) break } + + const contextType = this.telemetryHelper.getContextType(prompt) + if (contextType === 'rule') { + triggerPayload.truncatedAdditionalContextLengths.ruleContextLength += entry.innerContext.length + } else if (contextType === 'prompt') { + triggerPayload.truncatedAdditionalContextLengths.promptContextLength += entry.innerContext.length + } else if (contextType === 'file') { + triggerPayload.truncatedAdditionalContextLengths.fileContextLength += entry.innerContext.length + } + triggerPayload.additionalContents.push(entry) currentContextLength += entry.innerContext.length let relativePath = path.relative(workspaceFolder, prompt.filePath) diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index f8033649599..bc729dba954 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -194,6 +194,15 @@ export interface TriggerPayload { documentReferences?: DocumentReference[] useRelevantDocuments?: boolean traceId?: string + additionalContextLengths?: AdditionalContextLengths + truncatedAdditionalContextLengths?: AdditionalContextLengths + workspaceRulesCount?: number +} + +export type AdditionalContextLengths = { + fileContextLength: number + promptContextLength: number + ruleContextLength: number } // TODO move this to API definition (or just use this across the codebase) diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 3488d02d4f9..519f3ceef87 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -27,6 +27,7 @@ import { ResponseBodyLinkClickMessage, SourceLinkClickMessage, TriggerPayload, + AdditionalContextLengths, } from './model' import { TriggerEvent, TriggerEventsStorage } from '../../storages/triggerEvents' import globals from '../../../shared/extensionGlobals' @@ -38,6 +39,8 @@ import { supportedLanguagesList } from '../chat/chatRequest/converter' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' import { undefinedIfEmpty } from '../../../shared/utilities/textUtilities' +import { AdditionalContextPrompt } from '../../../amazonq/lsp/types' +import { getUserPromptsDirectory } from '../../constants' export function logSendTelemetryEventFailure(error: any) { let requestId: string | undefined @@ -142,6 +145,39 @@ export class CWCTelemetryHelper { telemetry.amazonq_exitFocusChat.emit({ result: 'Succeeded', passive: true }) } + public getContextType(prompt: AdditionalContextPrompt): string { + if (prompt.relativePath.startsWith('.amazonq/rules')) { + return 'rule' + } else if (prompt.filePath.startsWith(getUserPromptsDirectory())) { + return 'prompt' + } else { + return 'file' + } + } + + public getContextLengths(prompts: AdditionalContextPrompt[]): AdditionalContextLengths { + let fileContextLength = 0 + let promptContextLength = 0 + let ruleContextLength = 0 + + for (const prompt of prompts) { + const type = this.getContextType(prompt) + switch (type) { + case 'rule': + ruleContextLength += prompt.content.length + break + case 'file': + fileContextLength += prompt.content.length + break + case 'prompt': + promptContextLength += prompt.content.length + break + } + } + + return { fileContextLength, promptContextLength, ruleContextLength } + } + public async recordFeedback(message: ChatItemFeedbackMessage) { const logger = getLogger() try { @@ -420,6 +456,30 @@ export class CWCTelemetryHelper { }) } + private getAdditionalContextCounts(triggerPayload: TriggerPayload) { + const counts = { + fileContextCount: 0, + folderContextCount: 0, + promptContextCount: 0, + } + + if (triggerPayload.context) { + for (const context of triggerPayload.context) { + if (typeof context !== 'string') { + if (context.id === 'file') { + counts.fileContextCount++ + } else if (context.id === 'folder') { + counts.folderContextCount++ + } else if (context.id === 'prompt') { + counts.promptContextCount++ + } + } + } + } + + return counts + } + public emitAddMessage(tabID: string, fullDisplayLatency: number, traceId: string, startTime?: number) { const payload = this.messageStorage.get(tabID) if (!payload) { @@ -433,6 +493,9 @@ export class CWCTelemetryHelper { triggerPayload.relevantTextDocuments && triggerPayload.relevantTextDocuments.length > 0 && triggerPayload.useRelevantDocuments === true + + const contextCounts = this.getAdditionalContextCounts(triggerPayload) + const event: AmazonqAddMessage = { result: 'Succeeded', cwsprChatConversationId: this.getConversationId(message.tabID) ?? '', @@ -464,6 +527,20 @@ export class CWCTelemetryHelper { credentialStartUrl: AuthUtil.instance.startUrl, codewhispererCustomizationArn: triggerPayload.customization.arn, cwsprChatHasProjectContext: hasProjectLevelContext, + cwsprChatHasContextList: triggerPayload.documentReferences && triggerPayload.documentReferences?.length > 0, + cwsprChatFolderContextCount: contextCounts.folderContextCount, + cwsprChatFileContextCount: contextCounts.fileContextCount, + cwsprChatFileContextLength: triggerPayload.additionalContextLengths?.fileContextLength ?? 0, + cwsprChatFileContextTruncatedLength: + triggerPayload.truncatedAdditionalContextLengths?.fileContextLength ?? 0, + cwsprChatRuleContextCount: triggerPayload.workspaceRulesCount, + cwsprChatRuleContextLength: triggerPayload.additionalContextLengths?.ruleContextLength ?? 0, + cwsprChatRuleContextTruncatedLength: + triggerPayload.truncatedAdditionalContextLengths?.ruleContextLength ?? 0, + cwsprChatPromptContextCount: contextCounts.promptContextCount, + cwsprChatPromptContextLength: triggerPayload.additionalContextLengths?.promptContextLength ?? 0, + cwsprChatPromptContextTruncatedLength: + triggerPayload.truncatedAdditionalContextLengths?.promptContextLength ?? 0, traceId, } diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index 3487d7b4157..09339283bee 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -83,6 +83,61 @@ "type": "int", "description": "Query latency in ms for local project context" }, + { + "name": "cwsprChatFolderContextCount", + "type": "int", + "description": "Number of folders manually added to context" + }, + { + "name": "cwsprChatFileContextCount", + "type": "int", + "description": "Number of files manually added to context" + }, + { + "name": "cwsprChatFileContextLength", + "type": "int", + "description": "Total length of files added to context" + }, + { + "name": "cwsprChatFileContextTruncatedLength", + "type": "int", + "description": "Truncated length of files added to context" + }, + { + "name": "cwsprChatPromptContextCount", + "type": "int", + "description": "Number of saved prompts manually added to context" + }, + { + "name": "cwsprChatPromptContextLength", + "type": "int", + "description": "Total length of saved prompts added to context" + }, + { + "name": "cwsprChatPromptContextTruncatedLength", + "type": "int", + "description": "Truncated length of saved prompts added to context" + }, + { + "name": "cwsprChatRuleContextCount", + "type": "int", + "description": "Number of workspace rules automatically added to context" + }, + { + "name": "cwsprChatRuleContextLength", + "type": "int", + "description": "Total length of workspace rules added to context" + }, + { + "name": "cwsprChatRuleContextTruncatedLength", + "type": "int", + "description": "Truncated length of workspace rules added to context" + }, + { + "name": "cwsprChatHasContextList", + "type": "boolean", + "description": "true if context list is displayed to user" + }, { "name": "amazonqIndexFileSizeInMB", "type": "int", @@ -804,6 +859,50 @@ { "type": "codewhispererCustomizationArn", "required": false + }, + { + "type": "cwsprChatHasContextList", + "required": false + }, + { + "type": "cwsprChatFolderContextCount", + "required": false + }, + { + "type": "cwsprChatFileContextCount", + "required": false + }, + { + "type": "cwsprChatFileContextLength", + "required": false + }, + { + "type": "cwsprChatFileContextTruncatedLength", + "required": false + }, + { + "type": "cwsprChatPromptContextCount", + "required": false + }, + { + "type": "cwsprChatPromptContextLength", + "required": false + }, + { + "type": "cwsprChatRuleContextTruncatedLength", + "required": false + }, + { + "type": "cwsprChatRuleContextCount", + "required": false + }, + { + "type": "cwsprChatRuleContextLength", + "required": false + }, + { + "type": "cwsprChatPromptContextTruncatedLength", + "required": false } ] }, From a19f3581562cb39bf3b7af6c27f9223c5405c6c5 Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:06:29 -0500 Subject: [PATCH 39/40] feat(amazonq): Copy changes for falcon features (#6703) Problem: - We need to update copy text Solution: - Move hardcoded strings to language file - Update titles/descriptions of new Context menu options - Update welcome message to include new context menu options - Update mynah-ui to latest - add changelog for all falcon features --- - 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. --- package-lock.json | 8 +++--- ...-1707f610-2dde-4af1-b704-0e541efb7c4a.json | 4 +++ ...-dc148db8-5936-478c-a8cf-909d0885105b.json | 4 +++ packages/core/package.json | 2 +- packages/core/package.nls.json | 6 +++++ packages/core/src/amazonq/webview/ui/main.ts | 1 + .../src/amazonq/webview/ui/tabs/constants.ts | 2 +- .../controllers/chat/controller.ts | 25 +++++++------------ 8 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Feature-1707f610-2dde-4af1-b704-0e541efb7c4a.json create mode 100644 packages/amazonq/.changes/next-release/Feature-dc148db8-5936-478c-a8cf-909d0885105b.json diff --git a/package-lock.json b/package-lock.json index d345cda465c..5da7babfb3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4935,9 +4935,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.23.0-beta.14", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0-beta.14.tgz", - "integrity": "sha512-agmqB6ilxhgFi2eToEWYbmfzvUBOQcYUYdVtJ0/q1kjQj2UoJcRsLjulnsHd7Ml1pJlvSSh/LmxyNKVhiBwVQg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.23.0.tgz", + "integrity": "sha512-mrtZTPvt7NNhHtHbHLuA59RyYwhqzio3ciaciOTnUwVUYtLYqVQAeiS9USEuIAJj1uPX+KULmo5Mhbgq1vaotw==", "hasInstallScript": true, "dependencies": { "escape-html": "^1.0.3", @@ -18925,7 +18925,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.14", + "@aws/mynah-ui": "^4.23.0", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/amazonq/.changes/next-release/Feature-1707f610-2dde-4af1-b704-0e541efb7c4a.json b/packages/amazonq/.changes/next-release/Feature-1707f610-2dde-4af1-b704-0e541efb7c4a.json new file mode 100644 index 00000000000..4800cd44fa6 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-1707f610-2dde-4af1-b704-0e541efb7c4a.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Amazon Q chat: Use `@` to add folders, files, and saved prompts as context" +} diff --git a/packages/amazonq/.changes/next-release/Feature-dc148db8-5936-478c-a8cf-909d0885105b.json b/packages/amazonq/.changes/next-release/Feature-dc148db8-5936-478c-a8cf-909d0885105b.json new file mode 100644 index 00000000000..d49889457e3 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-dc148db8-5936-478c-a8cf-909d0885105b.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Amazon Q chat: Show list of files sent as context in chat response" +} diff --git a/packages/core/package.json b/packages/core/package.json index 83d721d4235..2ac7065e546 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -510,7 +510,7 @@ "@aws-sdk/property-provider": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.23.0-beta.14", + "@aws/mynah-ui": "^4.23.0", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^3.0.0", diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 2cb2a1ba005..0a33858a232 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -309,6 +309,12 @@ "AWS.codewhisperer.customization.notification.new_customizations.learn_more": "Learn More", "AWS.amazonq.title": "Amazon Q", "AWS.amazonq.chat": "Chat", + "AWS.amazonq.context.folders.title": "Folders", + "AWS.amazonq.context.folders.description": "Add all files in a folder to context", + "AWS.amazonq.context.files.title": "Files", + "AWS.amazonq.context.files.description": "Add a file to context", + "AWS.amazonq.context.prompts.title": "Prompts", + "AWS.amazonq.context.prompts.description": "Add a saved prompt to context", "AWS.amazonq.savedPrompts.title": "Prompt name", "AWS.amazonq.savedPrompts.create": "Create", "AWS.amazonq.savedPrompts.action": "Create a new prompt", diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index bbd78dbfb4a..9a832eaf60f 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -375,6 +375,7 @@ export const createMynahUI = ( fileTreeTitle: '', filePaths: item.contextList.map((file) => file.relativeFilePath), rootFolderTitle: 'Context', + flatList: true, collapsed: true, hideFileCount: true, details: Object.fromEntries( diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index 5675439f8c4..1ac0c5ae156 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -28,7 +28,7 @@ export const commonTabData: TabTypeData = { placeholder: 'Ask a question. Use @ to add context, / for quick actions', welcome: `Hi, I'm Amazon Q. I can answer your software development questions. Ask me to explain, debug, or optimize your code. - You can enter \`/\` to see a list of quick actions. Add @workspace to the beginning of your message to include your entire workspace as context.`, + You can enter \`/\` to see a list of quick actions. Use \`@\` to add saved prompts, files, folders, or your entire workspace as context.`, contextCommands: [workspaceCommand], } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 4ce8bfc1a4e..8c9d3e15aed 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -424,43 +424,36 @@ export class ChatController { commands: [ ...workspaceCommand.commands, { - command: 'folder', + command: i18n('AWS.amazonq.context.folders.title'), children: [ { - groupName: 'Folders', + groupName: i18n('AWS.amazonq.context.folders.title'), commands: [], }, ], - description: 'All files within a specific folder', + description: i18n('AWS.amazonq.context.folders.description'), icon: 'folder' as MynahIconsType, }, { - command: 'file', + command: i18n('AWS.amazonq.context.files.title'), children: [ { - groupName: 'Files', + groupName: i18n('AWS.amazonq.context.files.title'), commands: [], }, ], - description: 'File', + description: i18n('AWS.amazonq.context.files.description'), icon: 'file' as MynahIconsType, }, { - command: 'prompts', + command: i18n('AWS.amazonq.context.prompts.title'), children: [ { - groupName: 'Prompts', - actions: [ - { - id: createSavedPromptCommandId, - icon: 'plus', - description: i18n('AWS.amazonq.savedPrompts.action'), - }, - ], + groupName: i18n('AWS.amazonq.context.prompts.title'), commands: [], }, ], - description: 'Prompts', + description: i18n('AWS.amazonq.context.prompts.description'), icon: 'magic' as MynahIconsType, }, ], From 64a3adc1ab76a44bb6a411ab003570a3dd5c0838 Mon Sep 17 00:00:00 2001 From: andrewyuq <89420755+andrewyuq@users.noreply.github.com> Date: Tue, 4 Mar 2025 09:05:55 -0800 Subject: [PATCH 40/40] fix(amazonq): context fixes for multiroot workspace setup (#6707) ## Problem In multiroot workspace setup, the contextList UI displays files that are deduped for the same files and doesn't resolve relative paths correctly. 1. In a multi-root workspace, the index is built on the root of the 1st project after sorting workspaceFolders, thus the chunk response also contains relativePath to the 1st project root after sorting. In order to get the same relativePath from explicit @file, we also need to use the root of the 1st project after sorting workspaceFolders. 2. Use a relativePath to projectRoot map to store the project root for each relativePath so that on clicking the file(which only returns relativePath in the event) we correctly construct the absolute path of the file to open in the editor. ## Solution --- - 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. --- .../codewhispererChat/clients/chat/v0/chat.ts | 6 ++-- .../controllers/chat/controller.ts | 36 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index 70235923ecc..3cf030b9b8e 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -14,8 +14,10 @@ import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWr export class ChatSession { private sessionId?: string - contexts: Map> = new Map() - currentContextId: number = 0 + contexts: Map = new Map() + // TODO: doesn't handle the edge case when two files share the same relativePath string but from different root + // e.g. root_a/file1 vs root_b/file1 + relativePathToWorkspaceRoot: Map = new Map() public get sessionIdentifier(): string | undefined { return this.sessionId } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 8c9d3e15aed..b1ce1ac12e3 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -79,6 +79,7 @@ import { additionalContentInnerContextLimit, contextMaxLength, } from '../../constants' +import { ChatSession } from '../../clients/chat/v0/chat' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher @@ -581,15 +582,14 @@ export class ChatController { } private async processFileClickMessage(message: FileClick) { const session = this.sessionStorage.getSession(message.tabID) - // TODO remove currentContextId but use messageID to track context for each answer message - const lineRanges = session.contexts.get(session.currentContextId)?.get(message.filePath) + const lineRanges = session.contexts.get(message.filePath) if (!lineRanges) { return } // TODO: Fix for multiple workspace setup - const projectRoot = workspace.workspaceFolders?.[0]?.uri.fsPath + const projectRoot = session.relativePathToWorkspaceRoot.get(message.filePath) if (!projectRoot) { return } @@ -877,7 +877,10 @@ export class ChatController { this.messenger.sendStaticTextResponse(responseType, triggerID, tabID) } - private async resolveContextCommandPayload(triggerPayload: TriggerPayload): Promise { + private async resolveContextCommandPayload( + triggerPayload: TriggerPayload, + session: ChatSession + ): Promise { const contextCommands: ContextCommandItem[] = [] const relativePaths: string[] = [] @@ -914,7 +917,19 @@ export class ChatController { if (contextCommands.length === 0) { return [] } - const workspaceFolder = contextCommands[0].workspaceFolder + const workspaceFolders = (vscode.workspace.workspaceFolders ?? []).map((folder) => folder.uri.fsPath) + if (!workspaceFolders) { + return [] + } + workspaceFolders.sort() + const workspaceFolder = workspaceFolders[0] + for (const contextCommand of contextCommands) { + const relativePath = path.relative( + workspaceFolder, + path.join(contextCommand.workspaceFolder, contextCommand.relativePath) + ) + session.relativePathToWorkspaceRoot.set(relativePath, contextCommand.workspaceFolder) + } const prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) if (prompts.length === 0) { return [] @@ -1000,7 +1015,8 @@ export class ChatController { return } - const relativePathsOfContextCommandFiles = await this.resolveContextCommandPayload(triggerPayload) + const session = this.sessionStorage.getSession(tabID) + const relativePathsOfContextCommandFiles = await this.resolveContextCommandPayload(triggerPayload, session) triggerPayload.useRelevantDocuments = triggerPayload.context?.some( (context) => typeof context !== 'string' && context.command === '@workspace' @@ -1047,10 +1063,7 @@ export class ChatController { triggerPayload.documentReferences = this.mergeRelevantTextDocuments(triggerPayload.relevantTextDocuments || []) const request = triggerPayloadToChatRequest(triggerPayload) - const session = this.sessionStorage.getSession(tabID) - session.currentContextId++ - session.contexts.set(session.currentContextId, new Map()) if (triggerPayload.documentReferences !== undefined) { const relativePathsOfMergedRelevantDocuments = triggerPayload.documentReferences.map( (doc) => doc.relativeFilePath @@ -1065,10 +1078,7 @@ export class ChatController { } if (triggerPayload.documentReferences) { for (const doc of triggerPayload.documentReferences) { - const currentContext = session.contexts.get(session.currentContextId) - if (currentContext) { - currentContext.set(doc.relativeFilePath, doc.lineRanges) - } + session.contexts.set(doc.relativeFilePath, doc.lineRanges) } } }