Skip to content

Commit bee5cad

Browse files
XiaoxuanLuconstewart9manodnyab
authored
feat: send pinned context button immediately with pending state (#2353)
* feat: context command pending state passed to ui, triggered when indexing complete * refactored pending flow * fix: removed extra spaces * fix: workspace pending turned back on when building index (when config is changed) * fix: log errors if inital context commands fail to send * refactor: changed pending state from boolean to string to send to ui * refactor: update from pending to disabledText * fix: send intial pending state before any other action in contextCommandsProvider * fix: send initial pending context commands onReady, instead of before * test: added unit test for onReady * test: added unit test for when indexingInProgress is changed * chore: fix the mynal ui test * chore: fix the mynal test for lack of disabled field --------- Co-authored-by: Conor Stewart <[email protected]> Co-authored-by: constewart9 <[email protected]> Co-authored-by: manodnyab <[email protected]>
1 parent 30b33a1 commit bee5cad

File tree

6 files changed

+92
-2
lines changed

6 files changed

+92
-2
lines changed

chat-client/src/client/mynahUi.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ describe('MynahUI', () => {
634634
route: ['/workspace', 'src/file1.ts'],
635635
icon: 'file',
636636
children: undefined,
637+
disabled: false,
637638
},
638639
],
639640
promptTopBarTitle: '@',
@@ -690,6 +691,7 @@ describe('MynahUI', () => {
690691
...activeEditorCommand,
691692
description: 'file:///workspace/src/active.ts',
692693
children: undefined,
694+
disabled: false,
693695
},
694696
],
695697
promptTopBarTitle: '@Pin Context',
@@ -729,7 +731,7 @@ describe('MynahUI', () => {
729731
// Verify updateStore was called with empty context items
730732
// Active editor should be removed since no textDocument was provided
731733
sinon.assert.calledWith(updateStoreSpy, tabId, {
732-
promptTopBarContextItems: [{ ...fileCommand, children: undefined }],
734+
promptTopBarContextItems: [{ ...fileCommand, children: undefined, disabled: false }],
733735
promptTopBarTitle: '@',
734736
promptTopBarButton: null,
735737
})

chat-client/src/client/mynahUi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,7 @@ ${params.message}`,
15601560
commands: toContextCommands(child.commands),
15611561
})),
15621562
icon: toMynahIcon(command.icon),
1563+
disabled: command.disabledText != null,
15631564
}))
15641565
}
15651566

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3847,9 +3847,11 @@ export class AgenticChatController implements ChatHandlers {
38473847
*/
38483848
async onReady() {
38493849
await this.restorePreviousChats()
3850+
this.#contextCommandsProvider.onReady()
38503851
try {
38513852
const localProjectContextController = await LocalProjectContextController.getInstance()
38523853
const contextItems = await localProjectContextController.getContextCommandItems()
3854+
this.#contextCommandsProvider.setFilesAndFoldersPending(false)
38533855
await this.#contextCommandsProvider.processContextCommandUpdate(contextItems)
38543856
void this.#contextCommandsProvider.maybeUpdateCodeSymbols()
38553857
} catch (error) {

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as sinon from 'sinon'
33
import { TestFeatures } from '@aws/language-server-runtimes/testing'
44
import * as chokidar from 'chokidar'
55
import { ContextCommandItem } from 'local-indexing'
6+
import { LocalProjectContextController } from '../../../shared/localProjectContextController'
67

78
describe('ContextCommandsProvider', () => {
89
let provider: ContextCommandsProvider
@@ -21,6 +22,12 @@ describe('ContextCommandsProvider', () => {
2122

2223
testFeatures.workspace.fs.exists = fsExistsStub
2324
testFeatures.workspace.fs.readdir = fsReadDirStub
25+
26+
sinon.stub(LocalProjectContextController, 'getInstance').resolves({
27+
onContextItemsUpdated: sinon.stub(),
28+
onIndexingInProgressChanged: sinon.stub(),
29+
} as any)
30+
2431
provider = new ContextCommandsProvider(
2532
testFeatures.logging,
2633
testFeatures.chat,
@@ -58,6 +65,26 @@ describe('ContextCommandsProvider', () => {
5865
})
5966
})
6067

68+
describe('onReady', () => {
69+
it('should call processContextCommandUpdate with empty array on first call', async () => {
70+
const processUpdateSpy = sinon.spy(provider, 'processContextCommandUpdate')
71+
72+
provider.onReady()
73+
74+
sinon.assert.calledOnce(processUpdateSpy)
75+
sinon.assert.calledWith(processUpdateSpy, [])
76+
})
77+
78+
it('should not call processContextCommandUpdate on subsequent calls', async () => {
79+
const processUpdateSpy = sinon.spy(provider, 'processContextCommandUpdate')
80+
81+
provider.onReady()
82+
provider.onReady()
83+
84+
sinon.assert.calledOnce(processUpdateSpy)
85+
})
86+
})
87+
6188
describe('onContextItemsUpdated', () => {
6289
it('should call processContextCommandUpdate when controller raises event', async () => {
6390
const mockContextItems: ContextCommandItem[] = [
@@ -78,4 +105,29 @@ describe('ContextCommandsProvider', () => {
78105
sinon.assert.calledWith(processUpdateSpy, mockContextItems)
79106
})
80107
})
108+
109+
describe('onIndexingInProgressChanged', () => {
110+
it('should update workspacePending and call processContextCommandUpdate when indexing status changes', async () => {
111+
let capturedCallback: ((indexingInProgress: boolean) => void) | undefined
112+
113+
const mockController = {
114+
onContextItemsUpdated: sinon.stub(),
115+
set onIndexingInProgressChanged(callback: (indexingInProgress: boolean) => void) {
116+
capturedCallback = callback
117+
},
118+
}
119+
120+
const processUpdateSpy = sinon.spy(provider, 'processContextCommandUpdate')
121+
;(LocalProjectContextController.getInstance as sinon.SinonStub).resolves(mockController as any)
122+
123+
// Set initial state to false so condition is met
124+
;(provider as any).workspacePending = false
125+
126+
await (provider as any).registerContextCommandHandler()
127+
128+
capturedCallback?.(true)
129+
130+
sinon.assert.calledWith(processUpdateSpy, [])
131+
})
132+
})
81133
})

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { activeFileCmd } from './additionalContextProvider'
1212
export class ContextCommandsProvider implements Disposable {
1313
private promptFileWatcher?: FSWatcher
1414
private cachedContextCommands?: ContextCommandItem[]
15+
private codeSymbolsPending = true
16+
private filesAndFoldersPending = true
17+
private workspacePending = true
18+
private initialStateSent = false
1519
constructor(
1620
private readonly logging: Logging,
1721
private readonly chat: Chat,
@@ -24,12 +28,27 @@ export class ContextCommandsProvider implements Disposable {
2428
)
2529
}
2630

31+
onReady() {
32+
if (!this.initialStateSent) {
33+
this.initialStateSent = true
34+
void this.processContextCommandUpdate([]).catch(e =>
35+
this.logging.error(`Failed to send initial context commands: ${e}`)
36+
)
37+
}
38+
}
39+
2740
private async registerContextCommandHandler() {
2841
try {
2942
const controller = await LocalProjectContextController.getInstance()
3043
controller.onContextItemsUpdated = async contextItems => {
3144
await this.processContextCommandUpdate(contextItems)
3245
}
46+
controller.onIndexingInProgressChanged = (indexingInProgress: boolean) => {
47+
if (this.workspacePending !== indexingInProgress) {
48+
this.workspacePending = indexingInProgress
49+
void this.processContextCommandUpdate(this.cachedContextCommands ?? [])
50+
}
51+
}
3352
} catch (e) {
3453
this.logging.warn(`Error processing context command update: ${e}`)
3554
}
@@ -105,6 +124,7 @@ export class ContextCommandsProvider implements Disposable {
105124
],
106125
description: 'Add all files in a folder to context',
107126
icon: 'folder',
127+
disabledText: this.filesAndFoldersPending ? 'pending' : undefined,
108128
}
109129

110130
const fileCmds: ContextCommand[] = [activeFileCmd]
@@ -118,6 +138,7 @@ export class ContextCommandsProvider implements Disposable {
118138
],
119139
description: 'Add a file to context',
120140
icon: 'file',
141+
disabledText: this.filesAndFoldersPending ? 'pending' : undefined,
121142
}
122143

123144
const codeCmds: ContextCommand[] = []
@@ -131,6 +152,7 @@ export class ContextCommandsProvider implements Disposable {
131152
],
132153
description: 'Add code to context',
133154
icon: 'code-block',
155+
disabledText: this.codeSymbolsPending ? 'pending' : undefined,
134156
}
135157

136158
const promptCmds: ContextCommand[] = []
@@ -152,10 +174,12 @@ export class ContextCommandsProvider implements Disposable {
152174
icon: 'image',
153175
placeholder: 'Select an image file',
154176
}
155-
const workspaceCmd = {
177+
178+
const workspaceCmd: ContextCommand = {
156179
command: '@workspace',
157180
id: '@workspace',
158181
description: 'Reference all code in workspace',
182+
disabledText: this.workspacePending ? 'pending' : undefined,
159183
}
160184
const commands = [workspaceCmd, folderCmdGroup, fileCmdGroup, codeCmdGroup, promptCmdGroup]
161185

@@ -209,11 +233,16 @@ export class ContextCommandsProvider implements Disposable {
209233
await LocalProjectContextController.getInstance()
210234
).shouldUpdateContextCommandSymbolsOnce()
211235
if (needUpdate) {
236+
this.codeSymbolsPending = false
212237
const items = await (await LocalProjectContextController.getInstance()).getContextCommandItems()
213238
await this.processContextCommandUpdate(items)
214239
}
215240
}
216241

242+
setFilesAndFoldersPending(value: boolean) {
243+
this.filesAndFoldersPending = value
244+
}
245+
217246
dispose() {
218247
void this.promptFileWatcher?.close()
219248
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export interface LocalProjectContextInitializationOptions {
5050
export class LocalProjectContextController {
5151
// Event handler for context items updated
5252
public onContextItemsUpdated: ((contextItems: ContextCommandItem[]) => Promise<void>) | undefined
53+
// Event handler for when index is being built
54+
public onIndexingInProgressChanged: ((enabled: boolean) => void) | undefined
5355
private static instance: LocalProjectContextController | undefined
5456

5557
private workspaceFolders: WorkspaceFolder[]
@@ -214,6 +216,7 @@ export class LocalProjectContextController {
214216
}
215217
try {
216218
this._isIndexingInProgress = true
219+
this.onIndexingInProgressChanged?.(this._isIndexingInProgress)
217220
if (this._vecLib) {
218221
if (!this.workspaceFolders.length) {
219222
this.log.info('skip building index because no workspace folder found')
@@ -234,6 +237,7 @@ export class LocalProjectContextController {
234237
this.log.error(`Error building index: ${error}`)
235238
} finally {
236239
this._isIndexingInProgress = false
240+
this.onIndexingInProgressChanged?.(this._isIndexingInProgress)
237241
}
238242
}
239243

0 commit comments

Comments
 (0)