Skip to content

Commit 0ad24d4

Browse files
authored
fix(amazonq): move context command provider to agentic chat controller (#999)
1 parent b2033d7 commit 0ad24d4

File tree

8 files changed

+56
-55
lines changed

8 files changed

+56
-55
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import * as path from 'path'
7+
import * as chokidar from 'chokidar'
78
import {
89
ChatResponseStream,
910
CodeWhispererStreaming,
@@ -39,6 +40,7 @@ import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/A
3940
import { TabBarController } from './tabBarController'
4041
import { getUserPromptsDirectory } from './context/contextUtils'
4142
import { AdditionalContextProvider } from './context/addtionalContextProvider'
43+
import { ContextCommandsProvider } from './context/contextCommandsProvider'
4244

4345
describe('AgenticChatController', () => {
4446
const mockTabId = 'tab-1'
@@ -120,6 +122,11 @@ describe('AgenticChatController', () => {
120122
const setCredentials = setCredentialsForAmazonQTokenServiceManagerFactory(() => testFeatures)
121123

122124
beforeEach(() => {
125+
sinon.stub(chokidar, 'watch').returns({
126+
on: sinon.stub(),
127+
close: sinon.stub(),
128+
} as unknown as chokidar.FSWatcher)
129+
123130
sendMessageStub = sinon.stub(CodeWhispererStreaming.prototype, 'sendMessage').callsFake(() => {
124131
return new Promise(resolve =>
125132
setTimeout(() => {
@@ -190,6 +197,7 @@ describe('AgenticChatController', () => {
190197
emitConversationMetricStub = sinon.stub(ChatTelemetryController.prototype, 'emitConversationMetric')
191198

192199
disposeStub = sinon.stub(ChatSessionService.prototype, 'dispose')
200+
sinon.stub(ContextCommandsProvider.prototype, 'maybeUpdateCodeSymbols').resolves()
193201

194202
AmazonQTokenServiceManager.resetInstance()
195203

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ import {
8383
} from './context/agenticChatTriggerContext'
8484
import { AdditionalContextProvider } from './context/addtionalContextProvider'
8585
import { getNewPromptFilePath } from './context/contextUtils'
86+
import { ContextCommandsProvider } from './context/contextCommandsProvider'
87+
import { LocalProjectContextController } from '../../shared/localProjectContextController'
8688

8789
type ChatHandlers = Omit<
8890
LspHandlers<Chat>,
@@ -107,6 +109,7 @@ export class AgenticChatController implements ChatHandlers {
107109
#tabBarController: TabBarController
108110
#chatHistoryDb: ChatDatabase
109111
#additionalContextProvider: AdditionalContextProvider
112+
#contextCommandsProvider: ContextCommandsProvider
110113

111114
constructor(
112115
chatSessionManagementService: ChatSessionManagementService,
@@ -123,6 +126,11 @@ export class AgenticChatController implements ChatHandlers {
123126
this.#chatHistoryDb = new ChatDatabase(features)
124127
this.#tabBarController = new TabBarController(features, this.#chatHistoryDb)
125128
this.#additionalContextProvider = new AdditionalContextProvider(features.workspace)
129+
this.#contextCommandsProvider = new ContextCommandsProvider(
130+
this.#features.logging,
131+
this.#features.chat,
132+
this.#features.workspace
133+
)
126134
}
127135

128136
async onCreatePrompt(params: CreatePromptParams): Promise<void> {
@@ -140,6 +148,7 @@ export class AgenticChatController implements ChatHandlers {
140148
this.#chatSessionManagementService.dispose()
141149
this.#telemetryController.dispose()
142150
this.#chatHistoryDb.close()
151+
this.#contextCommandsProvider?.dispose()
143152
}
144153

145154
async onListConversations(params: ListConversationsParams) {
@@ -727,6 +736,9 @@ export class AgenticChatController implements ChatHandlers {
727736

728737
async onReady() {
729738
await this.#tabBarController.loadChats()
739+
const contextItems = await (await LocalProjectContextController.getInstance()).getContextCommandItems()
740+
await this.#contextCommandsProvider.processContextCommandUpdate(contextItems)
741+
void this.#contextCommandsProvider.maybeUpdateCodeSymbols()
730742
}
731743

732744
onSendFeedback({ tabId, feedbackPayload }: FeedbackParams) {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('AdditionalContextProvider', () => {
2424
testFeatures.workspace.fs.readdir = fsReadDirStub
2525
getContextCommandPromptStub = sinon.stub()
2626
provider = new AdditionalContextProvider(testFeatures.workspace)
27-
localProjectContextControllerInstanceStub = sinon.stub(LocalProjectContextController, 'getInstance').returns({
27+
localProjectContextControllerInstanceStub = sinon.stub(LocalProjectContextController, 'getInstance').resolves({
2828
getContextCommandPrompt: getContextCommandPromptStub,
2929
} as unknown as LocalProjectContextController)
3030
})

server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/addtionalContextProvider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ export class AdditionalContextProvider {
8181
return []
8282
}
8383

84-
const prompts =
85-
await LocalProjectContextController.getInstance().getContextCommandPrompt(additionalContextCommands)
84+
const prompts = await (
85+
await LocalProjectContextController.getInstance()
86+
).getContextCommandPrompt(additionalContextCommands)
8687

8788
const contextEntry: AdditionalContentEntryAddition[] = []
8889
for (const prompt of prompts.slice(0, 20)) {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Disposable } from 'vscode-languageclient/node'
55
import { Chat, Logging, Workspace } from '@aws/language-server-runtimes/server-interface'
66
import { getUserPromptsDirectory, promptFileExtension } from './contextUtils'
77
import { ContextCommandItem } from 'local-indexing'
8+
import { LocalProjectContextController } from '../../../shared/localProjectContextController'
89

910
export class ContextCommandsProvider implements Disposable {
1011
private promptFileWatcher?: FSWatcher
@@ -164,6 +165,16 @@ export class ContextCommandsProvider implements Disposable {
164165
return allCommands
165166
}
166167

168+
async maybeUpdateCodeSymbols() {
169+
const needUpdate = await (
170+
await LocalProjectContextController.getInstance()
171+
).shouldUpdateContextCommandSymbolsOnce()
172+
if (needUpdate) {
173+
const items = await (await LocalProjectContextController.getInstance()).getContextCommandItems()
174+
await this.processContextCommandUpdate(items)
175+
}
176+
}
177+
167178
dispose() {
168179
void this.promptFileWatcher?.close()
169180
}

server/aws-lsp-codewhisperer/src/language-server/localProjectContext/localProjectContextServer.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ export const LocalProjectContextServer = (): Server => features => {
2121
localProjectContextController = new LocalProjectContextController(
2222
params.clientInfo?.name ?? 'unknown',
2323
params.workspaceFolders ?? [],
24-
logging,
25-
chat,
26-
workspace
24+
logging
2725
)
2826

2927
const supportedFilePatterns = Object.keys(languageByExtension).map(ext => `**/*${ext}`)

server/aws-lsp-codewhisperer/src/shared/localProjectContextController.test.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { Dirent } from 'fs'
66
import * as path from 'path'
77
import { URI } from 'vscode-uri'
88
import { TestFeatures } from '@aws/language-server-runtimes/testing'
9-
import * as chokidar from 'chokidar'
109

1110
class LoggingMock {
1211
public error: SinonStub
@@ -41,10 +40,6 @@ describe('LocalProjectContextController', () => {
4140
name: 'workspace1',
4241
},
4342
]
44-
stub(chokidar, 'watch').returns({
45-
on: stub(),
46-
close: stub(),
47-
} as unknown as chokidar.FSWatcher)
4843

4944
vectorLibMock = {
5045
start: stub().resolves({
@@ -70,14 +65,7 @@ describe('LocalProjectContextController', () => {
7065
}
7166
})
7267

73-
controller = new LocalProjectContextController(
74-
'testClient',
75-
mockWorkspaceFolders,
76-
logging as any,
77-
testFeatures.chat,
78-
testFeatures.workspace
79-
)
80-
stub(controller, 'maybeUpdateCodeSymbols').resolves()
68+
controller = new LocalProjectContextController('testClient', mockWorkspaceFolders, logging as any)
8169
})
8270

8371
afterEach(() => {
@@ -113,9 +101,7 @@ describe('LocalProjectContextController', () => {
113101
const uninitializedController = new LocalProjectContextController(
114102
'testClient',
115103
mockWorkspaceFolders,
116-
logging as any,
117-
testFeatures.chat,
118-
testFeatures.workspace
104+
logging as any
119105
)
120106

121107
const result = await uninitializedController.queryVectorIndex({ query: 'test' })
@@ -146,9 +132,7 @@ describe('LocalProjectContextController', () => {
146132
const uninitializedController = new LocalProjectContextController(
147133
'testClient',
148134
mockWorkspaceFolders,
149-
logging as any,
150-
testFeatures.chat,
151-
testFeatures.workspace
135+
logging as any
152136
)
153137

154138
const result = await uninitializedController.queryInlineProjectContext({
@@ -191,9 +175,7 @@ describe('LocalProjectContextController', () => {
191175
const uninitializedController = new LocalProjectContextController(
192176
'testClient',
193177
mockWorkspaceFolders,
194-
logging as any,
195-
testFeatures.chat,
196-
testFeatures.workspace
178+
logging as any
197179
)
198180

199181
await uninitializedController.updateIndex(['test.java'], 'add')

server/aws-lsp-codewhisperer/src/shared/localProjectContextController.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {
1414
} from 'local-indexing'
1515
import { URI } from 'vscode-uri'
1616
import { waitUntil } from '@aws/lsp-core/out/util/timeoutUtils'
17-
import { ContextCommandsProvider } from '../language-server/agenticChat/context/contextCommandsProvider'
1817

1918
import * as fs from 'fs'
2019
import * as path from 'path'
@@ -51,7 +50,6 @@ export class LocalProjectContextController {
5150
private workspaceFolders: WorkspaceFolder[]
5251
private _vecLib?: VectorLibAPI
5352
private _contextCommandSymbolsUpdated = false
54-
private readonly contextCommandsProvider: ContextCommandsProvider
5553
private readonly clientName: string
5654
private readonly log: Logging
5755

@@ -67,24 +65,28 @@ export class LocalProjectContextController {
6765
private readonly DEFAULT_MAX_FILE_SIZE_MB = 10
6866
private readonly MB_TO_BYTES = 1024 * 1024
6967

70-
constructor(
71-
clientName: string,
72-
workspaceFolders: WorkspaceFolder[],
73-
logging: Logging,
74-
chat: Chat,
75-
workspace: Workspace
76-
) {
68+
constructor(clientName: string, workspaceFolders: WorkspaceFolder[], logging: Logging) {
7769
this.workspaceFolders = workspaceFolders
7870
this.clientName = clientName
7971
this.log = logging
80-
this.contextCommandsProvider = new ContextCommandsProvider(logging, chat, workspace)
8172
}
8273

83-
public static getInstance() {
84-
if (!this.instance) {
85-
throw new Error('LocalProjectContextController not initialized')
74+
public static async getInstance(): Promise<LocalProjectContextController> {
75+
try {
76+
await waitUntil(async () => this.instance, {
77+
interval: 1000,
78+
timeout: 60_000,
79+
truthy: true,
80+
})
81+
82+
if (!this.instance) {
83+
throw new Error('LocalProjectContextController initialization timeout after 60 seconds')
84+
}
85+
86+
return this.instance
87+
} catch (error) {
88+
throw new Error(`Failed to get LocalProjectContextController instance: ${error}`)
8689
}
87-
return this.instance
8890
}
8991

9092
public async init({
@@ -111,10 +113,6 @@ export class LocalProjectContextController {
111113
this._vecLib = await vecLib.start(LIBRARY_DIR, this.clientName, this.indexCacheDirPath)
112114
await this.buildIndex()
113115
LocalProjectContextController.instance = this
114-
115-
const contextItems = await this.getContextCommandItems()
116-
await this.contextCommandsProvider.processContextCommandUpdate(contextItems)
117-
void this.maybeUpdateCodeSymbols()
118116
} else {
119117
this.log.warn(`Vector library could not be imported from: ${libraryPath}`)
120118
}
@@ -128,7 +126,6 @@ export class LocalProjectContextController {
128126
await this._vecLib?.clear?.()
129127
this._vecLib = undefined
130128
}
131-
this.contextCommandsProvider?.dispose()
132129
}
133130

134131
public async updateIndex(filePaths: string[], operation: UpdateMode): Promise<void> {
@@ -269,14 +266,6 @@ export class LocalProjectContextController {
269266
}
270267
}
271268

272-
async maybeUpdateCodeSymbols() {
273-
const needUpdate = await LocalProjectContextController.getInstance().shouldUpdateContextCommandSymbolsOnce()
274-
if (needUpdate) {
275-
const items = await this.getContextCommandItems()
276-
await this.contextCommandsProvider.processContextCommandUpdate(items)
277-
}
278-
}
279-
280269
private fileMeetsFileSizeConstraints(filePath: string, sizeConstraints: SizeConstraints): boolean {
281270
let fileSize
282271

0 commit comments

Comments
 (0)