From fab3cc38eea3d6397bb0b83754a9a4364a5345d3 Mon Sep 17 00:00:00 2001 From: Avi Alpert Date: Mon, 16 Jun 2025 09:26:23 -0400 Subject: [PATCH 1/4] feat(amazonq): pinned context and rules --- chat-client/src/client/chat.ts | 31 ++ chat-client/src/client/features/history.ts | 2 +- chat-client/src/client/features/rules.ts | 191 ++++++++ chat-client/src/client/messager.ts | 31 +- chat-client/src/client/mynahUi.test.ts | 4 + chat-client/src/client/mynahUi.ts | 89 +++- chat-client/src/client/utils.ts | 1 + chat-client/src/client/withAdapter.ts | 3 + .../src/contracts/chatClientAdapter.ts | 3 + chat-client/src/contracts/serverContracts.ts | 14 + client/vscode/src/chatActivation.ts | 18 + .../agenticChat/agenticChatController.ts | 83 +++- .../context/additionalContextProvider.test.ts | 26 +- .../context/addtionalContextProvider.ts | 443 ++++++++++++++++-- .../context/agenticChatTriggerContext.ts | 3 +- .../context/contextCommandsProvider.ts | 6 +- .../agenticChat/context/contextUtils.ts | 69 ++- .../agenticChat/qAgenticChatServer.ts | 20 + .../agenticChat/tabBarController.test.ts | 2 +- .../agenticChat/tabBarController.ts | 16 +- .../agenticChat/tools/chatDb/chatDb.ts | 141 +++++- .../agenticChat/tools/chatDb/util.ts | 23 + .../language-server/chat/chatController.ts | 6 + .../chat/telemetry/chatTelemetryController.ts | 5 + .../src/shared/telemetry/telemetryService.ts | 10 + .../src/shared/telemetry/types.ts | 7 + 26 files changed, 1147 insertions(+), 100 deletions(-) create mode 100644 chat-client/src/client/features/rules.ts diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index 20b7ddda15..37cd385812 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -62,10 +62,13 @@ import { InfoLinkClickParams, LINK_CLICK_NOTIFICATION_METHOD, LIST_CONVERSATIONS_REQUEST_METHOD, + LIST_RULES_REQUEST_METHOD, LIST_MCP_SERVERS_REQUEST_METHOD, LinkClickParams, ListConversationsParams, ListConversationsResult, + ListRulesParams, + ListRulesResult, ListMcpServersParams, ListMcpServersResult, MCP_SERVER_CLICK_REQUEST_METHOD, @@ -74,11 +77,18 @@ import { OPEN_TAB_REQUEST_METHOD, OpenTabParams, OpenTabResult, + PINNED_CONTEXT_ADD_NOTIFICATION_METHOD, + PINNED_CONTEXT_NOTIFICATION_METHOD, + PINNED_CONTEXT_REMOVE_NOTIFICATION_METHOD, PROMPT_INPUT_OPTION_CHANGE_METHOD, + PinnedContextParams, PromptInputOptionChangeParams, QUICK_ACTION_REQUEST_METHOD, QuickActionParams, READY_NOTIFICATION_METHOD, + RULE_CLICK_REQUEST_METHOD, + RuleClickParams, + RuleClickResult, SOURCE_LINK_CLICK_NOTIFICATION_METHOD, SourceLinkClickParams, TAB_ADD_NOTIFICATION_METHOD, @@ -193,9 +203,18 @@ export const createChat = ( case CONTEXT_COMMAND_NOTIFICATION_METHOD: mynahApi.sendContextCommands(message.params as ContextCommandParams) break + case PINNED_CONTEXT_NOTIFICATION_METHOD: + mynahApi.sendPinnedContext(message.params as PinnedContextParams) + break case LIST_CONVERSATIONS_REQUEST_METHOD: mynahApi.listConversations(message.params as ListConversationsResult) break + case LIST_RULES_REQUEST_METHOD: + mynahApi.listRules(message.params as ListRulesResult) + break + case RULE_CLICK_REQUEST_METHOD: + mynahApi.ruleClicked(message.params as RuleClickResult) + break case CONVERSATION_CLICK_REQUEST_METHOD: mynahApi.conversationClicked(message.params as ConversationClickResult) break @@ -432,6 +451,18 @@ export const createChat = ( onOpenSettings: (settingKey: string) => { sendMessageToClient({ command: OPEN_SETTINGS, params: { settingKey } }) }, + onRuleClick: (params: RuleClickParams) => { + sendMessageToClient({ command: RULE_CLICK_REQUEST_METHOD, params }) + }, + listRules: (params: ListRulesParams) => { + sendMessageToClient({ command: LIST_RULES_REQUEST_METHOD, params }) + }, + onAddPinnedContext: (params: PinnedContextParams) => { + sendMessageToClient({ command: PINNED_CONTEXT_ADD_NOTIFICATION_METHOD, params }) + }, + onRemovePinnedContext: (params: PinnedContextParams) => { + sendMessageToClient({ command: PINNED_CONTEXT_REMOVE_NOTIFICATION_METHOD, params }) + }, } const messager = new Messager(chatApi) diff --git a/chat-client/src/client/features/history.ts b/chat-client/src/client/features/history.ts index 20d3bc9579..b9702a3555 100644 --- a/chat-client/src/client/features/history.ts +++ b/chat-client/src/client/features/history.ts @@ -7,7 +7,7 @@ export const ChatHistory = { TabBarButtonId: 'history_sheet', } as const -interface MynahDetailedList { +export interface MynahDetailedList { update: (data: DetailedList) => void close: () => void changeTarget: (direction: 'up' | 'down', snapOnLastAndFirst?: boolean) => void diff --git a/chat-client/src/client/features/rules.ts b/chat-client/src/client/features/rules.ts new file mode 100644 index 0000000000..f2be8948a1 --- /dev/null +++ b/chat-client/src/client/features/rules.ts @@ -0,0 +1,191 @@ +import { MynahIconsType, MynahUI, DetailedListItem, DetailedListItemGroup, MynahIcons } from '@aws/mynah-ui' +import { Messager } from '../messager' +import { ListRulesResult } from '@aws/language-server-runtimes-types' +import { RulesFolder } from '@aws/language-server-runtimes-types' +import { MynahDetailedList } from './history' + +export const ContextRule = { + CreateRuleId: 'create-rule', + CancelButtonId: 'cancel-create-rule', + SubmitButtonId: 'submit-create-rule', + RuleNameFieldId: 'rule-name', +} as const + +export class RulesList { + rulesList: MynahDetailedList | undefined + tabId: string = '' + + constructor( + private mynahUi: MynahUI, + private messager: Messager + ) {} + + private onRuleFolderClick = (groupName: string) => { + this.messager.onRuleClick({ tabId: this.tabId, type: 'folder', id: groupName }) + } + + private onRuleClick = (item: DetailedListItem) => { + if (item.id) { + if (item.id === ContextRule.CreateRuleId) { + this.rulesList?.close() + this.mynahUi.showCustomForm( + this.tabId, + [ + { + id: ContextRule.RuleNameFieldId, + type: 'textinput', + mandatory: true, + autoFocus: true, + title: 'Rule name', + placeholder: 'Enter rule name', + validationPatterns: { + patterns: [ + { + pattern: /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,99}$/, + errorMessage: + 'Use only letters, numbers, hyphens, and underscores, starting with a letter or number. Maximum 100 characters.', + }, + ], + }, + description: + "This will create a [rule name].md file in your project's .amazonq/rules folder.", + }, + ], + [ + { + id: ContextRule.CancelButtonId, + text: 'Cancel', + status: 'clear', + waitMandatoryFormItems: false, + }, + { + id: ContextRule.SubmitButtonId, + text: 'Create', + status: 'main', + waitMandatoryFormItems: true, + }, + ], + `Create a rule` + ) + } else { + this.messager.onRuleClick({ tabId: this.tabId, type: 'rule', id: item.id }) + } + } + } + + showLoading(tabId: string) { + this.tabId = tabId + const rulesList = this.mynahUi.openTopBarButtonOverlay({ + tabId: this.tabId, + topBarButtonOverlay: { + list: [{ groupName: 'Loading rules...' }], + selectable: false, + }, + events: { + onGroupClick: this.onRuleFolderClick, + onItemClick: this.onRuleClick, + onClose: this.onClose, + onKeyPress: this.onKeyPress, + }, + }) + + this.rulesList = { + ...rulesList, + changeTarget: () => {}, + getTargetElementId: () => { + return undefined + }, + } + } + + show(params: ListRulesResult) { + this.tabId = params.tabId + if (this.rulesList) { + this.rulesList.update({ + filterOptions: params.filterOptions?.map(option => ({ + ...option, + icon: option.icon as MynahIconsType, + })), + list: convertRulesListToDetailedListGroup(params.rules), + selectable: 'clickable', + }) + } else { + const rulesList = this.mynahUi.openTopBarButtonOverlay({ + tabId: this.tabId, + topBarButtonOverlay: { + list: convertRulesListToDetailedListGroup(params.rules), + selectable: 'clickable', + }, + events: { + onGroupClick: this.onRuleFolderClick, + onItemClick: this.onRuleClick, + onClose: this.onClose, + onKeyPress: this.onKeyPress, + }, + }) + + this.rulesList = { + ...rulesList, + changeTarget: () => {}, + getTargetElementId: () => { + return undefined + }, + } + } + } + + private onKeyPress = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + this.close() + } + } + + close() { + this.rulesList?.close() + } + + private onClose = () => { + this.rulesList = undefined + } +} + +const createRuleListItem: DetailedListItem = { + description: 'Create a new rule', + icon: MynahIcons.LIST_ADD, + id: ContextRule.CreateRuleId, +} + +export function convertRulesListToDetailedListGroup(rules: RulesFolder[]): DetailedListItemGroup[] { + return rules + .map( + ruleFolder => + ({ + groupName: ruleFolder.folderName, + actions: [ + { + id: ruleFolder.folderName, + icon: convertRuleStatusToIcon(ruleFolder.active), + status: 'clear', + }, + ], + icon: MynahIcons.FOLDER, + childrenIndented: true, + children: ruleFolder.rules.map(rule => ({ + id: rule.id, + icon: MynahIcons.CHECK_LIST, + description: rule.name, + actions: [{ id: rule.id, icon: convertRuleStatusToIcon(rule.active), status: 'clear' }], + })), + }) as DetailedListItemGroup + ) + .concat({ children: [createRuleListItem] }) +} + +function convertRuleStatusToIcon(status: boolean | 'indeterminate'): MynahIcons | undefined { + if (status === true) { + return MynahIcons.OK + } else if (status === 'indeterminate') { + return MynahIcons.MINUS + } + return undefined +} diff --git a/chat-client/src/client/messager.ts b/chat-client/src/client/messager.ts index e0ee13eaa1..fa2e340608 100644 --- a/chat-client/src/client/messager.ts +++ b/chat-client/src/client/messager.ts @@ -34,11 +34,14 @@ import { InfoLinkClickParams, LinkClickParams, ListConversationsParams, + ListRulesParams, ListMcpServersParams, McpServerClickParams, OpenTabResult, + PinnedContextParams, PromptInputOptionChangeParams, QuickActionParams, + RuleClickParams, SourceLinkClickParams, TabAddParams, TabBarActionParams, @@ -100,16 +103,24 @@ export interface OutboundChatApi { stopChatResponse(tabId: string): void sendButtonClickEvent(params: ButtonClickParams): void onOpenSettings(settingKey: string): void + onRuleClick(params: RuleClickParams): void + listRules(params: ListRulesParams): void + onAddPinnedContext(params: PinnedContextParams): void + onRemovePinnedContext(params: PinnedContextParams): void } export class Messager { constructor(private readonly chatApi: OutboundChatApi) {} - onTabAdd = (tabId: string, triggerType?: TriggerType): void => { - this.chatApi.tabAdded({ tabId }) + onTabAdd = (tabId: string, triggerType?: TriggerType, restoredTab?: boolean): void => { + this.chatApi.tabAdded({ tabId, restoredTab }) this.chatApi.telemetry({ triggerType: triggerType ?? 'click', tabId, name: TAB_ADD_TELEMETRY_EVENT }) } + onRuleClick = (params: RuleClickParams): void => { + this.chatApi.onRuleClick(params) + } + onTabChange = (tabId: string): void => { this.chatApi.tabChanged({ tabId }) } @@ -203,8 +214,8 @@ export class Messager { this.chatApi.onOpenTab(requestId, result) } - onCreatePrompt = (promptName: string): void => { - this.chatApi.createPrompt({ promptName }) + onCreatePrompt = (params: CreatePromptParams): void => { + this.chatApi.createPrompt(params) } onFileClick = (params: FileClickParams): void => { @@ -218,6 +229,10 @@ export class Messager { } } + onListRules = (params: ListRulesParams): void => { + this.chatApi.listRules(params) + } + onConversationClick = (conversationId: string, action?: ConversationAction): void => { this.chatApi.conversationClick({ id: conversationId, action }) } @@ -257,4 +272,12 @@ export class Messager { onOpenSettings = (settingKey: string): void => { this.chatApi.onOpenSettings(settingKey) } + + onAddPinnedContext = (params: PinnedContextParams) => { + this.chatApi.onAddPinnedContext(params) + } + + onRemovePinnedContext = (params: PinnedContextParams) => { + this.chatApi.onRemovePinnedContext(params) + } } diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts index 80be9073c6..f92bb01295 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -70,6 +70,10 @@ describe('MynahUI', () => { stopChatResponse: sinon.stub(), sendButtonClickEvent: sinon.stub(), onOpenSettings: sinon.stub(), + onRuleClick: sinon.stub(), + listRules: sinon.stub(), + onAddPinnedContext: sinon.stub(), + onRemovePinnedContext: sinon.stub(), } messager = new Messager(outboundChatApi) diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 10c6ef1c8c..105e6890b4 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -26,10 +26,13 @@ import { InfoLinkClickParams, LinkClickParams, ListConversationsResult, + ListRulesResult, ListMcpServersResult, McpServerClickResult, OPEN_WORKSPACE_INDEX_SETTINGS_BUTTON_ID, OpenTabParams, + PinnedContextParams, + RuleClickResult, SourceLinkClickParams, } from '@aws/language-server-runtimes-types' import { @@ -60,6 +63,7 @@ import { } from './utils' import { ChatHistory, ChatHistoryList } from './features/history' import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming' +import { ContextRule, RulesList } from './features/rules' import { getModelSelectionChatItem, modelUnavailableBanner } from './texts/modelSelection' import { freeTierLimitSticky, @@ -78,11 +82,14 @@ export interface InboundChatApi { openTab(requestId: string, params: OpenTabParams): void sendContextCommands(params: ContextCommandParams): void listConversations(params: ListConversationsResult): void + listRules(params: ListRulesResult): void conversationClicked(params: ConversationClickResult): void + ruleClicked(params: RuleClickResult): void listMcpServers(params: ListMcpServersResult): void mcpServerClick(params: McpServerClickResult): void getSerializedChat(requestId: string, params: GetSerializedChatParams): void createTabId(openTab?: boolean): string | undefined + sendPinnedContext(params: PinnedContextParams): void } type ContextCommandGroups = MynahUIDataModel['contextCommands'] @@ -302,7 +309,7 @@ export const createMynahUi = ( defaultTabConfig.chatItems = tabFactory.getChatItems(true, programmingModeCardActive, []) } mynahUi.updateStore(tabId, defaultTabConfig) - messager.onTabAdd(tabId) + messager.onTabAdd(tabId, undefined, tabStore?.tabMetadata?.openTabKey === true) }, onTabRemove: (tabId: string) => { messager.onStopChatResponse(tabId) @@ -343,6 +350,18 @@ export const createMynahUi = ( } messager.onVote(payload) }, + onPromptTopBarItemAdded: (tabId, item, eventId) => { + messager.onAddPinnedContext({ tabId, contextCommandGroups: [{ commands: [item as ContextCommand] }] }) + }, + onPromptTopBarItemRemoved: (tabId, item, eventId) => { + messager.onRemovePinnedContext({ tabId, contextCommandGroups: [{ commands: [item as ContextCommand] }] }) + }, + onPromptTopBarButtonClick(tabId, button, eventId) { + if (button.id === 'Rules') { + rulesList.showLoading(tabId) + messager.onListRules({ tabId }) + } + }, onSendFeedback: (tabId, feedbackPayload, eventId) => { const payload: FeedbackParams = { tabId, @@ -467,7 +486,12 @@ export const createMynahUi = ( }, onCustomFormAction: (tabId, action) => { if (action.id === ContextPrompt.SubmitButtonId) { - messager.onCreatePrompt(action.formItemValues![ContextPrompt.PromptNameFieldId]) + messager.onCreatePrompt({ promptName: action.formItemValues![ContextPrompt.PromptNameFieldId] }) + } else if (action.id === ContextRule.SubmitButtonId) { + messager.onCreatePrompt({ + promptName: action.formItemValues![ContextRule.RuleNameFieldId], + isRule: true, + }) } }, onFormTextualItemKeyPress: ( @@ -477,10 +501,16 @@ export const createMynahUi = ( _tabId: string, _eventId?: string ) => { - if (itemId === ContextPrompt.PromptNameFieldId && event.key === 'Enter') { - event.preventDefault() - messager.onCreatePrompt(formData[ContextPrompt.PromptNameFieldId]) - return true + if (event.key === 'Enter') { + if (itemId === ContextPrompt.PromptNameFieldId) { + event.preventDefault() + messager.onCreatePrompt({ promptName: formData[ContextPrompt.PromptNameFieldId] }) + return true + } else if (itemId === ContextRule.RuleNameFieldId) { + event.preventDefault() + messager.onCreatePrompt({ promptName: formData[ContextRule.RuleNameFieldId], isRule: true }) + return true + } } return false }, @@ -1209,6 +1239,31 @@ ${params.message}`, })) } + const sendPinnedContext = (params: PinnedContextParams) => { + const pinnedContext = toContextCommands(params.contextCommandGroups[0]?.commands || []) + const activeEditor = pinnedContext[0]?.id === ACTIVE_EDITOR_CONTEXT_ID + // Update Active File pill description with active editor URI passed from IDE + if (activeEditor) { + if (params.textDocument != null) { + pinnedContext[0].description = params.textDocument.uri + } else { + pinnedContext.shift() + } + } + let promptTopBarTitle = '@' + // Show full `@Pin Context` title until user adds a pinned context item + if (pinnedContext.length == 0 || (activeEditor && pinnedContext.length === 1)) { + promptTopBarTitle = '@Pin Context' + } + mynahUi.updateStore(params.tabId, { + promptTopBarContextItems: pinnedContext, + promptTopBarTitle, + promptTopBarButton: params.showRules + ? { id: 'Rules', status: 'clear', text: 'Rules', icon: 'check-list' } + : null, + }) + } + const sendContextCommands = (params: ContextCommandParams) => { contextCommandGroups = params.contextCommandGroups.map(group => ({ ...group, @@ -1237,6 +1292,23 @@ ${params.message}`, chatHistoryList.show(params) } + const rulesList = new RulesList(mynahUi, messager) + + const listRules = (params: ListRulesResult) => { + rulesList.show(params) + } + + const ruleClicked = (params: RuleClickResult) => { + if (!params.success) { + mynahUi.notify({ + content: `Failed to toggle the workspace rule`, + type: NotificationType.ERROR, + }) + return + } + messager.onListRules({ tabId: params.tabId }) + } + const conversationClicked = (params: ConversationClickResult) => { if (!params.success) { mynahUi.notify({ @@ -1312,17 +1384,22 @@ ${params.message}`, showError: showError, openTab: openTab, sendContextCommands: sendContextCommands, + sendPinnedContext: sendPinnedContext, listConversations: listConversations, + listRules: listRules, conversationClicked: conversationClicked, listMcpServers: listMcpServers, mcpServerClick: mcpServerClick, getSerializedChat: getSerializedChat, createTabId: createTabId, + ruleClicked: ruleClicked, } return [mynahUi, api] } +const ACTIVE_EDITOR_CONTEXT_ID = 'active-editor' + export const DEFAULT_HELP_PROMPT = 'What can Amazon Q help me with?' const uiComponentsTexts = { mainTitle: 'Amazon Q (Preview)', diff --git a/chat-client/src/client/utils.ts b/chat-client/src/client/utils.ts index 2a6d29aca3..c951f7ffe8 100644 --- a/chat-client/src/client/utils.ts +++ b/chat-client/src/client/utils.ts @@ -74,6 +74,7 @@ export function toMynahContextCommand(feature?: FeatureContext): any { return { command: feature.value.stringValue, + id: feature.value.stringValue, description: feature.variation, } } diff --git a/chat-client/src/client/withAdapter.ts b/chat-client/src/client/withAdapter.ts index 51b35dafa9..1246fa091f 100644 --- a/chat-client/src/client/withAdapter.ts +++ b/chat-client/src/client/withAdapter.ts @@ -59,6 +59,9 @@ export const withAdapter = ( onPromptInputOptionChange: addDefaultRouting('onPromptInputOptionChange'), onPromptInputButtonClick: addDefaultRouting('onPromptInputButtonClick'), onMessageDismiss: addDefaultRouting('onMessageDismiss'), + onPromptTopBarItemAdded: addDefaultRouting('onPromptTopBarItemAdded'), + onPromptTopBarItemRemoved: addDefaultRouting('onPromptTopBarItemRemoved'), + onPromptTopBarButtonClick: addDefaultRouting('onPromptTopBarButtonClick'), /** * Handler with special routing logic diff --git a/chat-client/src/contracts/chatClientAdapter.ts b/chat-client/src/contracts/chatClientAdapter.ts index ee0b41683e..5f8d093f1d 100644 --- a/chat-client/src/contracts/chatClientAdapter.ts +++ b/chat-client/src/contracts/chatClientAdapter.ts @@ -38,6 +38,9 @@ export interface ChatEventHandler | 'onPromptInputOptionChange' | 'onPromptInputButtonClick' | 'onMessageDismiss' + | 'onPromptTopBarItemAdded' + | 'onPromptTopBarItemRemoved' + | 'onPromptTopBarButtonClick' > {} /** diff --git a/chat-client/src/contracts/serverContracts.ts b/chat-client/src/contracts/serverContracts.ts index a47ee0716a..641b20191a 100644 --- a/chat-client/src/contracts/serverContracts.ts +++ b/chat-client/src/contracts/serverContracts.ts @@ -39,6 +39,13 @@ import { GetSerializedChatResult, PROMPT_INPUT_OPTION_CHANGE_METHOD, BUTTON_CLICK_REQUEST_METHOD, + RULE_CLICK_REQUEST_METHOD, + RuleClickParams, + ListRulesParams, + LIST_RULES_REQUEST_METHOD, + PINNED_CONTEXT_ADD_NOTIFICATION_METHOD, + PINNED_CONTEXT_REMOVE_NOTIFICATION_METHOD, + PinnedContextParams, } from '@aws/language-server-runtimes-types' export const TELEMETRY = 'telemetry/event' @@ -60,6 +67,7 @@ export type ServerMessageCommand = | typeof CREATE_PROMPT_NOTIFICATION_METHOD | typeof FILE_CLICK_NOTIFICATION_METHOD | typeof LIST_CONVERSATIONS_REQUEST_METHOD + | typeof LIST_RULES_REQUEST_METHOD | typeof CONVERSATION_CLICK_REQUEST_METHOD | typeof LIST_MCP_SERVERS_REQUEST_METHOD | typeof MCP_SERVER_CLICK_REQUEST_METHOD @@ -67,6 +75,9 @@ export type ServerMessageCommand = | typeof GET_SERIALIZED_CHAT_REQUEST_METHOD | typeof PROMPT_INPUT_OPTION_CHANGE_METHOD | typeof BUTTON_CLICK_REQUEST_METHOD + | typeof RULE_CLICK_REQUEST_METHOD + | typeof PINNED_CONTEXT_ADD_NOTIFICATION_METHOD + | typeof PINNED_CONTEXT_REMOVE_NOTIFICATION_METHOD export interface ServerMessage { command: ServerMessageCommand @@ -99,3 +110,6 @@ export type ServerMessageParams = | McpServerClickParams | TabBarActionParams | GetSerializedChatResult + | RuleClickParams + | ListRulesParams + | PinnedContextParams diff --git a/client/vscode/src/chatActivation.ts b/client/vscode/src/chatActivation.ts index 449a63db0d..e9d635fd67 100644 --- a/client/vscode/src/chatActivation.ts +++ b/client/vscode/src/chatActivation.ts @@ -32,6 +32,8 @@ import { chatOptionsUpdateType, buttonClickRequestType, chatUpdateNotificationType, + listRulesRequestType, + ruleClickRequestType, } from '@aws/language-server-runtimes/protocol' import { v4 as uuidv4 } from 'uuid' import { Uri, Webview, WebviewView, commands, window } from 'vscode' @@ -164,6 +166,22 @@ export function registerChat( listConversationsRequestType.method ) break + case ruleClickRequestType.method: + await handleRequest( + languageClient, + message.params, + webviewView, + ruleClickRequestType.method + ) + break + case listRulesRequestType.method: + await handleRequest( + languageClient, + message.params, + webviewView, + listRulesRequestType.method + ) + break case conversationClickRequestType.method: await handleRequest( languageClient, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index 2ee4cc9ede..e4bb167422 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -29,6 +29,10 @@ import { InlineChatResultParams, PromptInputOptionChangeParams, TextDocument, + RuleClickParams, + ListRulesParams, + ActiveEditorChangedParams, + PinnedContextParams, ChatUpdateParams, MessageType, ExecuteCommandParams, @@ -114,7 +118,12 @@ import { TriggerContext, } from './context/agenticChatTriggerContext' import { AdditionalContextProvider } from './context/addtionalContextProvider' -import { getNewPromptFilePath, getUserPromptsDirectory, promptFileExtension } from './context/contextUtils' +import { + getNewPromptFilePath, + getNewRuleFilePath, + getUserPromptsDirectory, + promptFileExtension, +} from './context/contextUtils' import { ContextCommandsProvider } from './context/contextCommandsProvider' import { LocalProjectContextController } from '../../shared/localProjectContextController' import { CancellationError, workspaceUtils } from '@aws/lsp-core' @@ -168,6 +177,11 @@ type ChatHandlers = Omit< | 'chatOptionsUpdate' | 'onListMcpServers' | 'onMcpServerClick' + | 'onListRules' + | 'sendPinnedContext' + | 'onActiveEditorChanged' + | 'onPinnedContextAdd' + | 'onPinnedContextRemove' > export class AgenticChatController implements ChatHandlers { @@ -214,8 +228,14 @@ export class AgenticChatController implements ChatHandlers { this.#telemetryService = telemetryService this.#serviceManager = serviceManager this.#chatHistoryDb = new ChatDatabase(features) - this.#tabBarController = new TabBarController(features, this.#chatHistoryDb, telemetryService) - this.#additionalContextProvider = new AdditionalContextProvider(features.workspace) + this.#tabBarController = new TabBarController( + features, + this.#chatHistoryDb, + telemetryService, + (tabId: string) => this.sendPinnedContext(tabId) + ) + + this.#additionalContextProvider = new AdditionalContextProvider(features, this.#chatHistoryDb) this.#contextCommandsProvider = new ContextCommandsProvider( this.#features.logging, this.#features.chat, @@ -393,6 +413,23 @@ export class AgenticChatController implements ChatHandlers { } async onCreatePrompt(params: CreatePromptParams): Promise { + if (params.isRule) { + let workspaceFolders = workspaceUtils.getWorkspaceFolderPaths(this.#features.workspace) + let workspaceRulesDirectory = path.join(workspaceFolders[0], '.amazonq', 'rules') + if (workspaceFolders.length > 0) { + const newFilePath = getNewRuleFilePath(params.promptName, workspaceRulesDirectory) + const newFileContent = '' + try { + await this.#features.workspace.fs.mkdir(workspaceRulesDirectory, { recursive: true }) + await this.#features.workspace.fs.writeFile(newFilePath, newFileContent, { mode: 0o600 }) + await this.#features.lsp.window.showDocument({ uri: URI.file(newFilePath).toString() }) + } catch (e) { + this.#features.logging.warn(`Error creating rule file: ${e}`) + } + return + } + } + const newFilePath = getNewPromptFilePath(params.promptName) const newFileContent = '' try { @@ -420,6 +457,14 @@ export class AgenticChatController implements ChatHandlers { return this.#tabBarController.onConversationClick(params) } + async onRuleClick(params: RuleClickParams) { + return this.#additionalContextProvider.onRuleClick(params) + } + + async onListRules(params: ListRulesParams) { + return this.#additionalContextProvider.onListRules(params) + } + async onListMcpServers(params: ListMcpServersParams) { return this.#mcpEventHandler.onListMcpServers(params) } @@ -506,6 +551,7 @@ export class AgenticChatController implements ChatHandlers { const additionalContext = await this.#additionalContextProvider.getAdditionalContext( triggerContext, + params.tabId, params.context ) if (additionalContext.length) { @@ -2089,7 +2135,8 @@ export class AgenticChatController implements ChatHandlers { cwsprChatHasContextList: triggerContext.documentReference?.filePaths?.length ? true : false, cwsprChatFolderContextCount: triggerContext.contextInfo.contextCount.folderContextCount, cwsprChatFileContextCount: triggerContext.contextInfo.contextCount.fileContextCount, - cwsprChatRuleContextCount: triggerContext.contextInfo.contextCount.ruleContextCount, + cwsprChatRuleContextCount: triggerContext.contextInfo.contextCount.activeRuleContextCount, + cwsprChatTotalRuleContextCount: triggerContext.contextInfo.contextCount.totalRuleContextCount, cwsprChatPromptContextCount: triggerContext.contextInfo.contextCount.promptContextCount, cwsprChatFileContextLength: triggerContext.contextInfo.contextLength.fileContextLength, cwsprChatRuleContextLength: triggerContext.contextInfo.contextLength.ruleContextLength, @@ -2097,6 +2144,10 @@ export class AgenticChatController implements ChatHandlers { cwsprChatCodeContextCount: triggerContext.contextInfo.contextCount.codeContextCount, cwsprChatCodeContextLength: triggerContext.contextInfo.contextLength.codeContextLength, cwsprChatFocusFileContextLength: triggerContext.text?.length, + cwsprChatPinnedCodeContextCount: triggerContext.contextInfo.pinnedContextCount.codeContextCount, + cwsprChatPinnedFileContextCount: triggerContext.contextInfo.pinnedContextCount.fileContextCount, + cwsprChatPinnedFolderContextCount: triggerContext.contextInfo.pinnedContextCount.folderContextCount, + cwsprChatPinnedPromptContextCount: triggerContext.contextInfo.pinnedContextCount.promptContextCount, }) } await this.#telemetryController.emitAddMessageMetric(params.tabId, metric.metric, 'Succeeded') @@ -2302,6 +2353,12 @@ export class AgenticChatController implements ChatHandlers { await this.#telemetryService.emitInlineChatResultLog(params) } + async onActiveEditorChanged(params: ActiveEditorChangedParams): Promise { + if (this.#telemetryController.activeTabId) { + this.sendPinnedContext(this.#telemetryController.activeTabId) + } + } + async onCodeInsertToCursorPosition(params: InsertToCursorPositionParams) { // Implementation based on https://github.com/aws/aws-toolkit-vscode/blob/1814cc84228d4bf20270574c5980b91b227f31cf/packages/core/src/amazonq/commons/controllers/contentController.ts#L38 if (!params.textDocument || !params.cursorPosition || !params.code) { @@ -2468,6 +2525,10 @@ export class AgenticChatController implements ChatHandlers { const modelId = this.#chatHistoryDb.getModelId() ?? defaultModelId this.#features.chat.chatOptionsUpdate({ modelId: modelId, tabId: params.tabId }) + if (!params.restoredTab) { + this.sendPinnedContext(params.tabId) + } + const sessionResult = this.#chatSessionManagementService.createSession(params.tabId) const { data: session, success } = sessionResult if (!success) { @@ -2490,6 +2551,8 @@ export class AgenticChatController implements ChatHandlers { this.#telemetryController.activeTabId = params.tabId + this.sendPinnedContext(params.tabId) + this.#telemetryController.emitConversationMetric({ name: ChatTelemetryEventName.EnterFocusConversation, data: {}, @@ -2498,6 +2561,10 @@ export class AgenticChatController implements ChatHandlers { this.setPaidTierMode(params.tabId) } + sendPinnedContext(tabId: string) { + this.#additionalContextProvider.sendPinnedContext(tabId) + } + onTabRemove(params: TabRemoveParams) { if (this.#telemetryController.activeTabId === params.tabId) { this.#telemetryController.emitConversationMetric({ @@ -2548,6 +2615,14 @@ export class AgenticChatController implements ChatHandlers { } } + onPinnedContextAdd(params: PinnedContextParams) { + this.#additionalContextProvider.onPinnedContextAdd(params) + } + + onPinnedContextRemove(params: PinnedContextParams) { + this.#additionalContextProvider.onPinnedContextRemove(params) + } + async onTabBarAction(params: TabBarActionParams) { return this.#tabBarController.onTabBarAction(params) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts index 28e5a365c8..a4a9bfae55 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts @@ -8,10 +8,12 @@ import { AdditionalContextProvider } from './addtionalContextProvider' import { getUserPromptsDirectory } from './contextUtils' import { LocalProjectContextController } from '../../../shared/localProjectContextController' import { workspaceUtils } from '@aws/lsp-core' +import { ChatDatabase } from '../tools/chatDb/chatDb' describe('AdditionalContextProvider', () => { let provider: AdditionalContextProvider let testFeatures: TestFeatures + let chatHistoryDb: ChatDatabase let fsExistsStub: sinon.SinonStub let getContextCommandPromptStub: sinon.SinonStub let fsReadDirStub: sinon.SinonStub @@ -23,8 +25,26 @@ describe('AdditionalContextProvider', () => { fsReadDirStub = sinon.stub() testFeatures.workspace.fs.exists = fsExistsStub testFeatures.workspace.fs.readdir = fsReadDirStub + testFeatures.chat.sendPinnedContext = sinon.stub() getContextCommandPromptStub = sinon.stub() - provider = new AdditionalContextProvider(testFeatures.workspace) + chatHistoryDb = { + getHistory: sinon.stub().returns([]), + searchMessages: sinon.stub().returns([]), + getOpenTabId: sinon.stub(), + getTab: sinon.stub(), + deleteHistory: sinon.stub(), + setHistoryIdMapping: sinon.stub(), + getOpenTabs: sinon.stub().returns([]), + updateTabOpenState: sinon.stub(), + getDatabaseFileSize: sinon.stub(), + getLoadTime: sinon.stub(), + getRules: sinon.stub(), + setRules: sinon.stub(), + addPinnedContext: sinon.stub(), + removePinnedContext: sinon.stub(), + } as unknown as ChatDatabase + + provider = new AdditionalContextProvider(testFeatures, chatHistoryDb) localProjectContextControllerInstanceStub = sinon.stub(LocalProjectContextController, 'getInstance').resolves({ getContextCommandPrompt: getContextCommandPromptStub, } as unknown as LocalProjectContextController) @@ -45,7 +65,7 @@ describe('AdditionalContextProvider', () => { fsExistsStub.resolves(false) getContextCommandPromptStub.resolves([]) - const result = await provider.getAdditionalContext(triggerContext) + const result = await provider.getAdditionalContext(triggerContext, '') assert.deepStrictEqual(result, []) }) @@ -77,7 +97,7 @@ describe('AdditionalContextProvider', () => { }, ]) - const result = await provider.getAdditionalContext(triggerContext) + const result = await provider.getAdditionalContext(triggerContext, '') assert.strictEqual(result.length, 1) assert.strictEqual(result[0].name, 'Test Rule') diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts index b1c43ba413..e246f43576 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts @@ -1,4 +1,14 @@ -import { FileDetails, QuickActionCommand, FileList, ContextCommand } from '@aws/language-server-runtimes/protocol' +import { + FileDetails, + FileList, + ContextCommand, + ListRulesParams, + ListRulesResult, + RuleClickParams, + RuleClickResult, + RulesFolder, + PinnedContextParams, +} from '@aws/language-server-runtimes/protocol' import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing' import * as path from 'path' import { @@ -8,48 +18,153 @@ import { workspaceChunkMaxSize, } from './agenticChatTriggerContext' import { URI } from 'vscode-uri' -import { Workspace } from '@aws/language-server-runtimes/server-interface' import { pathUtils, workspaceUtils } from '@aws/lsp-core' import { additionalContentNameLimit, getUserPromptsDirectory, - initialContextInfo, + getInitialContextInfo, promptFileExtension, } from './contextUtils' import { LocalProjectContextController } from '../../../shared/localProjectContextController' +import { Features } from '../../types' +import { ChatDatabase } from '../tools/chatDb/chatDb' + +export const ACTIVE_EDITOR_CONTEXT_ID = 'active-editor' + +export const activeFileCmd = { + command: 'Active file', + id: ACTIVE_EDITOR_CONTEXT_ID, + icon: 'file', + description: 'Reference active text file', +} +type ContextCommandInfo = ContextCommand & { pinned: boolean } + +/** + * AdditionalContextProvider manages context information for Amazon Q chat sessions. + * It handles workspace rules, pinned context, and file context for chat interactions. + * The provider retrieves available rules whenever requested by the client. + */ export class AdditionalContextProvider { - constructor(private readonly workspace: Workspace) {} + private totalRulesCount: number = 0 + + constructor( + private readonly features: Features, + private readonly chatDb: ChatDatabase + ) {} + + /** + * Recursively collects markdown files from a directory and its subdirectories + * @param workspaceFolder The root workspace folder path + * @param dirPath The directory to search in + * @param rulesFiles Array to collect the found files + */ + private async collectMarkdownFilesRecursively( + workspaceFolder: string, + dirPath: string, + rulesFiles: ContextCommandItem[] + ): Promise { + const entries = await this.features.workspace.fs.readdir(dirPath) - async collectWorkspaceRules(): Promise { + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name) + + if (entry.isDirectory()) { + // Recursively search subdirectories + await this.collectMarkdownFilesRecursively(workspaceFolder, fullPath, rulesFiles) + } else if (entry.isFile() && entry.name.endsWith(promptFileExtension)) { + // Add markdown file to the list + const relativePath = path.relative(workspaceFolder, fullPath) + rulesFiles.push({ + workspaceFolder: workspaceFolder, + type: 'file', + relativePath, + id: fullPath, + }) + } + } + } + + /** + * Internal method to collect workspace rules without tab filtering + */ + private async collectWorkspaceRulesInternal(): Promise { const rulesFiles: ContextCommandItem[] = [] - let workspaceFolders = workspaceUtils.getWorkspaceFolderPaths(this.workspace) + let workspaceFolders = workspaceUtils.getWorkspaceFolderPaths(this.features.workspace) if (!workspaceFolders.length) { return rulesFiles } + for (const workspaceFolder of workspaceFolders) { + // Check for rules in .amazonq/rules directory and its subdirectories const rulesPath = path.join(workspaceFolder, '.amazonq', 'rules') - const folderExists = await this.workspace.fs.exists(rulesPath) + const folderExists = await this.features.workspace.fs.exists(rulesPath) if (folderExists) { - const entries = await this.workspace.fs.readdir(rulesPath) - - for (const entry of entries) { - if (entry.isFile() && entry.name.endsWith(promptFileExtension)) { - rulesFiles.push({ - workspaceFolder: workspaceFolder, - type: 'file', - relativePath: path.relative(workspaceFolder, path.join(rulesPath, entry.name)), - id: '', - }) - } - } + await this.collectMarkdownFilesRecursively(workspaceFolder, rulesPath, rulesFiles) + } + + // Check for README.md in workspace root + const readmePath = path.join(workspaceFolder, 'README.md') + const readmeExists = await this.features.workspace.fs.exists(readmePath) + if (readmeExists) { + rulesFiles.push({ + workspaceFolder: workspaceFolder, + type: 'file', + relativePath: 'README.md', + id: readmePath, + }) + } + + // Check for AmazonQ.md in workspace root + const amazonQPath = path.join(workspaceFolder, 'AmazonQ.md') + const amazonQExists = await this.features.workspace.fs.exists(amazonQPath) + if (amazonQExists) { + rulesFiles.push({ + workspaceFolder: workspaceFolder, + type: 'file', + relativePath: 'AmazonQ.md', + id: amazonQPath, + }) } } + return rulesFiles } + async collectWorkspaceRules(tabId?: string): Promise { + // Always collect rules directly from the filesystem + const rulesFiles = await this.collectWorkspaceRulesInternal() + this.totalRulesCount = rulesFiles.length + + // If no tabId, return all rules without filtering + if (!tabId) { + return rulesFiles + } + + // Filter rules based on user's rules preferences for current tab + let rulesState = this.chatDb.getRules(tabId) || { folders: {}, rules: {} } + return rulesFiles.filter(rule => { + // If the rule has an explicit state in rulesState, use that value + if (rulesState.rules[rule.id] !== undefined) { + return rulesState.rules[rule.id] + } + + // Otherwise, check the parent folder's state + const dirPath = path.dirname(rule.relativePath) + const folderName = dirPath === '.' ? '' : dirPath + + // If folder state is explicitly set to false, the rule inherits that state + if (rulesState.folders[folderName] === false) { + return false + } + + // Default to true for all other cases + return true + }) + } + getContextType(prompt: AdditionalContextPrompt): string { if (prompt.name === 'symbol') { return 'code' @@ -66,48 +181,73 @@ export class AdditionalContextProvider { async getAdditionalContext( triggerContext: TriggerContext, + tabId: string, context?: ContextCommand[] ): Promise { - if (!triggerContext.contextInfo) { - triggerContext.contextInfo = initialContextInfo - } + triggerContext.contextInfo = getInitialContextInfo() + const additionalContextCommands: ContextCommandItem[] = [] - const workspaceRules = await this.collectWorkspaceRules() + const workspaceRules = await this.collectWorkspaceRules(tabId) let workspaceFolderPath = triggerContext.workspaceFolder?.uri ? URI.parse(triggerContext.workspaceFolder.uri).fsPath - : workspaceUtils.getWorkspaceFolderPaths(this.workspace)[0] + : workspaceUtils.getWorkspaceFolderPaths(this.features.workspace)[0] if (workspaceRules.length > 0) { additionalContextCommands.push(...workspaceRules) } - triggerContext.contextInfo.contextCount.ruleContextCount = workspaceRules.length - if (context) { - let fileContextCount = 0 - let folderContextCount = 0 - let promptContextCount = 0 - let codeContextCount = 0 - additionalContextCommands.push(...this.mapToContextCommandItems(context, workspaceFolderPath)) - for (const c of context) { - if (typeof context !== 'string') { - if (c.id === 'prompt') { - promptContextCount++ - } else if (c.label === 'file') { - fileContextCount++ - } else if (c.label === 'folder') { - folderContextCount++ - } else if (c.label === 'code') { - codeContextCount++ - } - } - } - triggerContext.contextInfo!.contextCount = { - ...triggerContext.contextInfo!.contextCount, - fileContextCount, - folderContextCount, - promptContextCount, - codeContextCount, + + // Merge pinned context with context added to prompt, avoiding duplicates + let contextInfo: ContextCommandInfo[] = (context?.map(item => ({ ...item, pinned: false })) || []).concat( + this.chatDb + .getPinnedContext(tabId) + .filter(item => !context?.find(innerItem => item.id === innerItem.id)) + .map(item => ({ ...item, pinned: true })) + ) + // If Active File context pill was removed from pinned context, remove it from payload + if (!contextInfo?.find(item => item.id === ACTIVE_EDITOR_CONTEXT_ID)) { + triggerContext.text = undefined + triggerContext.cursorState = undefined + } else { + // Remove Active File from context list since its contents have already been added to triggerContext.text + contextInfo = contextInfo.filter(item => item.id !== ACTIVE_EDITOR_CONTEXT_ID) + } + + if (contextInfo.some(item => item.id === '@workspace')) { + triggerContext.hasWorkspace = true + } + + const contextCounts = getInitialContextInfo() + + additionalContextCommands.push(...this.mapToContextCommandItems(contextInfo, workspaceFolderPath)) + for (const c of contextInfo) { + if (c.id === 'prompt') { + c.pinned + ? contextCounts.pinnedContextCount.promptContextCount++ + : contextCounts.contextCount.promptContextCount++ + } else if (c.label === 'file') { + c.pinned + ? contextCounts.pinnedContextCount.fileContextCount++ + : contextCounts.contextCount.fileContextCount++ + } else if (c.label === 'folder') { + c.pinned + ? contextCounts.pinnedContextCount.folderContextCount++ + : contextCounts.contextCount.folderContextCount++ + } else if (c.label === 'code') { + c.pinned + ? contextCounts.pinnedContextCount.codeContextCount++ + : contextCounts.contextCount.codeContextCount++ } } + triggerContext.contextInfo = { + ...triggerContext.contextInfo, + contextCount: { + ...contextCounts.contextCount, + activeRuleContextCount: workspaceRules.length, + totalRuleContextCount: this.totalRulesCount, + }, + + pinnedContextCount: contextCounts.pinnedContextCount, + } if (additionalContextCommands.length === 0) { return [] @@ -202,4 +342,207 @@ export class AdditionalContextProvider { } return contextCommands } + + sendPinnedContext(tabId: string): void { + let pinnedContextEnabled = + this.features.lsp.getClientInitializeParams()?.initializationOptions?.aws?.awsClientCapabilities?.q + ?.pinnedContextEnabled === true + if (pinnedContextEnabled) { + let pinnedContext = this.chatDb.getPinnedContext(tabId) + this.features.chat.sendPinnedContext({ + tabId, + contextCommandGroups: [ + { + commands: pinnedContext, + }, + ], + showRules: workspaceUtils.getWorkspaceFolderPaths(this.features.workspace).length > 0, + }) + } + } + + async getRulesFolders(tabId: string): Promise { + const workspaceRules = await this.collectWorkspaceRules() + return this.convertRulesToRulesFolders(workspaceRules, tabId) + } + + async onRuleClick(params: RuleClickParams): Promise { + let rulesState = { ...this.chatDb.getRules(params.tabId) } + if (params.type === 'folder') { + // Get current state (default to true if not set) + const currentActive = rulesState.folders[params.id] !== false + // Toggle the state + rulesState.folders[params.id] = !currentActive + + // Get all rules in this folder to update their states + const rulesFolders = await this.getRulesFolders(params.tabId) + const folder = rulesFolders.find(folder => folder.folderName === params.id) + + if (folder && folder.rules) { + // Update all rules in this folder to match folder state + folder.rules.forEach(rule => { + rulesState.rules[rule.id] = !currentActive + }) + } + this.chatDb.setRules(params.tabId, rulesState) + + return { ...params, success: true } + } else if (params.type === 'rule') { + // Get current state (default to true if not set) + const currentActive = rulesState.rules[params.id] !== false + // Toggle the state + rulesState.rules[params.id] = !currentActive + + // Check if we need to update parent folder state + const rulesFolders = await this.getRulesFolders(params.tabId) + const folder = rulesFolders.find(folder => folder.rules.some(rule => rule.id === params.id)) + + if (folder) { + // Check if all rules in folder are now active/inactive + const allRulesInFolder = folder.rules.map(r => r.id) + const activeRulesCount = allRulesInFolder.filter(ruleId => rulesState.rules[ruleId] !== false).length + + // Update folder state based on its rules + if (activeRulesCount === 0) { + rulesState.folders[folder.folderName || ''] = false + } else if (activeRulesCount === allRulesInFolder.length) { + rulesState.folders[folder.folderName || ''] = true + } + } + this.chatDb.setRules(params.tabId, rulesState) + + return { ...params, success: true } + } + + return { ...params, success: false } + } + + async onListRules(params: ListRulesParams): Promise { + return { + tabId: params.tabId, + rules: await this.getRulesFolders(params.tabId), + } + } + + onPinnedContextAdd(params: PinnedContextParams) { + // add to this.#pinnedContext if that id isnt already in there + let itemToAdd = params.contextCommandGroups[0]?.commands?.[0] + if (itemToAdd) { + this.chatDb.addPinnedContext(params.tabId, itemToAdd) + } + this.sendPinnedContext(params.tabId) + } + + onPinnedContextRemove(params: PinnedContextParams) { + let itemToRemove = params.contextCommandGroups[0]?.commands?.[0] + if (itemToRemove) { + this.chatDb.removePinnedContext(params.tabId, itemToRemove) + } + this.sendPinnedContext(params.tabId) + } + + private convertRulesToRulesFolders(workspaceRules: ContextCommandItem[], tabId: string): RulesFolder[] { + // Check if there's only one workspace folder + const workspaceFolders = workspaceUtils.getWorkspaceFolderPaths(this.features.workspace) + const isSingleWorkspace = workspaceFolders.length <= 1 + + // Group rules by their parent folder + const folderMap = new Map() + + for (const rule of workspaceRules) { + // Extract the folder path from the relativePath + let folderName: string | undefined + + // Get directory path + const dirPath = path.dirname(rule.relativePath) + + if (isSingleWorkspace) { + // In single workspace: root files have undefined folder name + if (dirPath === '.') { + folderName = undefined + } else { + folderName = dirPath + } + } else { + // In multi-workspace: include workspace folder name for all files + // Root files will use the workspace folder name + // Subdir files will use workspace folder name + subdir + const workspaceFolderName = path.basename(rule.workspaceFolder) + folderName = dirPath === '.' ? workspaceFolderName : `${workspaceFolderName}/${dirPath}` + } + + // Get or create the folder's rule list + const folderRules = folderMap.get(folderName || '') || [] + folderRules.push(rule) + folderMap.set(folderName || '', folderRules) + } + + // Convert the map to RulesFolder array + const rulesFolders: RulesFolder[] = [] + let rulesState = this.chatDb.getRules(tabId) + for (const [folderName, rules] of folderMap.entries()) { + // Map rules to their active states + const ruleStates = rules.map(rule => { + const ruleId = rule.id + // For rule active state: + // 1. If explicitly set in rules map, use that value + // 2. Otherwise, new rules are active by default + const folderDefaultState = + rulesState.folders[folderName] !== undefined ? rulesState.folders[folderName] : true + + return rulesState.rules[ruleId] !== undefined ? rulesState.rules[ruleId] : folderDefaultState + }) + + // Determine folder active state + let folderActive: boolean | 'indeterminate' + + // If explicitly set in folders map, start with that value + if (rulesState.folders[folderName] !== undefined) { + folderActive = rulesState.folders[folderName] + } else { + // Default to true for new folders + folderActive = true + } + + // Check if we need to set indeterminate state + // Count active and inactive rules + const activeRules = ruleStates.filter(state => state === true).length + const inactiveRules = ruleStates.filter(state => state === false).length + + // If there are both active and inactive rules, set to indeterminate + if (activeRules > 0 && inactiveRules > 0) { + folderActive = 'indeterminate' + } + + const rulesFolder: RulesFolder = { + folderName: folderName || undefined, + active: folderActive, + rules: rules.map((rule, index) => { + return { + name: path.basename(rule.relativePath, promptFileExtension), + active: ruleStates[index], + id: rule.id, + } + }), + } + + rulesFolders.push(rulesFolder) + } + + // Sort the folders: undefined folderName first, then alphabetically + rulesFolders.sort((a, b) => { + // If a has undefined folderName, it should come first + if (a.folderName === undefined) { + return -1 + } + // If b has undefined folderName, it should come first + if (b.folderName === undefined) { + return 1 + } + // Otherwise sort alphabetically + return a.folderName.localeCompare(b.folderName) + }) + + return rulesFolders + } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts index b02d424d28..cc1c3c0d01 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts @@ -40,6 +40,7 @@ export interface TriggerContext extends Partial { triggerType?: TriggerType contextInfo?: ContextInfo documentReference?: FileList + hasWorkspace?: boolean } export type LineInfo = { startLine: number; endLine: number } @@ -113,7 +114,7 @@ export class AgenticChatTriggerContext { const { prompt } = params const workspaceFolders = workspaceUtils.getWorkspaceFolderPaths(this.#workspace).slice(0, maxWorkspaceFolders) const defaultEditorState = { workspaceFolders } - const hasWorkspace = 'context' in params ? params.context?.some(c => c.command === '@workspace') : false + const hasWorkspace = triggerContext.hasWorkspace // prompt.prompt is what user typed in the input, should be sent to backend // prompt.escapedPrompt is HTML serialized string, which should only be used for UI. diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts index fda4461b5e..c1cf5b4eb0 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts @@ -7,6 +7,7 @@ import { getUserPromptsDirectory, promptFileExtension } from './contextUtils' import { ContextCommandItem } from 'local-indexing' import { LocalProjectContextController } from '../../../shared/localProjectContextController' import { URI } from 'vscode-uri' +import { activeFileCmd } from './addtionalContextProvider' export class ContextCommandsProvider implements Disposable { private promptFileWatcher?: FSWatcher @@ -143,9 +144,10 @@ export class ContextCommandsProvider implements Disposable { } const workspaceCmd = { command: '@workspace', - description: 'Reference all code in workspace.', + id: '@workspace', + description: 'Reference all code in workspace', } - const commands = [workspaceCmd, folderCmdGroup, fileCmdGroup, codeCmdGroup, promptCmdGroup] + const commands = [workspaceCmd, activeFileCmd, folderCmdGroup, fileCmdGroup, codeCmdGroup, promptCmdGroup] const allCommands: ContextCommandGroup[] = [ { commands: commands, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts index 8ef9a14b2f..927cd737d6 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts @@ -4,11 +4,18 @@ import { sanitizeFilename } from '@aws/lsp-core/out/util/text' import { RelevantTextDocumentAddition } from './agenticChatTriggerContext' import { FileDetails, FileList } from '@aws/language-server-runtimes/server-interface' export interface ContextInfo { + pinnedContextCount: { + fileContextCount: number + folderContextCount: number + promptContextCount: number + codeContextCount: number + } contextCount: { fileContextCount: number folderContextCount: number promptContextCount: number - ruleContextCount: number + activeRuleContextCount: number + totalRuleContextCount: number codeContextCount: number } contextLength: { @@ -19,20 +26,34 @@ export interface ContextInfo { } } -export const initialContextInfo: ContextInfo = { - contextCount: { - fileContextCount: 0, - folderContextCount: 0, - promptContextCount: 0, - ruleContextCount: 0, - codeContextCount: 0, - }, - contextLength: { - fileContextLength: 0, - ruleContextLength: 0, - promptContextLength: 0, - codeContextLength: 0, - }, +/** + * Creates a new ContextInfo object with all values initialized to 0. + * Use this function to get a fresh context info structure. + * @returns A new ContextInfo object with zero-initialized values + */ +export function getInitialContextInfo(): ContextInfo { + return { + pinnedContextCount: { + fileContextCount: 0, + folderContextCount: 0, + promptContextCount: 0, + codeContextCount: 0, + }, + contextCount: { + fileContextCount: 0, + folderContextCount: 0, + promptContextCount: 0, + activeRuleContextCount: 0, + totalRuleContextCount: 0, + codeContextCount: 0, + }, + contextLength: { + fileContextLength: 0, + ruleContextLength: 0, + promptContextLength: 0, + codeContextLength: 0, + }, + } } export const promptFileExtension = '.md' @@ -63,6 +84,24 @@ export const getNewPromptFilePath = (promptName: string): string => { return finalPath } +/** + * Creates a secure file path for a new rule file. + * + * @param ruleName - The user-provided name for the prompt + * @returns A sanitized file path within the user prompts directory + */ +export const getNewRuleFilePath = (ruleName: string, workspaceRulesDirectory: string): string => { + const trimmedName = ruleName?.trim() || '' + + const truncatedName = trimmedName.slice(0, 100) + + const safePromptName = truncatedName ? sanitizeFilename(path.basename(truncatedName)) : 'default' + + const finalPath = path.join(workspaceRulesDirectory, `${safePromptName}${promptFileExtension}`) + + return finalPath +} + /** * Merges a RelevantTextDocumentAddition array into a FileList, which is used to display list of context files. * This function combines document fragments from the same file, merging overlapping diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts index f6ae458807..8c41e2cb2a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts @@ -144,10 +144,18 @@ export const QAgenticChatServer = return chatController.onListConversations(params) }) + chat.onListRules(params => { + return chatController.onListRules(params) + }) + chat.onConversationClick(params => { return chatController.onConversationClick(params) }) + chat.onRuleClick(params => { + return chatController.onRuleClick(params) + }) + chat.onListMcpServers(params => { return chatController.onListMcpServers(params) }) @@ -188,6 +196,18 @@ export const QAgenticChatServer = return chatController.onInlineChatResult(params) }) + chat.onActiveEditorChanged(params => { + return chatController.onActiveEditorChanged(params) + }) + + chat.onPinnedContextAdd(params => { + return chatController.onPinnedContextAdd(params) + }) + + chat.onPinnedContextRemove(params => { + return chatController.onPinnedContextRemove(params) + }) + logging.log('Q Chat server has been initialized') return () => { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts index 0bb7090945..0fd456ff8f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts @@ -43,7 +43,7 @@ describe('TabBarController', () => { emitLoadHistory: sinon.stub(), } as any - tabBarController = new TabBarController(testFeatures, chatHistoryDb, telemetryService) + tabBarController = new TabBarController(testFeatures, chatHistoryDb, telemetryService, sinon.stub()) clock = sinon.useFakeTimers() }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts index f44da9b4da..db3fe2487f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.ts @@ -41,11 +41,18 @@ export class TabBarController { #features: Features #chatHistoryDb: ChatDatabase #telemetryService: TelemetryService - - constructor(features: Features, chatHistoryDb: ChatDatabase, telemetryService: TelemetryService) { + #sendPinnedContext: (tabId: string) => void + + constructor( + features: Features, + chatHistoryDb: ChatDatabase, + telemetryService: TelemetryService, + sendPinnedContext: (tabId: string) => void + ) { this.#features = features this.#chatHistoryDb = chatHistoryDb this.#telemetryService = telemetryService + this.#sendPinnedContext = sendPinnedContext } /** @@ -298,6 +305,7 @@ export class TabBarController { const { tabId } = await this.#features.chat.openTab({ newTabOptions: { data: { messages } } }) this.#chatHistoryDb.setHistoryIdMapping(tabId, selectedTab.historyId) this.#chatHistoryDb.updateTabOpenState(tabId, true) + this.#sendPinnedContext(tabId) } } @@ -312,9 +320,7 @@ export class TabBarController { const openConversations = this.#chatHistoryDb.getOpenTabs() if (openConversations) { for (const conversation of openConversations) { - if (conversation.conversations && conversation.conversations.length > 0) { - await this.restoreTab(conversation) - } + await this.restoreTab(conversation) } this.#telemetryService.emitLoadHistory({ amazonqTimeToLoadHistory: this.#chatHistoryDb.getLoadTime() ?? -1, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts index caf116f752..b8c5f3c6e8 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts @@ -6,13 +6,16 @@ import * as Loki from 'lokijs' import { chatMessageToMessage, Conversation, + DEFAULT_PINNED_CONTEXT, FileSystemAdapter, groupTabsByDate, Message, + Rules, Settings, SettingsCollection, Tab, TabCollection, + TabContext, TabType, calculateDatabaseSize, updateOrCreateConversation, @@ -20,7 +23,7 @@ import { import * as crypto from 'crypto' import * as path from 'path' import { Features } from '@aws/language-server-runtimes/server-interface/server' -import { ConversationItemGroup } from '@aws/language-server-runtimes/protocol' +import { ContextCommand, ConversationItemGroup } from '@aws/language-server-runtimes/protocol' import { ChatMessage, ToolResultStatus } from '@aws/codewhisperer-streaming-client' import { ChatItemType } from '@aws/mynah-ui' import { getUserHomeDir } from '@aws/lsp-core/out/util/path' @@ -181,6 +184,122 @@ export class ChatDatabase { } } + addTabWithContext(collection: Collection, historyId: string, tabContext: TabContext) { + collection.insert({ + tabType: 'cwc', + historyId, + title: 'Amazon Q Chat', + conversations: [], + isOpen: true, + updatedAt: new Date(), + tabContext, + }) + } + + getRules(tabId: string): Rules { + if (this.#initialized) { + const collection = this.#db.getCollection(TabCollection) + const historyId = this.#historyIdMapping.get(tabId) + if (historyId) { + const tab = collection.findOne({ historyId }) + return tab?.tabContext?.rules || { folders: {}, rules: {} } + } + } + return { folders: {}, rules: {} } + } + + getPinnedContext(tabId: string): ContextCommand[] { + if (this.#initialized) { + const collection = this.#db.getCollection(TabCollection) + const historyId = this.getOrCreateHistoryId(tabId) + if (historyId) { + const tab = collection.findOne({ historyId }) + return tab?.tabContext?.pinnedContext || DEFAULT_PINNED_CONTEXT + } + } + return [] + } + + setRules(tabId: string, rules: Rules) { + if (this.#initialized) { + const collection = this.#db.getCollection(TabCollection) + const historyId = this.getOrCreateHistoryId(tabId) + const tab = collection.findOne({ historyId }) + + this.#features.logging.log(`Updating rules: rules=${JSON.stringify(rules)}`) + + if (!tab) { + this.addTabWithContext(collection, historyId, { rules }) + } else { + if (!tab.tabContext) { + tab.tabContext = {} + } + tab.tabContext.rules = rules + collection.update(tab) + } + } + } + + addPinnedContext(tabId: string, context: ContextCommand) { + if (this.#initialized) { + const collection = this.#db.getCollection(TabCollection) + const historyId = this.getOrCreateHistoryId(tabId) + if (historyId) { + this.#features.logging.log( + `Adding pinned context: historyId=${historyId}, context=${JSON.stringify(context)}` + ) + const tab = collection.findOne({ historyId }) + if (!tab) { + this.addTabWithContext(collection, historyId, { + pinnedContext: DEFAULT_PINNED_CONTEXT.concat([context]), + }) + } else { + if (!tab.tabContext) { + tab.tabContext = {} + } + if (!tab.tabContext.pinnedContext) { + tab.tabContext.pinnedContext = DEFAULT_PINNED_CONTEXT + } + // Only add context item if its not already in this tab's pinned context + if (!tab.tabContext.pinnedContext.find(c => c.id === context.id)) { + // Active file pill should always be at the beginning of pinned context + if (DEFAULT_PINNED_CONTEXT.find(item => context.id === item.id)) { + tab.tabContext.pinnedContext.unshift(context) + } else { + tab.tabContext.pinnedContext.push(context) + } + } + collection.update(tab) + } + } + } + } + + removePinnedContext(tabId: string, context: ContextCommand) { + if (this.#initialized) { + const collection = this.#db.getCollection(TabCollection) + const historyId = this.getOrCreateHistoryId(tabId) + if (historyId) { + this.#features.logging.log( + `Removing pinned context: historyId=${historyId}, context=${JSON.stringify(context)}` + ) + const tab = collection.findOne({ historyId }) + if (!tab) { + this.addTabWithContext(collection, historyId, { pinnedContext: [] }) + } else { + if (!tab.tabContext) { + tab.tabContext = {} + } + if (!tab.tabContext.pinnedContext) { + tab.tabContext.pinnedContext = [] + } + tab.tabContext.pinnedContext = tab.tabContext.pinnedContext.filter(c => c.id !== context.id) + collection.update(tab) + } + } + } + } + getLoadTime() { return this.#loadTimeMs } @@ -336,6 +455,18 @@ export class ChatDatabase { } } + getOrCreateHistoryId(tabId: string) { + let historyId = this.#historyIdMapping.get(tabId) + + if (!historyId) { + historyId = crypto.randomUUID() + this.#features.logging.log(`Creating new historyId=${historyId} for tabId=${tabId}`) + this.setHistoryIdMapping(tabId, historyId) + } + + return historyId + } + /** * Adds a message to a conversation within a specified tab. * @@ -354,13 +485,7 @@ export class ChatDatabase { `Adding message to history: tabId=${tabId}, tabType=${tabType}, conversationId=${conversationId}` ) - let historyId = this.#historyIdMapping.get(tabId) - - if (!historyId) { - historyId = crypto.randomUUID() - this.#features.logging.log(`Creating new historyId=${historyId} for tabId=${tabId}`) - this.setHistoryIdMapping(tabId, historyId) - } + let historyId = this.getOrCreateHistoryId(tabId) const tabData = historyId ? tabCollection.findOne({ historyId }) : undefined const tabTitle = diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts index a26b746906..102f376faf 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts @@ -6,6 +6,7 @@ import * as path from 'path' import { ChatMessage, + ContextCommand, ConversationItem, ConversationItemGroup, IconType, @@ -21,6 +22,7 @@ import { AssistantResponseMessage, } from '@aws/codewhisperer-streaming-client' import { Workspace } from '@aws/language-server-runtimes/server-interface' +import { activeFileCmd } from '../../context/addtionalContextProvider' import { ChatItemType } from '@aws/mynah-ui' import { PriorityQueue } from 'typescript-collections' import { Features } from '@aws/language-server-runtimes/server-interface/server' @@ -55,6 +57,27 @@ export type Tab = { tabType: TabType title: string conversations: Conversation[] + tabContext?: TabContext +} + +export const DEFAULT_PINNED_CONTEXT: ContextCommand[] = [activeFileCmd] + +/** + * Stores context scoped to a conversation, such as pinned context and rules. + */ +export type TabContext = { + pinnedContext?: ContextCommand[] + rules?: Rules +} + +/** + * Stores active/inactive state of workspace rules. + */ +export type Rules = { + // Track folder states by folder name + folders: Record + // Track individual rule states by rule ID + rules: Record } export type Settings = { diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/chatController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/chatController.ts index ad3cdfbd4e..78e37db6b5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/chatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/chatController.ts @@ -76,6 +76,12 @@ type ChatHandlers = Omit< | 'chatOptionsUpdate' | 'onListMcpServers' | 'onMcpServerClick' + | 'onRuleClick' + | 'onListRules' + | 'sendPinnedContext' + | 'onActiveEditorChanged' + | 'onPinnedContextAdd' + | 'onPinnedContextRemove' > export class ChatController implements ChatHandlers { diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts index 1655d8f040..c68b47166a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts @@ -281,10 +281,15 @@ export class ChatTelemetryController { cwsprChatPromptContextCount: metric.cwsprChatPromptContextCount, cwsprChatFileContextLength: metric.cwsprChatFileContextLength, cwsprChatRuleContextLength: metric.cwsprChatRuleContextLength, + cwsprChatTotalRuleContextCount: metric.cwsprChatTotalRuleContextCount, cwsprChatPromptContextLength: metric.cwsprChatPromptContextLength, cwsprChatCodeContextLength: metric.cwsprChatCodeContextLength, cwsprChatCodeContextCount: metric.cwsprChatCodeContextCount, cwsprChatFocusFileContextLength: metric.cwsprChatFocusFileContextLength, + cwsprChatPinnedCodeContextCount: metric.cwsprChatPinnedCodeContextCount, + cwsprChatPinnedFileContextCount: metric.cwsprChatPinnedFileContextCount, + cwsprChatPinnedFolderContextCount: metric.cwsprChatPinnedFolderContextCount, + cwsprChatPinnedPromptContextCount: metric.cwsprChatPinnedPromptContextCount, languageServerVersion: metric.languageServerVersion, requestIds: metric.requestIds, } diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index 6a8b6bc185..7172ce077c 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -499,11 +499,16 @@ export class TelemetryService { cwsprChatFileContextLength: number cwsprChatRuleContextCount: number cwsprChatRuleContextLength: number + cwsprChatTotalRuleContextCount: number cwsprChatPromptContextCount: number cwsprChatPromptContextLength: number cwsprChatCodeContextCount: number cwsprChatCodeContextLength: number cwsprChatFocusFileContextLength: number + cwsprChatPinnedCodeContextCount?: number + cwsprChatPinnedFileContextCount?: number + cwsprChatPinnedFolderContextCount?: number + cwsprChatPinnedPromptContextCount?: number languageServerVersion?: string requestIds?: string[] }> @@ -544,10 +549,15 @@ export class TelemetryService { cwsprChatPromptContextCount: additionalParams.cwsprChatPromptContextCount, cwsprChatFileContextLength: additionalParams.cwsprChatFileContextLength, cwsprChatRuleContextLength: additionalParams.cwsprChatRuleContextLength, + cwsprChatTotalRuleContextCount: additionalParams.cwsprChatTotalRuleContextCount, cwsprChatPromptContextLength: additionalParams.cwsprChatPromptContextLength, cwsprChatFocusFileContextLength: additionalParams.cwsprChatFocusFileContextLength, cwsprChatCodeContextCount: additionalParams.cwsprChatCodeContextCount, cwsprChatCodeContextLength: additionalParams.cwsprChatCodeContextLength, + cwsprChatPinnedCodeContextCount: additionalParams.cwsprChatPinnedCodeContextCount, + cwsprChatPinnedFileContextCount: additionalParams.cwsprChatPinnedFileContextCount, + cwsprChatPinnedFolderContextCount: additionalParams.cwsprChatPinnedFolderContextCount, + cwsprChatPinnedPromptContextCount: additionalParams.cwsprChatPinnedPromptContextCount, result: params.result ?? 'Succeeded', enabled: params.agenticCodingMode, languageServerVersion: additionalParams.languageServerVersion, diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts index 29dd7bcdae..c0af49ecb6 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts @@ -303,11 +303,18 @@ export type AddMessageEvent = { cwsprChatFileContextLength?: number cwsprChatRuleContextCount?: number cwsprChatRuleContextLength?: number + cwsprChatTotalRuleContextCount?: number cwsprChatPromptContextCount?: number cwsprChatPromptContextLength?: number cwsprChatFocusFileContextLength?: number cwsprChatCodeContextCount?: number cwsprChatCodeContextLength?: number + + //pinned context metrics + cwsprChatPinnedCodeContextCount?: number + cwsprChatPinnedFileContextCount?: number + cwsprChatPinnedFolderContextCount?: number + cwsprChatPinnedPromptContextCount?: number } // Agentic MCP Telemetry From 8b2886905649e5440f654db86f24e0b7059b673f Mon Sep 17 00:00:00 2001 From: Avi Alpert Date: Mon, 16 Jun 2025 11:15:58 -0400 Subject: [PATCH 2/4] test: unit test fixes --- chat-client/src/client/chat.test.ts | 2 +- .../agenticChat/agenticChatController.test.ts | 9 ++++++++- .../context/additionalContextProvider.test.ts | 19 ++++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/chat-client/src/client/chat.test.ts b/chat-client/src/client/chat.test.ts index a475104f59..32ba1fb725 100644 --- a/chat-client/src/client/chat.test.ts +++ b/chat-client/src/client/chat.test.ts @@ -82,7 +82,7 @@ describe('Chat', () => { assert.calledWithExactly(clientApi.postMessage.thirdCall, { command: TAB_ADD_NOTIFICATION_METHOD, - params: { tabId: initialTabId }, + params: { tabId: initialTabId, restoredTab: undefined }, }) assert.calledWithExactly(clientApi.postMessage.lastCall, { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 5cd0cc245b..acee2db77d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -26,6 +26,7 @@ import { TextDocumentEdit, InlineChatResult, CancellationTokenSource, + ContextCommand, } from '@aws/language-server-runtimes/server-interface' import { TestFeatures } from '@aws/language-server-runtimes/testing' import * as assert from 'assert' @@ -236,7 +237,13 @@ describe('AgenticChatController', () => { } additionalContextProviderStub = sinon.stub(AdditionalContextProvider.prototype, 'getAdditionalContext') - additionalContextProviderStub.resolves([]) + additionalContextProviderStub.callsFake(async (triggerContext, _, context: ContextCommand[]) => { + // When @workspace is in the context, set hasWorkspace flag + if (context && context.some(item => item.command === '@workspace')) { + triggerContext.hasWorkspace = true + } + return [] + }) // @ts-ignore const cachedInitializeParams: InitializeParams = { initializationOptions: { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts index a4a9bfae55..ac2879bf34 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts @@ -42,6 +42,7 @@ describe('AdditionalContextProvider', () => { setRules: sinon.stub(), addPinnedContext: sinon.stub(), removePinnedContext: sinon.stub(), + getPinnedContext: sinon.stub().returns([]), } as unknown as ChatDatabase provider = new AdditionalContextProvider(testFeatures, chatHistoryDb) @@ -82,8 +83,10 @@ describe('AdditionalContextProvider', () => { workspaceRulesCount: 0, } - fsExistsStub.resolves(true) - fsReadDirStub.resolves([{ name: 'rule1.md', isFile: () => true }]) + fsExistsStub.callsFake((path: string) => + Promise.resolve(!path.includes('README') && !path.includes('AmazonQ')) + ) + fsReadDirStub.resolves([{ name: 'rule1.md', isFile: () => true, isDirectory: () => false }]) getContextCommandPromptStub.resolves([ { @@ -220,10 +223,12 @@ describe('AdditionalContextProvider', () => { // Mock workspace folders sinon.stub(workspaceUtils, 'getWorkspaceFolderPaths').returns(['/workspace']) - fsExistsStub.resolves(true) + fsExistsStub.callsFake((path: string) => + Promise.resolve(!path.includes('README') && !path.includes('AmazonQ')) + ) fsReadDirStub.resolves([ - { name: 'rule1.md', isFile: () => true }, - { name: 'rule2.md', isFile: () => true }, + { name: 'rule1.md', isFile: () => true, isDirectory: () => false }, + { name: 'rule2.md', isFile: () => true, isDirectory: () => false }, ]) const result = await provider.collectWorkspaceRules() @@ -233,13 +238,13 @@ describe('AdditionalContextProvider', () => { workspaceFolder: '/workspace', type: 'file', relativePath: path.join('.amazonq', 'rules', 'rule1.md'), - id: '', + id: path.join(path.join('/workspace', '.amazonq', 'rules', 'rule1.md')), }, { workspaceFolder: '/workspace', type: 'file', relativePath: path.join('.amazonq', 'rules', 'rule2.md'), - id: '', + id: path.join(path.join('/workspace', '.amazonq', 'rules', 'rule2.md')), }, ]) }) From dbd5b65e756f69a3ff88a27e2d6009880ff7b7e3 Mon Sep 17 00:00:00 2001 From: Avi Alpert Date: Mon, 16 Jun 2025 14:57:22 -0400 Subject: [PATCH 3/4] chore: update mynah-ui and language-server-runtimes to latest --- app/aws-lsp-antlr4-runtimes/package.json | 2 +- .../package.json | 2 +- app/aws-lsp-identity-runtimes/package.json | 2 +- app/aws-lsp-json-runtimes/package.json | 2 +- .../package.json | 2 +- app/aws-lsp-yaml-json-webworker/package.json | 2 +- app/aws-lsp-yaml-runtimes/package.json | 2 +- app/hello-world-lsp-runtimes/package.json | 2 +- chat-client/package.json | 4 +- client/vscode/package.json | 2 +- package-lock.json | 63 +++++++++---------- package.json | 2 +- server/aws-lsp-antlr4/package.json | 2 +- server/aws-lsp-codewhisperer/package.json | 2 +- server/aws-lsp-identity/package.json | 2 +- server/aws-lsp-json/package.json | 2 +- server/aws-lsp-notification/package.json | 2 +- server/aws-lsp-partiql/package.json | 2 +- server/aws-lsp-yaml/package.json | 2 +- server/device-sso-auth-lsp/package.json | 2 +- server/hello-world-lsp/package.json | 2 +- 21 files changed, 52 insertions(+), 53 deletions(-) diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json index 7047f279dc..28bae2c257 100644 --- a/app/aws-lsp-antlr4-runtimes/package.json +++ b/app/aws-lsp-antlr4-runtimes/package.json @@ -12,7 +12,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index 99cf0cc5bf..1795eb0aa3 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -15,7 +15,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/app/aws-lsp-identity-runtimes/package.json b/app/aws-lsp-identity-runtimes/package.json index e29a041ea1..04ecf1708f 100644 --- a/app/aws-lsp-identity-runtimes/package.json +++ b/app/aws-lsp-identity-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-identity": "^0.0.1" } } diff --git a/app/aws-lsp-json-runtimes/package.json b/app/aws-lsp-json-runtimes/package.json index 68a851e40e..c41b638520 100644 --- a/app/aws-lsp-json-runtimes/package.json +++ b/app/aws-lsp-json-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-json": "*" }, "devDependencies": { diff --git a/app/aws-lsp-notification-runtimes/package.json b/app/aws-lsp-notification-runtimes/package.json index 717b6ea61e..ca00f92bf0 100644 --- a/app/aws-lsp-notification-runtimes/package.json +++ b/app/aws-lsp-notification-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-notification": "^0.0.1" } } diff --git a/app/aws-lsp-yaml-json-webworker/package.json b/app/aws-lsp-yaml-json-webworker/package.json index e4b59139c5..e4f50f4f07 100644 --- a/app/aws-lsp-yaml-json-webworker/package.json +++ b/app/aws-lsp-yaml-json-webworker/package.json @@ -11,7 +11,7 @@ "serve:webpack": "NODE_ENV=development webpack serve" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, diff --git a/app/aws-lsp-yaml-runtimes/package.json b/app/aws-lsp-yaml-runtimes/package.json index ba68af8220..8cade38f94 100644 --- a/app/aws-lsp-yaml-runtimes/package.json +++ b/app/aws-lsp-yaml-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-yaml": "*" }, "devDependencies": { diff --git a/app/hello-world-lsp-runtimes/package.json b/app/hello-world-lsp-runtimes/package.json index 64b7033d0a..0e1b178148 100644 --- a/app/hello-world-lsp-runtimes/package.json +++ b/app/hello-world-lsp-runtimes/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.96" + "@aws/language-server-runtimes": "^0.2.97" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/chat-client/package.json b/chat-client/package.json index 23b18d6897..67b049d77f 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes-types": "^0.1.34", - "@aws/mynah-ui": "^4.35.3" + "@aws/language-server-runtimes-types": "^0.1.39", + "@aws/mynah-ui": "^4.35.4" }, "devDependencies": { "@types/jsdom": "^21.1.6", diff --git a/client/vscode/package.json b/client/vscode/package.json index 4762b2af59..7701664f2f 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -347,7 +347,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", diff --git a/package-lock.json b/package-lock.json index d51474dc35..b033e5b60b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "server/**" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@smithy/types": "4.2.0", "typescript": "^5.8.2" }, @@ -43,7 +43,7 @@ "name": "@aws/lsp-antlr4-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" @@ -80,7 +80,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -113,7 +113,7 @@ "name": "@aws/lsp-identity-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-identity": "^0.0.1" } }, @@ -121,7 +121,7 @@ "name": "@aws/lsp-json-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-json": "*" }, "devDependencies": { @@ -141,7 +141,7 @@ "name": "@aws/lsp-notification-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-notification": "^0.0.1" } }, @@ -185,7 +185,7 @@ "name": "@aws/lsp-yaml-json-webworker", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, @@ -205,7 +205,7 @@ "name": "@aws/lsp-yaml-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-yaml": "*" }, "devDependencies": { @@ -227,7 +227,7 @@ "version": "0.0.1", "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.96" + "@aws/language-server-runtimes": "^0.2.97" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -248,8 +248,8 @@ "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes-types": "^0.1.34", - "@aws/mynah-ui": "^4.35.3" + "@aws/language-server-runtimes-types": "^0.1.39", + "@aws/mynah-ui": "^4.35.4" }, "devDependencies": { "@types/jsdom": "^21.1.6", @@ -271,7 +271,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", @@ -3917,11 +3917,11 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.96", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.96.tgz", - "integrity": "sha512-xoycDvjBYevaBCNwHlFsE79myM/GTNN/wbCZRO9GUHHyI52T/lDAa3rq8XrFzstl53VxXNAnJiOD271Q5nlORQ==", + "version": "0.2.97", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.97.tgz", + "integrity": "sha512-Wzt09iC5YTVRJmmW6DwunBFSR0mV+cHjDwJ5iic1sEvXlI9CnrxlEjfn09crkVQ2XZj3dNJHoLQPptH+AEQfNg==", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.38", + "@aws/language-server-runtimes-types": "^0.1.39", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", "@opentelemetry/core": "^2.0.0", @@ -3948,9 +3948,9 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.38", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.38.tgz", - "integrity": "sha512-PH9wC1rsvfQU2YleghYxKutNqWn4GRAddG5cRgjRvj16FQQ06nlGINxfGhe124t86eiSn7qth+UGV9hR/Xij7Q==", + "version": "0.1.39", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.39.tgz", + "integrity": "sha512-HjZ9tYcs++vcSyNwCcGLC8k1nvdWTD7XRa6sI71OYwFzJvyMa4/BY7Womq/kmyuD/IB6MRVvuRdgYQxuU1mSGA==", "dependencies": { "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5" @@ -4121,11 +4121,10 @@ "link": true }, "node_modules/@aws/mynah-ui": { - "version": "4.35.3", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.35.3.tgz", - "integrity": "sha512-BCp3MgjoCGL7NkUH3ush+xUlXtsXMlon7FlWT/s7W3Fm44Eh3ylrKx7c3Vzg2X+jbaWf0Jii4OGfomO+0NcRDQ==", + "version": "4.35.4", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.35.4.tgz", + "integrity": "sha512-LuOexbuMSKYCl/Qa7zj9d4/ueTLK3ltoYHeA0I7gOpPC/vYACxqjVqX6HPhNCE+L5zBKNMN2Z+FUaox+fYhvAQ==", "hasInstallScript": true, - "license": "Apache License 2.0", "dependencies": { "escape-html": "^1.0.3", "highlight.js": "^11.11.0", @@ -27224,7 +27223,7 @@ "version": "0.1.11", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9" }, "devDependencies": { @@ -27297,7 +27296,7 @@ "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.40", "@aws/codewhisperer-streaming-client": "^1.0.1", - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9", "@modelcontextprotocol/sdk": "^1.9.0", "@smithy/node-http-handler": "^2.5.0", @@ -27439,7 +27438,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -27486,7 +27485,7 @@ "version": "0.1.11", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -27503,7 +27502,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "0.0.9", "vscode-languageserver": "^9.0.1" }, @@ -27546,7 +27545,7 @@ "version": "0.0.12", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" @@ -27579,7 +27578,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", @@ -27593,7 +27592,7 @@ "name": "@amzn/device-sso-auth-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -27604,7 +27603,7 @@ "name": "@aws/hello-world-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/package.json b/package.json index 8957343d7f..80ef3957c6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "package": "npm run compile && npm run package --workspaces --if-present" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@smithy/types": "4.2.0", "typescript": "^5.8.2" }, diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index 6cc3e3dd35..68d4e109c9 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -28,7 +28,7 @@ "clean": "rm -rf node_modules" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9" }, "peerDependencies": { diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 01ce590462..3be95a6462 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -35,7 +35,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/codewhisperer-streaming-client": "^1.0.1", "@aws/lsp-core": "^0.0.9", "@modelcontextprotocol/sdk": "^1.9.0", diff --git a/server/aws-lsp-identity/package.json b/server/aws-lsp-identity/package.json index 0f3c5911a8..2af69d6201 100644 --- a/server/aws-lsp-identity/package.json +++ b/server/aws-lsp-identity/package.json @@ -26,7 +26,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index 0d99f997b8..40c876e330 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -26,7 +26,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-notification/package.json b/server/aws-lsp-notification/package.json index 5b58be2ca2..f3e759bcc2 100644 --- a/server/aws-lsp-notification/package.json +++ b/server/aws-lsp-notification/package.json @@ -22,7 +22,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "0.0.9", "vscode-languageserver": "^9.0.1" }, diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index bcac7b5b8e..1d38e31cbc 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -24,7 +24,7 @@ "out" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index 4ea529bb25..200d9ecd57 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -26,7 +26,7 @@ "postinstall": "node patchYamlPackage.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "@aws/lsp-core": "^0.0.9", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", diff --git a/server/device-sso-auth-lsp/package.json b/server/device-sso-auth-lsp/package.json index 0b29aeec8e..1195d2fc74 100644 --- a/server/device-sso-auth-lsp/package.json +++ b/server/device-sso-auth-lsp/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/hello-world-lsp/package.json b/server/hello-world-lsp/package.json index 1be4e6ec17..d72757ef88 100644 --- a/server/hello-world-lsp/package.json +++ b/server/hello-world-lsp/package.json @@ -13,7 +13,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.96", + "@aws/language-server-runtimes": "^0.2.97", "vscode-languageserver": "^9.0.1" }, "devDependencies": { From d17cb0772655d529c26cd6f3aa37663fa3a33ac7 Mon Sep 17 00:00:00 2001 From: Avi Alpert Date: Mon, 16 Jun 2025 15:30:43 -0400 Subject: [PATCH 4/4] test: fix unit tests --- .../agenticChat/tabBarController.test.ts | 16 ---------------- .../shared/telemetry/telemetryService.test.ts | 5 +++++ 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts index 0fd456ff8f..fecbdebe13 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tabBarController.test.ts @@ -559,22 +559,6 @@ describe('TabBarController', () => { result: 'Succeeded', }) }) - - it('should not restore tabs with empty conversations', async () => { - const mockTabs = [ - { historyId: 'history1', conversations: [] }, - { historyId: 'history2', conversations: [{ messages: [] }] }, - ] as unknown as Tab[] - - ;(chatHistoryDb.getOpenTabs as sinon.SinonStub).returns(mockTabs) - - const restoreTabStub = sinon.stub(tabBarController, 'restoreTab') - - await tabBarController.loadChats() - - sinon.assert.calledOnce(restoreTabStub) - sinon.assert.calledWith(restoreTabStub, mockTabs[1]) - }) }) }) diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts index 10589c908f..5fd874672e 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts @@ -841,9 +841,14 @@ describe('TelemetryService', () => { cwsprChatCodeContextCount: 2, cwsprChatFileContextLength: 0, cwsprChatRuleContextLength: 0, + cwsprChatTotalRuleContextCount: undefined, cwsprChatPromptContextLength: 0, cwsprChatCodeContextLength: 500, cwsprChatFocusFileContextLength: 0, + cwsprChatPinnedCodeContextCount: undefined, + cwsprChatPinnedFileContextCount: undefined, + cwsprChatPinnedFolderContextCount: undefined, + cwsprChatPinnedPromptContextCount: undefined, }, }) })