Repository: aws/language-servers
Pull request #948: feat(amazonq): support context commands in agentic chat
Author: login: zixlin7
Created: 2025-04-10T19:07:48Z | Merged: 2025-04-18T03:06:02Z

## Problem
We need to support files/folders/code symbols/prompts as context command in agentic chat, and also show user the context referenced in chat response
## Solution
<img width="656" alt="Screenshot 2025-04-17 at 9 46 32 AM" src="https://github.com/user-attachments/assets/ca4c5190-666f-4baa-b843-11b1810babab" />

note telemetry update will be followup in next PR

related VS Code change: https://github.com/aws/aws-toolkit-vscode/pull/6999

<!---
    REMINDER:
    - Read CONTRIBUTING.md first.
    - Add test coverage for your changes.
    - Link to related issues/commits.
    - Testing: how did you test your changes?
    - Screenshots if applicable
-->

## License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Diff:
diff --git a/package-lock.json b/package-lock.json
index c6841ac57c..36ff7eeaf1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8486,18 +8486,6 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/@wdio/cli/node_modules/readdirp": {
-            "version": "4.1.2",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 14.18.0"
-            },
-            "funding": {
-                "type": "individual",
-                "url": "https://paulmillr.com/funding/"
-            }
-        },
         "node_modules/@wdio/cli/node_modules/strip-final-newline": {
             "version": "4.0.0",
             "dev": true,
@@ -10698,18 +10686,13 @@
         },
         "node_modules/chokidar": {
             "version": "3.6.0",
+            "dev": true,
             "license": "MIT",
             "dependencies": {
-                "anymatch": "~3.1.2",
-                "braces": "~3.0.2",
-                "glob-parent": "~5.1.2",
-                "is-binary-path": "~2.1.0",
-                "is-glob": "~4.0.1",
-                "normalize-path": "~3.0.0",
-                "readdirp": "~3.6.0"
+                "readdirp": "^4.0.1"
             },
             "engines": {
-                "node": ">= 8.10.0"
+                "node": ">= 14.16.0"
             },
             "funding": {
                 "url": "https://paulmillr.com/funding/"
@@ -10718,16 +10701,6 @@
                 "fsevents": "~2.3.2"
             }
         },
-        "node_modules/chokidar/node_modules/glob-parent": {
-            "version": "5.1.2",
-            "license": "ISC",
-            "dependencies": {
-                "is-glob": "^4.0.1"
-            },
-            "engines": {
-                "node": ">= 6"
-            }
-        },
         "node_modules/chrome-trace-event": {
             "version": "1.0.4",
             "devOptional": true,
@@ -17028,6 +17001,31 @@
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
             }
         },
+        "node_modules/mocha/node_modules/chokidar": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+            "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            },
+            "engines": {
+                "node": ">= 8.10.0"
+            },
+            "funding": {
+                "url": "https://paulmillr.com/funding/"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            }
+        },
         "node_modules/mocha/node_modules/find-up": {
             "version": "5.0.0",
             "dev": true,
@@ -17043,6 +17041,19 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/mocha/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/mocha/node_modules/locate-path": {
             "version": "6.0.0",
             "dev": true,
@@ -17090,6 +17101,19 @@
                 "node": ">=8"
             }
         },
+        "node_modules/mocha/node_modules/readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "picomatch": "^2.2.1"
+            },
+            "engines": {
+                "node": ">=8.10.0"
+            }
+        },
         "node_modules/mocha/node_modules/supports-color": {
             "version": "8.1.1",
             "dev": true,
@@ -18817,13 +18841,16 @@
             }
         },
         "node_modules/readdirp": {
-            "version": "3.6.0",
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+            "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
             "license": "MIT",
-            "dependencies": {
-                "picomatch": "^2.2.1"
-            },
             "engines": {
-                "node": ">=8.10.0"
+                "node": ">= 14.18.0"
+            },
+            "funding": {
+                "type": "individual",
+                "url": "https://paulmillr.com/funding/"
             }
         },
         "node_modules/real-require": {
@@ -22076,6 +22103,54 @@
                 }
             }
         },
+        "node_modules/webpack-dev-server/node_modules/chokidar": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+            "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+            "license": "MIT",
+            "dependencies": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            },
+            "engines": {
+                "node": ">= 8.10.0"
+            },
+            "funding": {
+                "url": "https://paulmillr.com/funding/"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            }
+        },
+        "node_modules/webpack-dev-server/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "license": "ISC",
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/webpack-dev-server/node_modules/readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "license": "MIT",
+            "dependencies": {
+                "picomatch": "^2.2.1"
+            },
+            "engines": {
+                "node": ">=8.10.0"
+            }
+        },
         "node_modules/webpack-merge": {
             "version": "6.0.1",
             "dev": true,
@@ -22810,6 +22885,7 @@
                 "adm-zip": "^0.5.10",
                 "archiver": "^7.0.1",
                 "aws-sdk": "^2.1403.0",
+                "chokidar": "^4.0.3",
                 "deepmerge": "^4.3.1",
                 "diff": "^7.0.0",
                 "fastest-levenshtein": "^1.0.16",
@@ -22914,6 +22990,21 @@
                 "node": ">=14.0.0"
             }
         },
+        "server/aws-lsp-codewhisperer/node_modules/chokidar": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+            "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+            "license": "MIT",
+            "dependencies": {
+                "readdirp": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 14.16.0"
+            },
+            "funding": {
+                "url": "https://paulmillr.com/funding/"
+            }
+        },
         "server/aws-lsp-codewhisperer/node_modules/diff": {
             "version": "7.0.0",
             "license": "BSD-3-Clause",
diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json
index bc4ee4a427..a75db9287b 100644
--- a/server/aws-lsp-codewhisperer/package.json
+++ b/server/aws-lsp-codewhisperer/package.json
@@ -38,6 +38,7 @@
         "adm-zip": "^0.5.10",
         "archiver": "^7.0.1",
         "aws-sdk": "^2.1403.0",
+        "chokidar": "^4.0.3",
         "deepmerge": "^4.3.1",
         "diff": "^7.0.0",
         "fastest-levenshtein": "^1.0.16",
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 fdd7b4396e..d817798561 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
@@ -3,6 +3,7 @@
  * Will be deleted or merged.
  */
 
+import * as path from 'path'
 import {
     ChatResponseStream,
     CodeWhispererStreaming,
@@ -36,6 +37,8 @@ import { DEFAULT_HELP_FOLLOW_UP_PROMPT, HELP_MESSAGE } from '../chat/constants'
 import { TelemetryService } from '../../shared/telemetry/telemetryService'
 import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager'
 import { TabBarController } from './tabBarController'
+import { getUserPromptsDirectory } from './context/contextUtils'
+import { AdditionalContextProvider } from './context/addtionalContextProvider'
 
 describe('AgenticChatController', () => {
     const mockTabId = 'tab-1'
@@ -97,11 +100,13 @@ describe('AgenticChatController', () => {
 
     let sendMessageStub: sinon.SinonStub
     let generateAssistantResponseStub: sinon.SinonStub
+    let additionalContextProviderStub: sinon.SinonStub
     let disposeStub: sinon.SinonStub
     let activeTabSpy: {
         get: sinon.SinonSpy<[], string | undefined>
         set: sinon.SinonSpy<[string | undefined], void>
     }
+    let fsWriteFileStub: sinon.SinonStub
     let removeConversationSpy: sinon.SinonSpy
     let emitConversationMetricStub: sinon.SinonStub
 
@@ -144,13 +149,14 @@ describe('AgenticChatController', () => {
             })
 
         testFeatures = new TestFeatures()
+        fsWriteFileStub = sinon.stub()
 
         testFeatures.workspace.fs = {
             ...testFeatures.workspace.fs,
             getServerDataDirPath: sinon.stub().returns('/mock/server/data/path'),
             mkdir: sinon.stub().resolves(),
             readFile: sinon.stub().resolves(),
-            writeFile: sinon.stub().resolves(),
+            writeFile: fsWriteFileStub.resolves(),
             rm: sinon.stub().resolves(),
         }
 
@@ -161,6 +167,8 @@ describe('AgenticChatController', () => {
             addTool: sinon.stub().resolves(),
         }
 
+        additionalContextProviderStub = sinon.stub(AdditionalContextProvider.prototype, 'getAdditionalContext')
+        additionalContextProviderStub.resolves([])
         // @ts-ignore
         const cachedInitializeParams: InitializeParams = {
             initializationOptions: {
@@ -173,6 +181,7 @@ describe('AgenticChatController', () => {
                 },
             },
         }
+        testFeatures.lsp.window.showDocument = sinon.stub()
         testFeatures.lsp.getClientInitializeParams.returns(cachedInitializeParams)
         setCredentials('builderId')
 
@@ -1001,6 +1010,25 @@ describe('AgenticChatController', () => {
         })
     })
 
+    describe('onCreatePrompt', () => {
+        it('should create prompt file with given name', async () => {
+            const promptName = 'testPrompt'
+            const expectedPath = path.join(getUserPromptsDirectory(), 'testPrompt.prompt.md')
+
+            await chatController.onCreatePrompt({ promptName })
+
+            sinon.assert.calledOnceWithExactly(fsWriteFileStub, expectedPath, '', { mode: 0o600 })
+        })
+
+        it('should create default prompt file when no name provided', async () => {
+            const expectedPath = path.join(getUserPromptsDirectory(), 'default.prompt.md')
+
+            await chatController.onCreatePrompt({ promptName: '' })
+
+            sinon.assert.calledOnceWithExactly(fsWriteFileStub, expectedPath, '', { mode: 0o600 })
+        })
+    })
+
     describe('onInlineChatPrompt', () => {
         it('read all the response streams and return compiled results', async () => {
             const chatResultPromise = chatController.onInlineChatPrompt(
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 677c03b695..f7139be70e 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -7,7 +7,9 @@ import {
     ChatTriggerType,
     GenerateAssistantResponseCommandInput,
     GenerateAssistantResponseCommandOutput,
+    SendMessageCommandInput,
     SendMessageCommandInput as SendMessageCommandInputCodeWhispererStreaming,
+    SendMessageCommandOutput,
     ToolResult,
     ToolResultContentBlock,
     ToolUse,
@@ -24,10 +26,15 @@ import {
     ConversationClickParams,
     ListConversationsParams,
     TabBarActionParams,
+    CreatePromptParams,
+    FileClickParams,
+} from '@aws/language-server-runtimes/protocol'
+import {
     CancellationToken,
     Chat,
     ChatParams,
     ChatResult,
+    FileList,
     EndChatParams,
     LSPErrorCodes,
     QuickActionParams,
@@ -52,7 +59,6 @@ import { ChatTelemetryController } from '../chat/telemetry/chatTelemetryControll
 import { QuickAction } from '../chat/quickActions'
 import { Metric } from '../../shared/telemetry/metric'
 import { getErrorMessage, isAwsError, isNullish, isObject } from '../../shared/utils'
-import { QChatTriggerContext, TriggerContext } from '../chat/contexts/triggerContext'
 import { HELP_MESSAGE } from '../chat/constants'
 import { TelemetryService } from '../../shared/telemetry/telemetryService'
 import {
@@ -63,7 +69,6 @@ import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/A
 import { AmazonQWorkspaceConfig } from '../../shared/amazonQServiceManager/configurationUtils'
 import { TabBarController } from './tabBarController'
 import { ChatDatabase } from './tools/chatDb/chatDb'
-import { SendMessageCommandInput, SendMessageCommandOutput } from '../../shared/streamingClientService'
 import {
     AgenticChatEventParser,
     ChatResultWithMetadata as AgenticChatResultWithMetadata,
@@ -71,14 +76,22 @@ import {
 import { ChatSessionService } from '../chat/chatSessionService'
 import { AgenticChatResultStream } from './agenticChatResultStream'
 import { executeToolMessage, toolErrorMessage, toolResultMessage } from './textFormatting'
+import {
+    AdditionalContentEntryAddition,
+    AgenticChatTriggerContext,
+    TriggerContext,
+} from './context/agenticChatTriggerContext'
+import { AdditionalContextProvider } from './context/addtionalContextProvider'
+import { getNewPromptFilePath } from './context/contextUtils'
 
 type ChatHandlers = Omit<
     LspHandlers<Chat>,
     | 'openTab'
     | 'sendChatUpdate'
-    | 'onFileClicked'
     | 'sendContextCommands'
-    | 'onCreatePrompt'
+    | 'onListConversations'
+    | 'onConversationClick'
+    | 'onTabBarAction'
     | 'getSerializedChat'
     | 'chatOptionsUpdate'
 >
@@ -87,12 +100,13 @@ export class AgenticChatController implements ChatHandlers {
     #features: Features
     #chatSessionManagementService: ChatSessionManagementService
     #telemetryController: ChatTelemetryController
-    #triggerContext: QChatTriggerContext
+    #triggerContext: AgenticChatTriggerContext
     #customizationArn?: string
     #telemetryService: TelemetryService
     #amazonQServiceManager?: AmazonQTokenServiceManager
     #tabBarController: TabBarController
     #chatHistoryDb: ChatDatabase
+    #additionalContextProvider: AdditionalContextProvider
 
     constructor(
         chatSessionManagementService: ChatSessionManagementService,
@@ -102,12 +116,24 @@ export class AgenticChatController implements ChatHandlers {
     ) {
         this.#features = features
         this.#chatSessionManagementService = chatSessionManagementService
-        this.#triggerContext = new QChatTriggerContext(features.workspace, features.logging)
+        this.#triggerContext = new AgenticChatTriggerContext(features.workspace, features.logging)
         this.#telemetryController = new ChatTelemetryController(features, telemetryService)
         this.#telemetryService = telemetryService
         this.#amazonQServiceManager = amazonQServiceManager
         this.#chatHistoryDb = new ChatDatabase(features)
         this.#tabBarController = new TabBarController(features, this.#chatHistoryDb)
+        this.#additionalContextProvider = new AdditionalContextProvider(features.workspace)
+    }
+
+    async onCreatePrompt(params: CreatePromptParams): Promise<void> {
+        const newFilePath = getNewPromptFilePath(params.promptName)
+        const newFileContent = ''
+        try {
+            await this.#features.workspace.fs.writeFile(newFilePath, newFileContent, { mode: 0o600 })
+            await this.#features.lsp.window.showDocument({ uri: newFilePath })
+        } catch (e) {
+            this.#features.logging.warn(`Error creating prompt file: ${e}`)
+        }
     }
 
     dispose() {
@@ -166,8 +192,21 @@ export class AgenticChatController implements ChatHandlers {
         const conversationIdentifier = session?.conversationId ?? 'New conversation'
         const chatResultStream = this.#getChatResultStream(params.partialResultToken)
         try {
+            const additionalContext = await this.#additionalContextProvider.getAdditionalContext(
+                triggerContext,
+                (params.prompt as any).context
+            )
+            if (additionalContext.length) {
+                triggerContext.documentReference =
+                    this.#additionalContextProvider.getFileListFromContext(additionalContext)
+            }
             // Get the initial request input
-            const initialRequestInput = await this.#prepareRequestInput(params, session, triggerContext)
+            const initialRequestInput = await this.#prepareRequestInput(
+                params,
+                session,
+                triggerContext,
+                additionalContext
+            )
 
             // Start the agent loop
             const finalResult = await this.#runAgentLoop(
@@ -176,7 +215,8 @@ export class AgenticChatController implements ChatHandlers {
                 metric,
                 chatResultStream,
                 conversationIdentifier,
-                token
+                token,
+                triggerContext.documentReference
             )
 
             // Phase 5: Result Handling - This happens only once
@@ -200,7 +240,8 @@ export class AgenticChatController implements ChatHandlers {
     async #prepareRequestInput(
         params: ChatParams,
         session: ChatSessionService,
-        triggerContext: TriggerContext
+        triggerContext: TriggerContext,
+        additionalContext: AdditionalContentEntryAddition[]
     ): Promise<GenerateAssistantResponseCommandInput> {
         this.#debug('Preparing request input')
         const profileArn = AmazonQTokenServiceManager.getInstance(this.#features).getActiveProfileArn()
@@ -210,7 +251,8 @@ export class AgenticChatController implements ChatHandlers {
             ChatTriggerType.MANUAL,
             this.#customizationArn,
             profileArn,
-            this.#features.agent.getTools({ format: 'bedrock' })
+            this.#features.agent.getTools({ format: 'bedrock' }),
+            additionalContext
         )
 
         if (!session.localHistoryHydrated && requestInput.conversationState) {
@@ -230,7 +272,8 @@ export class AgenticChatController implements ChatHandlers {
         metric: Metric<CombinedConversationEvent>,
         chatResultStream: AgenticChatResultStream,
         conversationIdentifier?: string,
-        token?: CancellationToken
+        token?: CancellationToken,
+        documentReference?: FileList
     ): Promise<Result<AgenticChatResultWithMetadata, string>> {
         let currentRequestInput = { ...initialRequestInput }
         let finalResult: Result<AgenticChatResultWithMetadata, string> | null = null
@@ -260,7 +303,8 @@ export class AgenticChatController implements ChatHandlers {
                     cwsprChatResponseCode: response.$metadata.httpStatusCode,
                     cwsprChatMessageId: response.$metadata.requestId,
                 }),
-                chatResultStream
+                chatResultStream,
+                documentReference
             )
 
             // Store the conversation ID from the first response
@@ -670,6 +714,11 @@ export class AgenticChatController implements ChatHandlers {
         return success
     }
 
+    async onFileClicked(params: FileClickParams) {
+        // TODO: also pass in selection and handle on client side
+        await this.#features.lsp.window.showDocument({ uri: params.filePath })
+    }
+
     onFollowUpClicked() {}
 
     onInfoLinkClick() {}
@@ -811,13 +860,14 @@ export class AgenticChatController implements ChatHandlers {
     async #processGenerateAssistantResponseResponse(
         response: GenerateAssistantResponseCommandOutput,
         metric: Metric<AddMessageEvent>,
-        chatResultStream: AgenticChatResultStream
+        chatResultStream: AgenticChatResultStream,
+        contextList?: FileList
     ): Promise<Result<AgenticChatResultWithMetadata, string>> {
         const requestId = response.$metadata.requestId!
         const chatEventParser = new AgenticChatEventParser(requestId, metric)
         const streamWriter = chatResultStream.getResultStreamWriter()
         for await (const chatEvent of response.generateAssistantResponseResponse!) {
-            const result = chatEventParser.processPartialEvent(chatEvent)
+            const result = chatEventParser.processPartialEvent(chatEvent, contextList)
 
             // terminate early when there is an error
             if (!result.success) {
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts
index 9a3e5f7cde..2a2fe4898d 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts
@@ -34,6 +34,7 @@ describe('AgenticChatEventParser', () => {
                         codeReference: undefined,
                         followUp: undefined,
                         relatedContent: undefined,
+                        contextList: undefined,
                     },
                     conversationId: undefined,
                 },
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts
index 0f69531cc8..2f05e303d1 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts
@@ -7,6 +7,7 @@ import { ChatResponseStream, Reference, SupplementaryWebLink, ToolUse } from '@a
 import {
     ChatItemAction,
     ChatResult,
+    FileList,
     ReferenceTrackerInformation,
     SourceLink,
 } from '@aws/language-server-runtimes/protocol'
@@ -31,6 +32,7 @@ export class AgenticChatEventParser implements ChatResult {
     followUp?: { text?: string; options?: ChatItemAction[] }
     codeReference?: ReferenceTrackerInformation[]
     toolUses: Record<string, ToolUse & { stop: boolean }> = {}
+    contextList?: FileList = undefined
 
     conversationId?: string
 
@@ -78,7 +80,10 @@ export class AgenticChatEventParser implements ChatResult {
         return this.#totalEvents
     }
 
-    public processPartialEvent(chatEvent: ChatResponseStream): Result<ChatResultWithMetadata, string> {
+    public processPartialEvent(
+        chatEvent: ChatResponseStream,
+        contextList?: FileList
+    ): Result<ChatResultWithMetadata, string> {
         const {
             messageMetadataEvent,
             followupPromptEvent,
@@ -108,6 +113,9 @@ export class AgenticChatEventParser implements ChatResult {
         } else if (invalidStateEvent) {
             this.error = invalidStateEvent.message ?? invalidStateEvent.reason ?? 'Invalid state'
         } else if (assistantResponseEvent?.content) {
+            if (contextList?.filePaths?.length) {
+                this.contextList = contextList
+            }
             this.#totalEvents.assistantResponseEvent += 1
             this.body = (this.body ?? '') + assistantResponseEvent.content
         } else if (toolUseEvent) {
@@ -185,6 +193,7 @@ export class AgenticChatEventParser implements ChatResult {
             relatedContent: this.relatedContent,
             followUp: this.followUp,
             codeReference: this.codeReference,
+            ...(this.contextList && { contextList: { ...this.contextList } }),
         }
 
         const chatResultWithMetadata = {
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
new file mode 100644
index 0000000000..d3d2790c7a
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts
@@ -0,0 +1,221 @@
+import * as path from 'path'
+import * as sinon from 'sinon'
+import { URI } from 'vscode-uri'
+import { TestFeatures } from '@aws/language-server-runtimes/testing'
+import * as assert from 'assert'
+import { AdditionalContextPrompt } from 'local-indexing'
+import { AdditionalContextProvider } from './addtionalContextProvider'
+import { getUserPromptsDirectory } from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+describe('AdditionalContextProvider', () => {
+    let provider: AdditionalContextProvider
+    let testFeatures: TestFeatures
+    let fsExistsStub: sinon.SinonStub
+    let getContextCommandPromptStub: sinon.SinonStub
+    let fsReadDirStub: sinon.SinonStub
+    let localProjectContextControllerInstanceStub: sinon.SinonStub
+
+    beforeEach(() => {
+        testFeatures = new TestFeatures()
+        fsExistsStub = sinon.stub()
+        fsReadDirStub = sinon.stub()
+        testFeatures.workspace.fs.exists = fsExistsStub
+        testFeatures.workspace.fs.readdir = fsReadDirStub
+        getContextCommandPromptStub = sinon.stub()
+        provider = new AdditionalContextProvider(testFeatures.workspace)
+        localProjectContextControllerInstanceStub = sinon.stub(LocalProjectContextController, 'getInstance').returns({
+            getContextCommandPrompt: getContextCommandPromptStub,
+        } as unknown as LocalProjectContextController)
+    })
+
+    afterEach(() => {
+        sinon.restore()
+    })
+
+    describe('getAdditionalContext', () => {
+        it('should return empty array when no additional context commands', async () => {
+            const triggerContext = {
+                workspaceFolder: null,
+                context: [],
+                workspaceRulesCount: 0,
+            }
+
+            fsExistsStub.resolves(false)
+            getContextCommandPromptStub.resolves([])
+
+            const result = await provider.getAdditionalContext(triggerContext)
+
+            assert.deepStrictEqual(result, [])
+        })
+
+        it('should process workspace rules and context correctly', async () => {
+            const mockWorkspaceFolder = {
+                uri: URI.file('/workspace').toString(),
+                name: 'test',
+            }
+            const triggerContext = {
+                workspaceFolder: mockWorkspaceFolder,
+                context: [],
+                workspaceRulesCount: 0,
+            }
+
+            fsExistsStub.resolves(true)
+            fsReadDirStub.resolves([{ name: 'rule1.prompt.md', isFile: () => true }])
+
+            getContextCommandPromptStub.resolves([
+                {
+                    name: 'Test Rule',
+                    description: 'Test Description',
+                    content: 'Test Content',
+                    filePath: '/workspace/.amazonq/rules/rule1.prompt.md',
+                    relativePath: '.amazonq/rules/rule1.prompt.md',
+                    startLine: 1,
+                    endLine: 10,
+                },
+            ])
+
+            const result = await provider.getAdditionalContext(triggerContext)
+
+            assert.strictEqual(result.length, 1)
+            assert.strictEqual(result[0].name, 'Test Rule')
+            assert.strictEqual(result[0].type, 'rule')
+        })
+    })
+
+    describe('getFileListFromContext', () => {
+        it('should create correct file list for symbol entries', () => {
+            const mockContext = [
+                {
+                    relativePath: 'test/path.ts',
+                    name: 'symbol',
+                    startLine: 1,
+                    endLine: 10,
+                    type: 'code',
+                    description: 'test',
+                    innerContext: 'test',
+                },
+            ]
+
+            const result = provider.getFileListFromContext(mockContext)
+            const fileDetail = result.details ? result.details['test/path.ts'] : undefined
+            const lineRange = fileDetail?.lineRanges ? fileDetail.lineRanges[0] : undefined
+
+            assert.deepStrictEqual(result.filePaths, ['test/path.ts'])
+            assert.deepStrictEqual(lineRange, {
+                first: 1,
+                second: 10,
+            })
+        })
+
+        it('should handle non-symbol entries with -1 line ranges', () => {
+            const mockContext = [
+                {
+                    relativePath: 'test/path.ts',
+                    name: 'not-symbol',
+                    startLine: 1,
+                    endLine: 10,
+                    type: 'file',
+                    description: 'test',
+                    innerContext: 'test',
+                },
+            ]
+
+            const result = provider.getFileListFromContext(mockContext)
+            const fileDetail = result.details ? result.details['test/path.ts'] : undefined
+            const lineRange = fileDetail?.lineRanges ? fileDetail.lineRanges[0] : undefined
+
+            assert.deepStrictEqual(lineRange, {
+                first: -1,
+                second: -1,
+            })
+        })
+    })
+
+    describe('getContextType', () => {
+        const mockPrompt: AdditionalContextPrompt = {
+            filePath: path.join('/workspace', '.amazonq', 'rules', 'test.prompt.md'),
+            relativePath: path.join('.amazonq', 'rules', 'test.prompt.md'),
+            content: 'Sample content',
+            name: 'Test Rule',
+            description: 'Test Description',
+            startLine: 1,
+            endLine: 10,
+        }
+        it('should identify rule type for files in .amazonq/rules', () => {
+            const result = provider.getContextType(mockPrompt)
+
+            assert.strictEqual(result, 'rule')
+        })
+
+        it('should identify prompt type for files in user prompts directory', () => {
+            const userPromptsDir = getUserPromptsDirectory()
+            const mockPrompt = {
+                filePath: path.join(userPromptsDir, 'test.prompt.md'),
+                relativePath: 'test.prompt.md',
+                content: 'Sample content',
+                name: 'Test Prompt',
+                description: 'Test Description',
+                startLine: 1,
+                endLine: 10,
+            }
+
+            const result = provider.getContextType(mockPrompt)
+
+            assert.strictEqual(result, 'prompt')
+        })
+
+        it('should return file type for non-prompt files', () => {
+            const mockPrompt = {
+                filePath: 'test.ts',
+                relativePath: 'test.ts',
+                content: 'Sample content',
+                name: 'Test File',
+                description: 'Test Description',
+                startLine: 1,
+                endLine: 10,
+            }
+
+            const result = provider.getContextType(mockPrompt)
+
+            assert.strictEqual(result, 'file')
+        })
+    })
+
+    describe('collectWorkspaceRules', () => {
+        it('should return empty array when no workspace folder', async () => {
+            const triggerContext = {
+                relativeFilePath: 'test.ts',
+                workspaceFolder: null,
+            }
+
+            const result = await provider.collectWorkspaceRules(triggerContext)
+
+            assert.deepStrictEqual(result, [])
+        })
+
+        it('should return rules files when they exist', async () => {
+            const mockWorkspaceFolder = {
+                uri: URI.file('/workspace').toString(),
+                name: 'test',
+            }
+            const triggerContext = {
+                relativeFilePath: 'test.ts',
+                workspaceFolder: mockWorkspaceFolder,
+            }
+
+            fsExistsStub.resolves(true)
+            fsReadDirStub.resolves([
+                { name: 'rule1.prompt.md', isFile: () => true },
+                { name: 'rule2.prompt.md', isFile: () => true },
+            ])
+
+            const result = await provider.collectWorkspaceRules(triggerContext)
+
+            assert.deepStrictEqual(result, [
+                path.join('/workspace', '.amazonq', 'rules', 'rule1.prompt.md'),
+                path.join('/workspace', '.amazonq', 'rules', 'rule2.prompt.md'),
+            ])
+        })
+    })
+})
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
new file mode 100644
index 0000000000..574fc7786b
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
@@ -0,0 +1,155 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri ? URI.parse(folder.uri).fsPath : process.cwd()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
+                return 'rule'
+            } else if (pathUtils.isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
+                return 'prompt'
+            }
+        }
+        return 'file'
+    }
+
+    async getAdditionalContext(
+        triggerContext: TriggerContext,
+        context?: any[]
+    ): Promise<AdditionalContentEntryAddition[]> {
+        const additionalContextCommands: ContextCommandItem[] = []
+        const workspaceRules = await this.collectWorkspaceRules(triggerContext)
+        let workspaceFolderPath = triggerContext.workspaceFolder?.uri
+            ? URI.parse(triggerContext.workspaceFolder.uri).fsPath
+            : process.cwd()
+
+        if (workspaceRules.length > 0) {
+            additionalContextCommands.push(
+                ...workspaceRules.map(
+                    file =>
+                        ({
+                            workspaceFolder: workspaceFolderPath,
+                            type: 'file',
+                            relativePath: path.relative(workspaceFolderPath, file),
+                            id: '',
+                        }) as ContextCommandItem
+                )
+            )
+        }
+        triggerContext.workspaceRulesCount = workspaceRules.length
+        if (context) {
+            additionalContextCommands.push(...this.mapToContextCommandItems(context, workspaceFolderPath))
+        }
+
+        if (additionalContextCommands.length === 0) {
+            return []
+        }
+
+        const prompts =
+            await LocalProjectContextController.getInstance().getContextCommandPrompt(additionalContextCommands)
+
+        const contextEntry: AdditionalContentEntryAddition[] = []
+        for (const prompt of prompts.slice(0, 20)) {
+            const contextType = this.getContextType(prompt)
+            const description =
+                contextType === 'rule' || contextType === 'prompt'
+                    ? `You must follow the instructions in ${prompt.relativePath}. Below are lines ${prompt.startLine}-${prompt.endLine} of this file:\n`
+                    : prompt.description
+
+            const relativePath = prompt.filePath.startsWith(getUserPromptsDirectory())
+                ? path.basename(prompt.filePath)
+                : path.relative(workspaceFolderPath, prompt.filePath)
+
+            const entry = {
+                name: prompt.name.substring(0, additionalContentNameLimit),
+                description: description.substring(0, additionalContentNameLimit),
+                innerContext: prompt.content.substring(0, additionalContentInnerContextLimit),
+                type: contextType,
+                relativePath: relativePath,
+                startLine: prompt.startLine,
+                endLine: prompt.endLine,
+            }
+
+            contextEntry.push(entry)
+        }
+        return contextEntry
+    }
+
+    getFileListFromContext(context: AdditionalContentEntryAddition[]): FileList {
+        const fileDetails: Record<string, FileDetails> = {}
+        for (const item of context) {
+            fileDetails[item.relativePath] = {
+                lineRanges: [
+                    {
+                        first: item.name === 'symbol' ? item.startLine : -1,
+                        second: item.name === 'symbol' ? item.endLine : -1,
+                    },
+                ],
+            }
+        }
+        const fileList: FileList = {
+            filePaths: context.map(item => item.relativePath),
+            details: fileDetails,
+        }
+        return fileList
+    }
+
+    mapToContextCommandItems(context: any[], workspaceFolderPath: string): ContextCommandItem[] {
+        const contextCommands: ContextCommandItem[] = []
+        for (const item of context) {
+            let itemType: ContextCommandItemType | undefined
+            if (item.icon === 'file') {
+                itemType = 'file'
+            } else if (item.icon === 'folder') {
+                itemType = 'folder'
+            } else if (item.icon === 'code-block') {
+                itemType = 'code'
+            }
+            if (itemType) {
+                contextCommands.push({
+                    workspaceFolder: workspaceFolderPath,
+                    type: itemType,
+                    relativePath: item.command,
+                    id: item.id,
+                })
+            }
+        }
+        return contextCommands
+    }
+}
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
new file mode 100644
index 0000000000..2ce227c3e1
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts
@@ -0,0 +1,137 @@
+/**
+ * Copied from chat/contexts/triggerContext.ts for the purpose of developing a divergent implementation.
+ * Will be deleted or merged.
+ */
+
+import { TriggerType } from '@aws/chat-client-ui-types'
+import {
+    ChatTriggerType,
+    UserIntent,
+    Tool,
+    ToolResult,
+    AdditionalContentEntry,
+    GenerateAssistantResponseCommandInput,
+} from '@amzn/codewhisperer-streaming'
+import {
+    BedrockTools,
+    ChatParams,
+    CursorState,
+    InlineChatParams,
+    QuickActionCommand,
+    FileList,
+} from '@aws/language-server-runtimes/server-interface'
+import { Features } from '../../types'
+import { DocumentContext, DocumentContextExtractor } from '../../chat/contexts/documentContext'
+
+export interface TriggerContext extends Partial<DocumentContext> {
+    userIntent?: UserIntent
+    triggerType?: TriggerType
+    workspaceRulesCount?: number
+    documentReference?: FileList
+}
+export type LineInfo = { startLine: number; endLine: number }
+
+export type AdditionalContentEntryAddition = AdditionalContentEntry & { type: string; relativePath: string } & LineInfo
+
+export class AgenticChatTriggerContext {
+    private static readonly DEFAULT_CURSOR_STATE: CursorState = { position: { line: 0, character: 0 } }
+
+    #workspace: Features['workspace']
+    #documentContextExtractor: DocumentContextExtractor
+
+    constructor(workspace: Features['workspace'], logger: Features['logging']) {
+        this.#workspace = workspace
+        this.#documentContextExtractor = new DocumentContextExtractor({ logger, workspace })
+    }
+
+    async getNewTriggerContext(params: ChatParams | InlineChatParams): Promise<TriggerContext> {
+        const documentContext: DocumentContext | undefined = await this.extractDocumentContext(params)
+
+        return {
+            ...documentContext,
+            userIntent: this.#guessIntentFromPrompt(params.prompt.prompt),
+        }
+    }
+
+    getChatParamsFromTrigger(
+        params: ChatParams | InlineChatParams,
+        triggerContext: TriggerContext,
+        chatTriggerType: ChatTriggerType,
+        customizationArn?: string,
+        profileArn?: string,
+        tools: BedrockTools = [],
+        additionalContent?: AdditionalContentEntryAddition[]
+    ): GenerateAssistantResponseCommandInput {
+        const { prompt } = params
+
+        const data: GenerateAssistantResponseCommandInput = {
+            conversationState: {
+                chatTriggerType: chatTriggerType,
+                currentMessage: {
+                    userInputMessage: {
+                        content: prompt.escapedPrompt ?? prompt.prompt,
+                        userInputMessageContext:
+                            triggerContext.cursorState && triggerContext.relativeFilePath
+                                ? {
+                                      editorState: {
+                                          cursorState: triggerContext.cursorState,
+                                          document: {
+                                              text: triggerContext.text,
+                                              programmingLanguage: triggerContext.programmingLanguage,
+                                              relativeFilePath: triggerContext.relativeFilePath,
+                                          },
+                                      },
+                                      tools,
+                                      additionalContext: additionalContent,
+                                  }
+                                : {
+                                      tools,
+                                      additionalContext: additionalContent,
+                                  },
+                        userIntent: triggerContext.userIntent,
+                        origin: 'IDE',
+                    },
+                },
+                customizationArn,
+            },
+            profileArn,
+        }
+
+        return data
+    }
+
+    // public for testing
+    async extractDocumentContext(
+        input: Pick<ChatParams | InlineChatParams, 'cursorState' | 'textDocument'>
+    ): Promise<DocumentContext | undefined> {
+        const { textDocument: textDocumentIdentifier, cursorState } = input
+
+        const textDocument =
+            textDocumentIdentifier?.uri && (await this.#workspace.getTextDocument(textDocumentIdentifier.uri))
+
+        return textDocument
+            ? this.#documentContextExtractor.extractDocumentContext(
+                  textDocument,
+                  // we want to include a default position if a text document is found so users can still ask questions about the opened file
+                  // the range will be expanded up to the max characters downstream
+                  cursorState?.[0] ?? AgenticChatTriggerContext.DEFAULT_CURSOR_STATE
+              )
+            : undefined
+    }
+
+    #guessIntentFromPrompt(prompt?: string): UserIntent | undefined {
+        if (prompt === undefined) {
+            return undefined
+        } else if (/^explain/i.test(prompt)) {
+            return UserIntent.EXPLAIN_CODE_SELECTION
+        } else if (/^refactor/i.test(prompt)) {
+            return UserIntent.SUGGEST_ALTERNATE_IMPLEMENTATION
+        } else if (/^fix/i.test(prompt)) {
+            return UserIntent.APPLY_COMMON_BEST_PRACTICES
+        } else if (/^optimize/i.test(prompt)) {
+            return UserIntent.IMPROVE_CODE
+        }
+
+        return undefined
+    }
+}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContexts.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContexts.test.ts
new file mode 100644
index 0000000000..fec4a637cb
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContexts.test.ts
@@ -0,0 +1,99 @@
+/**
+ * Copied from chat/contexts/triggerContext.test.ts for the purpose of developing a divergent implementation.
+ * Will be deleted or merged.
+ */
+
+import { TestFeatures } from '@aws/language-server-runtimes/testing'
+import assert = require('assert')
+import { TextDocument } from 'vscode-languageserver-textdocument'
+import sinon = require('sinon')
+import { AgenticChatTriggerContext } from './agenticChatTriggerContext'
+import { DocumentContext, DocumentContextExtractor } from '../../chat/contexts/documentContext'
+
+describe('AgenticChatTriggerContext', () => {
+    let testFeatures: TestFeatures
+
+    const filePath = 'file://test.ts'
+    const mockTSDocument = TextDocument.create(filePath, 'typescript', 1, '')
+    const mockDocumentContext: DocumentContext = {
+        text: '',
+        programmingLanguage: { languageName: 'typescript' },
+        relativeFilePath: 'file://test.ts',
+        hasCodeSnippet: false,
+        totalEditorCharacters: 0,
+    }
+
+    beforeEach(() => {
+        testFeatures = new TestFeatures()
+        sinon.stub(DocumentContextExtractor.prototype, 'extractDocumentContext').resolves(mockDocumentContext)
+    })
+
+    afterEach(() => {
+        sinon.restore()
+    })
+
+    it('returns null if text document is not defined in params', async () => {
+        const triggerContext = new AgenticChatTriggerContext(testFeatures.workspace, testFeatures.logging)
+
+        const documentContext = await triggerContext.extractDocumentContext({
+            cursorState: [
+                {
+                    position: {
+                        line: 5,
+                        character: 0,
+                    },
+                },
+            ],
+            textDocument: undefined,
+        })
+
+        assert.deepStrictEqual(documentContext, undefined)
+    })
+
+    it('returns null if text document is not found', async () => {
+        const triggerContext = new AgenticChatTriggerContext(testFeatures.workspace, testFeatures.logging)
+
+        const documentContext = await triggerContext.extractDocumentContext({
+            cursorState: [
+                {
+                    position: {
+                        line: 5,
+                        character: 0,
+                    },
+                },
+            ],
+            textDocument: {
+                uri: filePath,
+            },
+        })
+
+        assert.deepStrictEqual(documentContext, undefined)
+    })
+
+    it('passes default cursor state if no cursor is found', async () => {
+        const triggerContext = new AgenticChatTriggerContext(testFeatures.workspace, testFeatures.logging)
+
+        const documentContext = await triggerContext.extractDocumentContext({
+            cursorState: [],
+            textDocument: {
+                uri: filePath,
+            },
+        })
+
+        assert.deepStrictEqual(documentContext, undefined)
+    })
+
+    it('includes cursor state from the parameters and text document if found', async () => {
+        const triggerContext = new AgenticChatTriggerContext(testFeatures.workspace, testFeatures.logging)
+
+        testFeatures.openDocument(mockTSDocument)
+        const documentContext = await triggerContext.extractDocumentContext({
+            cursorState: [],
+            textDocument: {
+                uri: filePath,
+            },
+        })
+
+        assert.deepStrictEqual(documentContext, mockDocumentContext)
+    })
+})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.test.ts
new file mode 100644
index 0000000000..53760268bd
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.test.ts
@@ -0,0 +1,54 @@
+import { ContextCommandsProvider } from './contextCommandsProvider'
+import * as sinon from 'sinon'
+import { TestFeatures } from '@aws/language-server-runtimes/testing'
+import * as chokidar from 'chokidar'
+
+describe('ContextCommandsProvider', () => {
+    let provider: ContextCommandsProvider
+    let testFeatures: TestFeatures
+    let fsExistsStub: sinon.SinonStub
+    let fsReadDirStub: sinon.SinonStub
+
+    beforeEach(() => {
+        sinon.stub(chokidar, 'watch').returns({
+            on: sinon.stub(),
+            close: sinon.stub(),
+        } as unknown as chokidar.FSWatcher)
+        testFeatures = new TestFeatures()
+        fsExistsStub = sinon.stub()
+        fsReadDirStub = sinon.stub()
+
+        testFeatures.workspace.fs.exists = fsExistsStub
+        testFeatures.workspace.fs.readdir = fsReadDirStub
+        provider = new ContextCommandsProvider(testFeatures.logging, testFeatures.chat, testFeatures.workspace)
+        sinon.stub(provider, 'registerPromptFileWatcher').resolves()
+    })
+
+    afterEach(() => {
+        sinon.restore()
+    })
+
+    describe('getUserPrompts', () => {
+        it('should return empty commands list when directory does not exist', async () => {
+            fsExistsStub.resolves(false)
+            const result = await provider.getUserPromptsCommand()
+
+            sinon.assert.match(result.length, 1) // Only create prompt button
+            sinon.assert.match(result[0].command, 'Create a new prompt')
+        })
+
+        it('should return prompt commands when directory exists with files', async () => {
+            fsExistsStub.resolves(true)
+            fsReadDirStub.resolves([
+                { name: 'test1.prompt.md', isFile: () => true },
+                { name: 'test2.prompt.md', isFile: () => true },
+            ])
+
+            const result = await provider.getUserPromptsCommand()
+
+            sinon.assert.match(result.length, 3) // 2 files + create button
+            sinon.assert.match(result[0].command, 'test1')
+            sinon.assert.match(result[1].command, 'test2')
+        })
+    })
+})
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
new file mode 100644
index 0000000000..72cce548c6
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts
@@ -0,0 +1,170 @@
+import * as path from 'path'
+import { FSWatcher, watch } from 'chokidar'
+import { ContextCommand, ContextCommandGroup } from '@aws/language-server-runtimes/protocol'
+import { Disposable } from 'vscode-languageclient/node'
+import { Chat, Logging, Workspace } from '@aws/language-server-runtimes/server-interface'
+import { getUserPromptsDirectory, promptFileExtension } from './contextUtils'
+import { ContextCommandItem } from 'local-indexing'
+
+export class ContextCommandsProvider implements Disposable {
+    private promptFileWatcher?: FSWatcher
+    private cachedContextCommands?: ContextCommandItem[]
+    constructor(
+        private readonly logging: Logging,
+        private readonly chat: Chat,
+        private readonly workspace: Workspace
+    ) {
+        this.registerPromptFileWatcher()
+    }
+
+    registerPromptFileWatcher() {
+        this.promptFileWatcher = watch(getUserPromptsDirectory(), {
+            persistent: true,
+            interval: 2000,
+            ignoreInitial: true,
+        })
+
+        this.promptFileWatcher.on('add', async () => {
+            await this.processContextCommandUpdate(this.cachedContextCommands ?? [])
+        })
+
+        this.promptFileWatcher.on('unlink', async () => {
+            await this.processContextCommandUpdate(this.cachedContextCommands ?? [])
+        })
+    }
+
+    async getUserPromptsCommand(): Promise<ContextCommand[]> {
+        const createPromptCommand = {
+            command: 'Create a new prompt',
+            id: 'create-saved-prompt',
+            icon: 'list-add',
+        }
+        try {
+            const userPromptsDirectory = getUserPromptsDirectory()
+            const directoryExists = await this.workspace.fs.exists(userPromptsDirectory)
+            if (directoryExists) {
+                const files = await this.workspace.fs.readdir(userPromptsDirectory)
+                const systemPromptFiles = files.filter(file => file.name.endsWith(promptFileExtension))
+                const promptCommands = systemPromptFiles.map(
+                    file =>
+                        ({
+                            command: path.basename(file.name, promptFileExtension),
+                            icon: 'magic',
+                            label: 'file',
+                            id: 'prompt',
+                            route: [userPromptsDirectory, file.name],
+                        }) as ContextCommand
+                )
+                promptCommands.push(createPromptCommand)
+                return promptCommands
+            } else {
+                return [createPromptCommand]
+            }
+        } catch (e) {
+            this.logging.warn(`Error reading user prompts directory: ${e}`)
+            return [createPromptCommand]
+        }
+    }
+
+    async processContextCommandUpdate(items: ContextCommandItem[]) {
+        const allItems = await this.mapContextCommandItems(items)
+        this.chat.sendContextCommands({ contextCommandGroups: allItems })
+        this.cachedContextCommands = items
+    }
+
+    async mapContextCommandItems(items: ContextCommandItem[]): Promise<ContextCommandGroup[]> {
+        const folderCmds: ContextCommand[] = []
+        const folderCmdGroup: ContextCommand = {
+            command: 'Folders',
+            children: [
+                {
+                    groupName: 'Folders',
+                    commands: folderCmds,
+                },
+            ],
+            description: 'Add all files in a folder to context',
+            icon: 'folder',
+        }
+
+        const fileCmds: ContextCommand[] = []
+        const fileCmdGroup: ContextCommand = {
+            command: 'Files',
+            children: [
+                {
+                    groupName: 'Files',
+                    commands: fileCmds,
+                },
+            ],
+            description: 'Add a file to context',
+            icon: 'file',
+        }
+
+        const codeCmds: ContextCommand[] = []
+        const codeCmdGroup: ContextCommand = {
+            command: 'Code',
+            children: [
+                {
+                    groupName: 'Code',
+                    commands: codeCmds,
+                },
+            ],
+            description: 'Add code to context',
+            icon: 'code-block',
+        }
+
+        const promptCmds: ContextCommand[] = []
+        const promptCmdGroup: ContextCommand = {
+            command: 'Prompts',
+            children: [
+                {
+                    groupName: 'Prompts',
+                    commands: promptCmds,
+                },
+            ],
+            description: 'Add a saved prompt to context',
+            icon: 'magic',
+        }
+        const allCommands = [
+            {
+                commands: [folderCmdGroup, fileCmdGroup, codeCmdGroup, promptCmdGroup],
+            },
+        ] as ContextCommandGroup[]
+
+        for (const item of items) {
+            const wsFolderName = path.basename(item.workspaceFolder)
+            let baseItem = {
+                command: path.basename(item.relativePath),
+                description: path.join(wsFolderName, item.relativePath),
+                route: [item.workspaceFolder, item.relativePath],
+                id: item.id,
+            }
+            if (item.type === 'file') {
+                fileCmds.push({
+                    ...baseItem,
+                    label: 'file',
+                    icon: 'file',
+                })
+            } else if (item.type === 'folder') {
+                folderCmds.push({
+                    ...baseItem,
+                    label: 'folder',
+                    icon: 'folder',
+                })
+            } else if (item.symbol) {
+                codeCmds.push({
+                    ...baseItem,
+                    description: `${item.symbol.kind}, ${path.join(wsFolderName, item.relativePath)}, L${item.symbol.range.start.line}-${item.symbol.range.end.line}`,
+                    label: 'code',
+                    icon: 'code-block',
+                })
+            }
+        }
+        const userPromptsItem = await this.getUserPromptsCommand()
+        promptCmds.push(...userPromptsItem)
+        return allCommands
+    }
+
+    dispose() {
+        void this.promptFileWatcher?.close()
+    }
+}
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
new file mode 100644
index 0000000000..5dfb134583
--- /dev/null
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts
@@ -0,0 +1,18 @@
+import { getUserHomeDir } from '@aws/lsp-core/out/util/path'
+import * as path from 'path'
+
+export const promptFileExtension = '.prompt.md'
+export const additionalContentInnerContextLimit = 8192
+export const additionalContentNameLimit = 1024
+
+export const getUserPromptsDirectory = (): string => {
+    return path.join(getUserHomeDir(), '.aws', 'amazonq', 'prompts')
+}
+
+export const getNewPromptFilePath = (promptName: string): string => {
+    const userPromptsDirectory = getUserPromptsDirectory()
+    return path.join(
+        userPromptsDirectory,
+        promptName ? `${promptName}${promptFileExtension}` : `default${promptFileExtension}`
+    )
+}
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 edcbae37f0..fe7c6b813e 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts
@@ -171,9 +171,9 @@ If a file is not open, use the \`fsRead\` tool to read from disk. Use this tool
             }, Promise.resolve<Record<string, string>>({}))
         )
 
-        chat.onChatPrompt((...params) => {
+        chat.onChatPrompt((params, token) => {
             logging.log('Received chat prompt')
-            return chatController.onChatPrompt(...params)
+            return chatController.onChatPrompt(params, token)
         })
 
         chat.onInlineChatPrompt((...params) => {
@@ -200,6 +200,14 @@ If a file is not open, use the \`fsRead\` tool to read from disk. Use this tool
         chat.onConversationClick(params => {
             return chatController.onConversationClick(params)
         })
+       
+        chat.onCreatePrompt((params) => {
+            return chatController.onCreatePrompt(params)
+        })
+
+        chat.onFileClicked((params) => {
+            return chatController.onFileClicked(params)
+        })
 
         chat.onTabBarAction(params => {
             return chatController.onTabBarAction(params)
diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.test.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.test.ts
index cf95a278cb..8fc5a7938f 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.test.ts
@@ -9,10 +9,13 @@ describe('DocumentContext', () => {
     const mockTypescriptCodeBlock = `function test() {
     console.log('test')
 }`
+
+    const mockWorkspaceFolder = {
+        uri: 'file://mock/workspace',
+        name: 'test',
+    }
     const mockWorkspace = {
-        getWorkspaceFolder: sinon.stub().returns({
-            uri: 'file://mock/workspace',
-        }),
+        getWorkspaceFolder: sinon.stub().returns(mockWorkspaceFolder),
         fs: {
             existsSync: sinon.stub().returns(true),
         },
@@ -48,6 +51,7 @@ describe('DocumentContext', () => {
                         },
                     },
                 },
+                workspaceFolder: mockWorkspaceFolder,
             }
 
             const result = await documentContextExtractor.extractDocumentContext(mockTSDocument, {
@@ -90,6 +94,7 @@ describe('DocumentContext', () => {
                         },
                     },
                 },
+                workspaceFolder: mockWorkspaceFolder,
             }
 
             const result = await documentContextExtractor.extractDocumentContext(mockTSDocument, {
@@ -131,6 +136,7 @@ describe('DocumentContext', () => {
                     end: { line: 0, character: 19 },
                 },
             },
+            workspaceFolder: mockWorkspaceFolder,
         }
         const result = await documentContextExtractor.extractDocumentContext(mockDocument, {
             range: {
diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.ts
index cd37272cf6..bac94d2587 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/chat/contexts/documentContext.ts
@@ -1,5 +1,5 @@
 import { EditorState, TextDocument as CwsprTextDocument } from '@amzn/codewhisperer-streaming'
-import { CursorState } from '@aws/language-server-runtimes/server-interface'
+import { CursorState, WorkspaceFolder } from '@aws/language-server-runtimes/server-interface'
 import { Range, TextDocument } from 'vscode-languageserver-textdocument'
 import { getLanguageId } from '../../../shared/languageDetection'
 import { Features } from '../../types'
@@ -11,6 +11,7 @@ export type DocumentContext = CwsprTextDocument & {
     cursorState?: EditorState['cursorState']
     hasCodeSnippet: boolean
     totalEditorCharacters: number
+    workspaceFolder?: WorkspaceFolder | null
 }
 
 export interface DocumentContextExtractorConfig {
@@ -49,6 +50,8 @@ export class DocumentContextExtractor {
 
         const rangeWithinCodeBlock = getSelectionWithinExtendedRange(targetRange, codeBlockRange)
 
+        const workspaceFolder = this.#workspace?.getWorkspaceFolder?.(document.uri)
+
         const relativePath = this.getRelativePath(document)
 
         const languageId = getLanguageId(document)
@@ -60,6 +63,7 @@ export class DocumentContextExtractor {
             relativeFilePath: relativePath,
             hasCodeSnippet: Boolean(rangeWithinCodeBlock),
             totalEditorCharacters: document.getText().length,
+            workspaceFolder,
         }
     }
 
diff --git a/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts b/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts
index b7cc578d15..3ff081dc99 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts
@@ -6,7 +6,7 @@ import { languageByExtension } from '../../shared/languageDetection'
 import { AmazonQWorkspaceConfig } from '../../shared/amazonQServiceManager/configurationUtils'
 
 export const LocalProjectContextServer = (): Server => features => {
-    const { credentialsProvider, telemetry, logging, lsp } = features
+    const { credentialsProvider, telemetry, logging, lsp, chat, workspace } = features
 
     let localProjectContextController: LocalProjectContextController
     let amazonQServiceManager: AmazonQTokenServiceManager
@@ -20,7 +20,9 @@ export const LocalProjectContextServer = (): Server => features => {
         localProjectContextController = new LocalProjectContextController(
             params.clientInfo?.name ?? 'unknown',
             params.workspaceFolders ?? [],
-            logging
+            logging,
+            chat,
+            workspace
         )
 
         const supportedFilePatterns = Object.keys(languageByExtension).map(ext => `**/*${ext}`)
diff --git a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts
index 04670a2d76..36cb4fbce8 100644
--- a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts
+++ b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts
@@ -1,10 +1,12 @@
 import { LocalProjectContextController } from './localProjectContextController'
-import { SinonStub, stub, spy, assert as sinonAssert, match } from 'sinon'
+import { SinonStub, stub, assert as sinonAssert, match, restore } from 'sinon'
 import * as assert from 'assert'
 import * as fs from 'fs'
 import { Dirent } from 'fs'
 import * as path from 'path'
 import { URI } from 'vscode-uri'
+import { TestFeatures } from '@aws/language-server-runtimes/testing'
+import * as chokidar from 'chokidar'
 
 class LoggingMock {
     public error: SinonStub
@@ -26,10 +28,12 @@ describe('LocalProjectContextController', () => {
     let mockWorkspaceFolders: any[]
     let vectorLibMock: any
     let fsStub: SinonStub
+    let testFeatures: TestFeatures
 
     const BASE_PATH = path.join(__dirname, 'path', 'to', 'workspace1')
 
     beforeEach(() => {
+        testFeatures = new TestFeatures()
         logging = new LoggingMock()
         mockWorkspaceFolders = [
             {
@@ -37,6 +41,10 @@ describe('LocalProjectContextController', () => {
                 name: 'workspace1',
             },
         ]
+        stub(chokidar, 'watch').returns({
+            on: stub(),
+            close: stub(),
+        } as unknown as chokidar.FSWatcher)
 
         vectorLibMock = {
             start: stub().resolves({
@@ -45,6 +53,9 @@ describe('LocalProjectContextController', () => {
                 queryVectorIndex: stub().resolves(['mockChunk1', 'mockChunk2']),
                 queryInlineProjectContext: stub().resolves(['mockContext1']),
                 updateIndexV2: stub().resolves(),
+                getContextCommandItems: stub().resolves([]),
+                getIndexSequenceNumber: stub().resolves(1),
+                getContextCommandPrompt: stub().resolves([]),
             }),
         }
 
@@ -58,11 +69,20 @@ describe('LocalProjectContextController', () => {
                 return Promise.resolve([])
             }
         })
-        controller = new LocalProjectContextController('testClient', mockWorkspaceFolders, logging as any)
+
+        controller = new LocalProjectContextController(
+            'testClient',
+            mockWorkspaceFolders,
+            logging as any,
+            testFeatures.chat,
+            testFeatures.workspace
+        )
+        stub(controller, 'maybeUpdateCodeSymbols').resolves()
     })
 
     afterEach(() => {
         fsStub.restore()
+        restore()
     })
 
     describe('init', () => {
@@ -93,7 +113,9 @@ describe('LocalProjectContextController', () => {
             const uninitializedController = new LocalProjectContextController(
                 'testClient',
                 mockWorkspaceFolders,
-                logging as any
+                logging as any,
+                testFeatures.chat,
+                testFeatures.workspace
             )
 
             const result = await uninitializedController.queryVectorIndex({ query: 'test' })
@@ -124,7 +146,9 @@ describe('LocalProjectContextController', () => {
             const uninitializedController = new LocalProjectContextController(
                 'testClient',
                 mockWorkspaceFolders,
-                logging as any
+                logging as any,
+                testFeatures.chat,
+                testFeatures.workspace
             )
 
             const result = await uninitializedController.queryInlineProjectContext({
@@ -167,7 +191,9 @@ describe('LocalProjectContextController', () => {
             const uninitializedController = new LocalProjectContextController(
                 'testClient',
                 mockWorkspaceFolders,
-                logging as any
+                logging as any,
+                testFeatures.chat,
+                testFeatures.workspace
             )
 
             await uninitializedController.updateIndex(['test.java'], 'add')
diff --git a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts
index ea410a400d..f4f5538251 100644
--- a/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts
+++ b/server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts
@@ -1,9 +1,11 @@
-import { WorkspaceIndexConfiguration, Logging, WorkspaceFolder } from '@aws/language-server-runtimes/server-interface'
+import { Logging, WorkspaceFolder, Chat, Workspace } from '@aws/language-server-runtimes/server-interface'
 import { dirname } from 'path'
 import { languageByExtension } from './languageDetection'
 import { homedir } from 'os'
 import type {
+    AdditionalContextPrompt,
     Chunk,
+    ContextCommandItem,
     InlineProjectContext,
     QueryInlineProjectContextRequestV2,
     QueryRequest,
@@ -11,6 +13,8 @@ import type {
     VectorLibAPI,
 } from 'local-indexing'
 import { URI } from 'vscode-uri'
+import { waitUntil } from '@aws/lsp-core/out/util/timeoutUtils'
+import { ContextCommandsProvider } from '../language-server/agenticChat/context/contextCommandsProvider'
 
 import * as fs from 'fs'
 import * as path from 'path'
@@ -46,6 +50,8 @@ export class LocalProjectContextController {
 
     private workspaceFolders: WorkspaceFolder[]
     private _vecLib?: VectorLibAPI
+    private _contextCommandSymbolsUpdated = false
+    private readonly contextCommandsProvider: ContextCommandsProvider
     private readonly clientName: string
     private readonly log: Logging
 
@@ -61,10 +67,17 @@ export class LocalProjectContextController {
     private readonly DEFAULT_MAX_FILE_SIZE_MB = 10
     private readonly MB_TO_BYTES = 1024 * 1024
 
-    constructor(clientName: string, workspaceFolders: WorkspaceFolder[], logging: Logging) {
+    constructor(
+        clientName: string,
+        workspaceFolders: WorkspaceFolder[],
+        logging: Logging,
+        chat: Chat,
+        workspace: Workspace
+    ) {
         this.workspaceFolders = workspaceFolders
         this.clientName = clientName
         this.log = logging
+        this.contextCommandsProvider = new ContextCommandsProvider(logging, chat, workspace)
     }
 
     public static getInstance() {
@@ -98,6 +111,10 @@ export class LocalProjectContextController {
                 this._vecLib = await vecLib.start(LIBRARY_DIR, this.clientName, this.indexCacheDirPath)
                 await this.buildIndex()
                 LocalProjectContextController.instance = this
+
+                const contextItems = await this.getContextCommandItems()
+                await this.contextCommandsProvider.processContextCommandUpdate(contextItems)
+                void this.maybeUpdateCodeSymbols()
             } else {
                 this.log.warn(`Vector library could not be imported from: ${libraryPath}`)
             }
@@ -111,6 +128,7 @@ export class LocalProjectContextController {
             await this._vecLib?.clear?.()
             this._vecLib = undefined
         }
+        this.contextCommandsProvider?.dispose()
     }
 
     public async updateIndex(filePaths: string[], operation: UpdateMode): Promise<void> {
@@ -194,6 +212,71 @@ export class LocalProjectContextController {
         }
     }
 
+    public async getContextCommandItems(): Promise<ContextCommandItem[]> {
+        if (!this._vecLib) {
+            return []
+        }
+
+        try {
+            const foldersPath = this.workspaceFolders.map(folder => URI.parse(folder.uri).fsPath)
+            const resp = await this._vecLib?.getContextCommandItems(foldersPath)
+            this.log.log(`received ${resp.length} context command items`)
+            return resp ?? []
+        } catch (error) {
+            this.log.error(`Error in getContextCommandItems: ${error}`)
+            return []
+        }
+    }
+
+    public async shouldUpdateContextCommandSymbolsOnce(): Promise<boolean> {
+        if (this._contextCommandSymbolsUpdated) {
+            return false
+        }
+        this._contextCommandSymbolsUpdated = true
+        try {
+            const indexSeqNum = await this._vecLib?.getIndexSequenceNumber()
+            await this.updateIndex([], 'context_command_symbol_update')
+            await waitUntil(
+                async () => {
+                    const newIndexSeqNum = await this._vecLib?.getIndexSequenceNumber()
+                    if (newIndexSeqNum && indexSeqNum && newIndexSeqNum > indexSeqNum) {
+                        return true
+                    }
+                    return false
+                },
+                { interval: 1000, timeout: 60_000, truthy: true }
+            )
+            return true
+        } catch (error) {
+            this.log.error(`Error in shouldUpdateContextCommandSymbolsOnce: ${error}`)
+            return false
+        }
+    }
+
+    public async getContextCommandPrompt(
+        contextCommandItems: ContextCommandItem[]
+    ): Promise<AdditionalContextPrompt[]> {
+        if (!this._vecLib) {
+            return []
+        }
+
+        try {
+            const resp = await this._vecLib?.getContextCommandPrompt(contextCommandItems)
+            return resp ?? []
+        } catch (error) {
+            this.log.error(`Error in getContextCommandPrompt: ${error}`)
+            return []
+        }
+    }
+
+    async maybeUpdateCodeSymbols() {
+        const needUpdate = await LocalProjectContextController.getInstance().shouldUpdateContextCommandSymbolsOnce()
+        if (needUpdate) {
+            const items = await this.getContextCommandItems()
+            await this.contextCommandsProvider.processContextCommandUpdate(items)
+        }
+    }
+
     private fileMeetsFileSizeConstraints(filePath: string, sizeConstraints: SizeConstraints): boolean {
         let fileSize
 


Reviews:
user: login: justinmk3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T19:47:39Z
user: login: zixlin7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:51:50Z
user: login: zixlin7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T23:40:10Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T21:37:20Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T21:50:09Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T21:50:52Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T21:55:00Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T21:57:49Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T22:07:28Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T22:09:11Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T22:14:40Z
user: login: kmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T22:26:10Z
user: login: kmile
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T22:32:52Z
user: login: zixlin7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T23:36:40Z
user: login: zixlin7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T23:37:40Z
user: login: jpinkney-aws
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T00:10:36Z
user: login: jpinkney-aws
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T01:51:11Z
user: login: ege0zcan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T16:55:08Z
user: login: avi-alpert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T23:06:06Z

Review comments:
diff_hunk: @@ -0,0 +1,147 @@
+import * as path from 'path'
+import { FSWatcher, watch } from 'chokidar'
+import { Features } from '../../types'
+import { ContextCommand, ContextCommandGroup } from '@aws/language-server-runtimes/protocol'
+import { TriggerContext } from '../../chat/contexts/triggerContext'
+import { URI } from 'vscode-uri'
+import { getUserHomeDir } from '@aws/lsp-core/out/util/path'
+
+export type AdditionalContextLengths = {
+    fileContextLength: number
+    promptContextLength: number
+    ruleContextLength: number
+}
+
+export class ContextCommandsProvider {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts
user: login: justinmk3
body: does this codebase need a `Disposable` interface?
```suggestion
export class ContextCommandsProvider implements Disposable {
```
created_at: 2025-04-10T19:47:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,147 @@
+import * as path from 'path'
+import { FSWatcher, watch } from 'chokidar'
+import { Features } from '../../types'
+import { ContextCommand, ContextCommandGroup } from '@aws/language-server-runtimes/protocol'
+import { TriggerContext } from '../../chat/contexts/triggerContext'
+import { URI } from 'vscode-uri'
+import { getUserHomeDir } from '@aws/lsp-core/out/util/path'
+
+export type AdditionalContextLengths = {
+    fileContextLength: number
+    promptContextLength: number
+    ruleContextLength: number
+}
+
+export class ContextCommandsProvider {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts
user: login: zixlin7
body: I guess we can use the one from `vscode-jsonrpc`, will do
created_at: 2025-04-10T20:51:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,134 @@
+import { TriggerType } from '@aws/chat-client-ui-types'
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts
user: login: zixlin7
body: copied over from `chat/contexts/triggerContext.ts` and added `context?: QuickActionCommand[]` and `documentReference?: FileList` to the interface
created_at: 2025-04-15T23:39:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,6 +181,7 @@ describe('AgenticChatController', () => {
                 },
             },
         }
+        testFeatures.lsp.window.showDocument = sinon.stub()
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
user: login: kmile
body: Is this stub missing in `TestFeatures`?
created_at: 2025-04-17T21:37:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: kmile
body: nit: Why does `promptsDirectory` get its own handler ahd `rulesDirectory` does not?
created_at: 2025-04-17T21:50:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
+                return 'rule'
+            } else if (pathUtils.isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: kmile
body: Why do we use `relativePath` for rules but `filePath` for prompts? Is there a meaningful distinction?
created_at: 2025-04-17T21:50:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
+                return 'rule'
+            } else if (pathUtils.isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
+                return 'prompt'
+            }
+        }
+        return 'file'
+    }
+
+    async getAdditionalContext(
+        triggerContext: TriggerContext,
+        context?: any[]
+    ): Promise<AdditionalContentEntryAddition[]> {
+        const additionalContextCommands: ContextCommandItem[] = []
+        const workspaceRules = await this.collectWorkspaceRules(triggerContext)
+        let workspaceFolderPath = triggerContext.workspaceFolder?.uri
+            ? URI.parse(triggerContext.workspaceFolder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+
+        if (workspaceRules.length > 0) {
+            additionalContextCommands.push(
+                ...workspaceRules.map(file => ({
+                    workspaceFolder: workspaceFolderPath,
+                    type: 'file' as ContextCommandItemType,
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: kmile
body: This hides the type safety of the `type` type. It might be safer to set the return type on the `map` or on the upper array, so the compiler actually complains when the `'file'` is not actually a `ContextCommandItemType`.

Right now, this would compile:

```
type: 'definitelyNotAnItemType' as ContextCommandItemType
```
created_at: 2025-04-17T21:55:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
+                return 'rule'
+            } else if (pathUtils.isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
+                return 'prompt'
+            }
+        }
+        return 'file'
+    }
+
+    async getAdditionalContext(
+        triggerContext: TriggerContext,
+        context?: any[]
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: kmile
body: Does this have to be `any[]`? Is there no more specific type we can use here? It looks like we're using `context` in a pretty specific way inside `mapToContextCommandItems`, is that a type we can extract and use here?
created_at: 2025-04-17T21:57:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
+                return 'rule'
+            } else if (pathUtils.isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
+                return 'prompt'
+            }
+        }
+        return 'file'
+    }
+
+    async getAdditionalContext(
+        triggerContext: TriggerContext,
+        context?: any[]
+    ): Promise<AdditionalContentEntryAddition[]> {
+        const additionalContextCommands: ContextCommandItem[] = []
+        const workspaceRules = await this.collectWorkspaceRules(triggerContext)
+        let workspaceFolderPath = triggerContext.workspaceFolder?.uri
+            ? URI.parse(triggerContext.workspaceFolder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+
+        if (workspaceRules.length > 0) {
+            additionalContextCommands.push(
+                ...workspaceRules.map(file => ({
+                    workspaceFolder: workspaceFolderPath,
+                    type: 'file' as ContextCommandItemType,
+                    relativePath: path.relative(workspaceFolderPath, file),
+                    id: '',
+                }))
+            )
+        }
+        triggerContext.workspaceRulesCount = workspaceRules.length
+        if (context) {
+            additionalContextCommands.push(...this.mapToContextCommandItems(context, workspaceFolderPath))
+        }
+
+        if (additionalContextCommands.length === 0) {
+            return []
+        }
+
+        const prompts =
+            await LocalProjectContextController.getInstance().getContextCommandPrompt(additionalContextCommands)
+
+        const contextEntry: AdditionalContentEntryAddition[] = []
+        for (const prompt of prompts.slice(0, 20)) {
+            const contextType = this.getContextType(prompt)
+            const description =
+                contextType === 'rule' || contextType === 'prompt'
+                    ? `You must follow the instructions in ${prompt.relativePath}. Below are lines ${prompt.startLine}-${prompt.endLine} of this file:\n`
+                    : prompt.description
+
+            const relativePath = prompt.filePath.startsWith(getUserPromptsDirectory())
+                ? path.basename(prompt.filePath)
+                : path.relative(workspaceFolderPath, prompt.filePath)
+
+            const entry = {
+                name: prompt.name.substring(0, additionalContentNameLimit),
+                description: description.substring(0, additionalContentNameLimit),
+                innerContext: prompt.content.substring(0, additionalContentInnerContextLimit),
+                type: contextType,
+                relativePath: relativePath,
+                startLine: prompt.startLine,
+                endLine: prompt.endLine,
+            }
+
+            contextEntry.push(entry)
+        }
+        return contextEntry
+    }
+
+    getFileListFromContext(context: AdditionalContentEntryAddition[]): FileList {
+        const fileDetails: Record<string, FileDetails> = {}
+        for (const item of context) {
+            fileDetails[item.relativePath] = {
+                lineRanges: [
+                    {
+                        first: item.name === 'symbol' ? item.startLine : -1,
+                        second: item.name === 'symbol' ? item.endLine : -1,
+                    },
+                ],
+            }
+        }
+        const fileList: FileList = {
+            filePaths: context.map(item => item.relativePath),
+            details: fileDetails,
+        }
+        return fileList
+    }
+
+    mapToContextCommandItems(context: any[], workspaceFolderPath: string): ContextCommandItem[] {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: kmile
body: I don't really get this method, instead of using `any` and converting it using this method, can't we use `ContextCommandItem` directly where we call this method?

The `context: any` always seems to have this type:

```
{ icon: 'file' | 'folder' | 'code-block' | string, command: string | undefined, id: string | undefined }
```
which combined with `workspaceFolderPath` is essentially the same as `ContextCommandItem` already.

This method maps the `any` to the specific type and filters out the known `icon`s?
created_at: 2025-04-17T22:07:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,134 @@
+import { TriggerType } from '@aws/chat-client-ui-types'
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts
user: login: kmile
body: Would be good to add this to the header of the file as well since this PR comment will get lost once merged. That way we can track the files' origins once we merge and clean up the `chat` and `agenticChat` folders.
created_at: 2025-04-17T22:09:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+import * as path from 'path'
+import { FSWatcher, watch } from 'chokidar'
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts
user: login: kmile
body: We're using `workspace.fs` to access the filesystem, but in this case I think `chokidar` uses the underlying `fs` directly.

Could this use https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWatchedFiles instead?

This could be a follow up, but it does render the `agenticChat` unusable in browser environments in the meantime.
created_at: 2025-04-17T22:14:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,15 +24,24 @@ export class LocalProjectContextController {
 
     private workspaceFolders: WorkspaceFolder[]
     private _vecLib?: VectorLibAPI
+    private _contextCommandSymbolsUpdated = false
+    private readonly contextCommandsProvider: ContextCommandsProvider
     private readonly fileExtensions: string[]
     private readonly clientName: string
     private readonly log: Logging
 
-    constructor(clientName: string, workspaceFolders: WorkspaceFolder[], logging: Logging) {
+    constructor(
+        clientName: string,
+        workspaceFolders: WorkspaceFolder[],
+        logging: Logging,
+        chat: Chat,
+        workspace: Workspace
+    ) {
         this.fileExtensions = Object.keys(languageByExtension)
         this.workspaceFolders = workspaceFolders
         this.clientName = clientName
         this.log = logging
+        this.contextCommandsProvider = new ContextCommandsProvider(logging, chat, workspace)
     }
 
     public static getInstance() {
path: server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts
user: login: kmile
body: Do we use this anywhere? It seems like we're just instantiating the class, and calling it in the `Server` (which is great, I much prefer that over singletons)
created_at: 2025-04-17T22:26:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,15 +24,24 @@ export class LocalProjectContextController {
 
     private workspaceFolders: WorkspaceFolder[]
     private _vecLib?: VectorLibAPI
+    private _contextCommandSymbolsUpdated = false
+    private readonly contextCommandsProvider: ContextCommandsProvider
     private readonly fileExtensions: string[]
     private readonly clientName: string
     private readonly log: Logging
 
-    constructor(clientName: string, workspaceFolders: WorkspaceFolder[], logging: Logging) {
+    constructor(
+        clientName: string,
+        workspaceFolders: WorkspaceFolder[],
+        logging: Logging,
+        chat: Chat,
+        workspace: Workspace
+    ) {
         this.fileExtensions = Object.keys(languageByExtension)
         this.workspaceFolders = workspaceFolders
         this.clientName = clientName
         this.log = logging
+        this.contextCommandsProvider = new ContextCommandsProvider(logging, chat, workspace)
     }
 
     public static getInstance() {
path: server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts
user: login: zixlin7
body: Ah I forgot to dispose it, fixed
created_at: 2025-04-17T23:31:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
+                return 'rule'
+            } else if (pathUtils.isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
+                return 'prompt'
+            }
+        }
+        return 'file'
+    }
+
+    async getAdditionalContext(
+        triggerContext: TriggerContext,
+        context?: any[]
+    ): Promise<AdditionalContentEntryAddition[]> {
+        const additionalContextCommands: ContextCommandItem[] = []
+        const workspaceRules = await this.collectWorkspaceRules(triggerContext)
+        let workspaceFolderPath = triggerContext.workspaceFolder?.uri
+            ? URI.parse(triggerContext.workspaceFolder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+
+        if (workspaceRules.length > 0) {
+            additionalContextCommands.push(
+                ...workspaceRules.map(file => ({
+                    workspaceFolder: workspaceFolderPath,
+                    type: 'file' as ContextCommandItemType,
+                    relativePath: path.relative(workspaceFolderPath, file),
+                    id: '',
+                }))
+            )
+        }
+        triggerContext.workspaceRulesCount = workspaceRules.length
+        if (context) {
+            additionalContextCommands.push(...this.mapToContextCommandItems(context, workspaceFolderPath))
+        }
+
+        if (additionalContextCommands.length === 0) {
+            return []
+        }
+
+        const prompts =
+            await LocalProjectContextController.getInstance().getContextCommandPrompt(additionalContextCommands)
+
+        const contextEntry: AdditionalContentEntryAddition[] = []
+        for (const prompt of prompts.slice(0, 20)) {
+            const contextType = this.getContextType(prompt)
+            const description =
+                contextType === 'rule' || contextType === 'prompt'
+                    ? `You must follow the instructions in ${prompt.relativePath}. Below are lines ${prompt.startLine}-${prompt.endLine} of this file:\n`
+                    : prompt.description
+
+            const relativePath = prompt.filePath.startsWith(getUserPromptsDirectory())
+                ? path.basename(prompt.filePath)
+                : path.relative(workspaceFolderPath, prompt.filePath)
+
+            const entry = {
+                name: prompt.name.substring(0, additionalContentNameLimit),
+                description: description.substring(0, additionalContentNameLimit),
+                innerContext: prompt.content.substring(0, additionalContentInnerContextLimit),
+                type: contextType,
+                relativePath: relativePath,
+                startLine: prompt.startLine,
+                endLine: prompt.endLine,
+            }
+
+            contextEntry.push(entry)
+        }
+        return contextEntry
+    }
+
+    getFileListFromContext(context: AdditionalContentEntryAddition[]): FileList {
+        const fileDetails: Record<string, FileDetails> = {}
+        for (const item of context) {
+            fileDetails[item.relativePath] = {
+                lineRanges: [
+                    {
+                        first: item.name === 'symbol' ? item.startLine : -1,
+                        second: item.name === 'symbol' ? item.endLine : -1,
+                    },
+                ],
+            }
+        }
+        const fileList: FileList = {
+            filePaths: context.map(item => item.relativePath),
+            details: fileDetails,
+        }
+        return fileList
+    }
+
+    mapToContextCommandItems(context: any[], workspaceFolderPath: string): ContextCommandItem[] {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: zixlin7
body: I think the main reason we have to do this is because context is missing `type` which is used in the call to get prompt from the local context server, but makes sense to create a type for it instead of using any, will follow up
created_at: 2025-04-17T23:35:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: zixlin7
body: because prompts are dynamically updated in the chat context menu, rules are just sent along with api requests
created_at: 2025-04-17T23:36:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,6 +181,7 @@ describe('AgenticChatController', () => {
                 },
             },
         }
+        testFeatures.lsp.window.showDocument = sinon.stub()
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
user: login: zixlin7
body: yes I think so
created_at: 2025-04-17T23:36:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import { FileDetails, QuickActionCommand, FileList } from '@aws/language-server-runtimes/protocol'
+import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from 'local-indexing'
+import * as path from 'path'
+import { AdditionalContentEntryAddition, TriggerContext } from './agenticChatTriggerContext'
+import { URI } from 'vscode-uri'
+import { Workspace } from '@aws/language-server-runtimes/server-interface'
+import { pathUtils } from '@aws/lsp-core'
+import {
+    additionalContentInnerContextLimit,
+    additionalContentNameLimit,
+    getUserPromptsDirectory,
+    promptFileExtension,
+} from './contextUtils'
+import { LocalProjectContextController } from '../../../shared/localProjectContextController'
+
+export class AdditionalContextProvider {
+    constructor(private readonly workspace: Workspace) {}
+
+    async collectWorkspaceRules(triggerContext: TriggerContext): Promise<string[]> {
+        const rulesFiles: string[] = []
+        const folder = triggerContext.workspaceFolder
+        if (!folder) {
+            return rulesFiles
+        }
+        const workspaceRoot = folder.uri
+            ? URI.parse(folder.uri).fsPath
+            : LocalProjectContextController.getInstance().getRootDirectory()
+        const rulesPath = path.join(workspaceRoot, '.amazonq', 'rules')
+        const folderExists = await this.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(path.join(rulesPath, entry.name))
+                }
+            }
+        }
+        return rulesFiles
+    }
+
+    getContextType(prompt: AdditionalContextPrompt): string {
+        if (prompt.filePath.endsWith(promptFileExtension)) {
+            if (pathUtils.isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) {
+                return 'rule'
+            } else if (pathUtils.isInDirectory(getUserPromptsDirectory(), prompt.filePath)) {
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts
user: login: zixlin7
body: because rules are in the workspace but prompts are in the `.aws` folder
created_at: 2025-04-17T23:37:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+import * as path from 'path'
+import { FSWatcher, watch } from 'chokidar'
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts
user: login: ege0zcan
body: Tested in a lsp server-client example and by configuring the following code on the server only (no changes on the client) I was able to get it to work 

```
connection.onInitialized(() => {
	// 1.a) build the folder URI
	const folderUri = URI.file(
		path.join(os.homedir(), '.aws', 'amazonq', 'prompts')
	).toString();

	// 1.b) register a watcher on “**/*” under that URI
	const options: DidChangeWatchedFilesRegistrationOptions = {
		watchers: [
			{
				globPattern: {
					baseUri: folderUri,
					pattern: '**/*',
				},
			} as FileSystemWatcher,
		],
	};

	// tell the client “please watch this!”
	connection.client.register(DidChangeWatchedFilesNotification.type, options);
});

connection.onDidChangeWatchedFiles((params) => {
	for (const evt of params.changes) {
		console.log(
			`File ${evt.uri} was ${['', 'created', 'changed', 'deleted'][evt.type]}`
		);
	}
});
```

created_at: 2025-04-18T16:55:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { getUserHomeDir } from '@aws/lsp-core/out/util/path'
+import * as path from 'path'
+
+export const promptFileExtension = '.prompt.md'
path: server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts
user: login: avi-alpert
body: the prompt file extension is `.md`, not `.prompt.md`
created_at: 2025-04-21T23:06:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: kmile
created_at: 2025-04-17T22:31:06Z
body: What is the actual mechanism for sending context items to the client? I don't think I saw it in `LocalProjectContextServer`.
author_association: CONTRIBUTOR
user: login: zixlin7
created_at: 2025-04-17T23:29:10Z
body: > What is the actual mechanism for sending context items to the client? I don't think I saw it in `LocalProjectContextServer`.

it is send in ContextCommandsProvider here: https://github.com/aws/language-servers/blob/98aa254f0541b7d508106b9c063370c065278475/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts#L71
author_association: CONTRIBUTOR