Skip to content

Commit 6ff8655

Browse files
avi-alpertaws-toolkit-automationleigaolandrewyuqjpinkney-aws
authored
feat(amazonq): Use @ to add folders, files, and saved prompts as context (aws#6712)
## Problem Users are unable to specify files or folders to add as context in a conversation. Additionally, when using `@workspace`, users don't know exactly which of their local files are being sent to Amazon Q. ## Solution Users can enter `@` to add folders, files, and saved prompts as context. The response from Amazon Q now displays a "Context" list with all the items sent as context. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: aws-toolkit-automation <[email protected]> Co-authored-by: Lei Gao <[email protected]> Co-authored-by: Lei Gao <[email protected]> Co-authored-by: andrewyuq <[email protected]> Co-authored-by: Josh Pinkney <[email protected]>
1 parent ccd21b2 commit 6ff8655

File tree

31 files changed

+1334
-47
lines changed

31 files changed

+1334
-47
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q chat: Use `@` to add folders, files, and saved prompts as context"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q chat: Show list of files sent as context in chat response"
4+
}

packages/amazonq/test/e2e/amazonq/chat.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { MynahUIDataModel } from '@aws/mynah-ui'
1111
import { assertContextCommands, assertQuickActions } from './assert'
1212
import { registerAuthHook, using } from 'aws-core-vscode/test'
1313
import { loginToIdC } from './utils/setup'
14-
import { webviewConstants } from 'aws-core-vscode/amazonq'
14+
import { webviewConstants, webviewTabConstants } from 'aws-core-vscode/amazonq'
1515

1616
describe('Amazon Q Chat', function () {
1717
let framework: qTestingFramework
@@ -60,7 +60,7 @@ describe('Amazon Q Chat', function () {
6060
})
6161

6262
it('Shows placeholder', () => {
63-
assert.deepStrictEqual(store.promptInputPlaceholder, 'Ask a question or enter "/" for quick actions')
63+
assert.deepStrictEqual(store.promptInputPlaceholder, webviewTabConstants.commonTabData.placeholder)
6464
})
6565

6666
it('Sends message', async () => {

packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export function injectJSDOM() {
4040
get() {
4141
return this.textContent
4242
},
43+
set(value) {
44+
this.textContent = value
45+
},
4346
})
4447

4548
// jsdom doesn't have support for structuredClone. See https://github.com/jsdom/jsdom/issues/3363

packages/core/package.nls.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,17 @@
309309
"AWS.codewhisperer.customization.notification.new_customizations.learn_more": "Learn More",
310310
"AWS.amazonq.title": "Amazon Q",
311311
"AWS.amazonq.chat": "Chat",
312+
"AWS.amazonq.context.folders.title": "Folders",
313+
"AWS.amazonq.context.folders.description": "Add all files in a folder to context",
314+
"AWS.amazonq.context.files.title": "Files",
315+
"AWS.amazonq.context.files.description": "Add a file to context",
316+
"AWS.amazonq.context.prompts.title": "Prompts",
317+
"AWS.amazonq.context.prompts.description": "Add a saved prompt to context",
318+
"AWS.amazonq.savedPrompts.title": "Prompt name",
319+
"AWS.amazonq.savedPrompts.create": "Create",
320+
"AWS.amazonq.savedPrompts.action": "Create a new prompt",
321+
"AWS.amazonq.savedPrompts.placeholder": "Enter prompt name",
322+
"AWS.amazonq.savedPrompts.description": "Use this prompt by typing '@' followed by the prompt name.",
312323
"AWS.amazonq.chat.workspacecontext.enable.message": "Amazon Q: Workspace index is now enabled. You can disable it from Amazon Q settings.",
313324
"AWS.amazonq.security": "Code Issues",
314325
"AWS.amazonq.login": "Login",

packages/core/src/amazonq/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export { init as testChatAppInit } from '../amazonqTest/app'
2626
export { init as docChatAppInit } from '../amazonqDoc/app'
2727
export { amazonQHelpUrl } from '../shared/constants'
2828
export * as webviewConstants from './webview/ui/texts/constants'
29+
export * as webviewTabConstants from './webview/ui/tabs/constants'
2930
export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/statusBarMenu'
3031
export { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands'
3132
export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens'

packages/core/src/amazonq/lsp/lspClient.ts

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ import {
2929
QueryRepomapIndexRequestType,
3030
GetRepomapIndexJSONRequestType,
3131
Usage,
32+
GetContextCommandItemsRequestType,
33+
ContextCommandItem,
34+
GetIndexSequenceNumberRequestType,
35+
GetContextCommandPromptRequestType,
36+
AdditionalContextPrompt,
3237
} from './types'
3338
import { Writable } from 'stream'
3439
import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings'
3540
import { fs } from '../../shared/fs/fs'
3641
import { getLogger } from '../../shared/logger/logger'
3742
import globals from '../../shared/extensionGlobals'
43+
import { waitUntil } from '../../shared/utilities/timeoutUtils'
3844

3945
const localize = nls.loadMessageBundle()
4046

@@ -168,6 +174,66 @@ export class LspClient {
168174
throw e
169175
}
170176
}
177+
178+
async getContextCommandItems(): Promise<ContextCommandItem[]> {
179+
try {
180+
const workspaceFolders = vscode.workspace.workspaceFolders || []
181+
const request = JSON.stringify({
182+
workspaceFolders: workspaceFolders.map((it) => it.uri.fsPath),
183+
})
184+
const resp: any = await this.client?.sendRequest(
185+
GetContextCommandItemsRequestType,
186+
await this.encrypt(request)
187+
)
188+
return resp
189+
} catch (e) {
190+
getLogger().error(`LspClient: getContextCommandItems error: ${e}`)
191+
throw e
192+
}
193+
}
194+
195+
async getContextCommandPrompt(contextCommandItems: ContextCommandItem[]): Promise<AdditionalContextPrompt[]> {
196+
try {
197+
const request = JSON.stringify({
198+
contextCommands: contextCommandItems,
199+
})
200+
const resp: any = await this.client?.sendRequest(
201+
GetContextCommandPromptRequestType,
202+
await this.encrypt(request)
203+
)
204+
return resp
205+
} catch (e) {
206+
getLogger().error(`LspClient: getContextCommandPrompt error: ${e}`)
207+
throw e
208+
}
209+
}
210+
211+
async getIndexSequenceNumber(): Promise<number> {
212+
try {
213+
const request = JSON.stringify({})
214+
const resp: any = await this.client?.sendRequest(
215+
GetIndexSequenceNumberRequestType,
216+
await this.encrypt(request)
217+
)
218+
return resp
219+
} catch (e) {
220+
getLogger().error(`LspClient: getIndexSequenceNumber error: ${e}`)
221+
throw e
222+
}
223+
}
224+
225+
async waitUntilReady() {
226+
return waitUntil(
227+
async () => {
228+
if (this.client === undefined) {
229+
return false
230+
}
231+
await this.client.onReady()
232+
return true
233+
},
234+
{ interval: 500, timeout: 60_000 * 3, truthy: true }
235+
)
236+
}
171237
}
172238
/**
173239
* Activates the language server, this will start LSP server running over IPC protocol.
@@ -249,6 +315,37 @@ export async function activate(extensionContext: ExtensionContext) {
249315

250316
let savedDocument: vscode.Uri | undefined = undefined
251317

318+
const onAdd = async (filePaths: string[]) => {
319+
const indexSeqNum = await LspClient.instance.getIndexSequenceNumber()
320+
await LspClient.instance.updateIndex(filePaths, 'add')
321+
await waitUntil(
322+
async () => {
323+
const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber()
324+
if (newIndexSeqNum > indexSeqNum) {
325+
await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`)
326+
return true
327+
}
328+
return false
329+
},
330+
{ interval: 500, timeout: 5_000, truthy: true }
331+
)
332+
}
333+
const onRemove = async (filePaths: string[]) => {
334+
const indexSeqNum = await LspClient.instance.getIndexSequenceNumber()
335+
await LspClient.instance.updateIndex(filePaths, 'remove')
336+
await waitUntil(
337+
async () => {
338+
const newIndexSeqNum = await LspClient.instance.getIndexSequenceNumber()
339+
if (newIndexSeqNum > indexSeqNum) {
340+
await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`)
341+
return true
342+
}
343+
return false
344+
},
345+
{ interval: 500, timeout: 5_000, truthy: true }
346+
)
347+
}
348+
252349
toDispose.push(
253350
vscode.workspace.onDidSaveTextDocument((document) => {
254351
if (document.uri.scheme !== 'file') {
@@ -260,18 +357,23 @@ export async function activate(extensionContext: ExtensionContext) {
260357
if (savedDocument && editor && editor.document.uri.fsPath !== savedDocument.fsPath) {
261358
void LspClient.instance.updateIndex([savedDocument.fsPath], 'update')
262359
}
360+
// user created a new empty file using File -> New File
361+
// these events will not be captured by vscode.workspace.onDidCreateFiles
362+
// because it was created by File Explorer(Win) or Finder(MacOS)
363+
// TODO: consider using a high performance fs watcher
364+
if (editor?.document.getText().length === 0) {
365+
void onAdd([editor.document.uri.fsPath])
366+
}
263367
}),
264-
vscode.workspace.onDidCreateFiles((e) => {
265-
void LspClient.instance.updateIndex(
266-
e.files.map((f) => f.fsPath),
267-
'add'
268-
)
368+
vscode.workspace.onDidCreateFiles(async (e) => {
369+
await onAdd(e.files.map((f) => f.fsPath))
269370
}),
270-
vscode.workspace.onDidDeleteFiles((e) => {
271-
void LspClient.instance.updateIndex(
272-
e.files.map((f) => f.fsPath),
273-
'remove'
274-
)
371+
vscode.workspace.onDidDeleteFiles(async (e) => {
372+
await onRemove(e.files.map((f) => f.fsPath))
373+
}),
374+
vscode.workspace.onDidRenameFiles(async (e) => {
375+
await onRemove(e.files.map((f) => f.oldUri.fsPath))
376+
await onAdd(e.files.map((f) => f.newUri.fsPath))
275377
})
276378
)
277379

packages/core/src/amazonq/lsp/lspController.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import fetch from 'node-fetch'
1313
import request from '../../shared/request'
1414
import { LspClient } from './lspClient'
1515
import AdmZip from 'adm-zip'
16-
import { RelevantTextDocument } from '@amzn/codewhisperer-streaming'
1716
import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities'
1817
import { activate as activateLsp } from './lspClient'
1918
import { telemetry } from '../../shared/telemetry/telemetry'
@@ -24,13 +23,16 @@ import { ToolkitError } from '../../shared/errors'
2423
import { isWeb } from '../../shared/extensionGlobals'
2524
import { getUserAgent } from '../../shared/telemetry/util'
2625
import { isAmazonInternalOs } from '../../shared/vscode/env'
26+
import { RelevantTextDocumentAddition } from '../../codewhispererChat/controllers/chat/model'
2727

2828
export interface Chunk {
2929
readonly filePath: string
3030
readonly content: string
3131
readonly context?: string
3232
readonly relativePath?: string
3333
readonly programmingLanguage?: string
34+
readonly startLine?: number
35+
readonly endLine?: number
3436
}
3537

3638
export interface Content {
@@ -60,7 +62,7 @@ export interface Manifest {
6062
}
6163
const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json'
6264
// this LSP client in Q extension is only going to work with these LSP server versions
63-
const supportedLspServerVersions = ['0.1.35']
65+
const supportedLspServerVersions = ['0.1.42']
6466

6567
const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node'
6668

@@ -279,9 +281,9 @@ export class LspController {
279281
}
280282
}
281283

282-
async query(s: string): Promise<RelevantTextDocument[]> {
284+
async query(s: string): Promise<RelevantTextDocumentAddition[]> {
283285
const chunks: Chunk[] | undefined = await LspClient.instance.queryVectorIndex(s)
284-
const resp: RelevantTextDocument[] = []
286+
const resp: RelevantTextDocumentAddition[] = []
285287
if (chunks) {
286288
for (const chunk of chunks) {
287289
const text = chunk.context ? chunk.context : chunk.content
@@ -292,11 +294,15 @@ export class LspController {
292294
programmingLanguage: {
293295
languageName: chunk.programmingLanguage,
294296
},
297+
startLine: chunk.startLine ?? -1,
298+
endLine: chunk.endLine ?? -1,
295299
})
296300
} else {
297301
resp.push({
298302
text: text,
299303
relativeFilePath: chunk.relativePath ? chunk.relativePath : path.basename(chunk.filePath),
304+
startLine: chunk.startLine ?? -1,
305+
endLine: chunk.endLine ?? -1,
300306
})
301307
}
302308
}
@@ -393,6 +399,7 @@ export class LspController {
393399
try {
394400
await activateLsp(context)
395401
getLogger().info('LspController: LSP activated')
402+
await vscode.commands.executeCommand(`aws.amazonq.updateContextCommandItems`)
396403
void LspController.instance.buildIndex(buildIndexConfig)
397404
// log the LSP server CPU and Memory usage per 30 minutes.
398405
globals.clock.setInterval(

packages/core/src/amazonq/lsp/types.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,43 @@ export type GetRepomapIndexJSONRequest = string
7676
export const GetRepomapIndexJSONRequestType: RequestType<GetRepomapIndexJSONRequest, any, any> = new RequestType(
7777
'lsp/getRepomapIndexJSON'
7878
)
79+
80+
export type GetContextCommandItemsRequestPayload = { workspaceFolders: string[] }
81+
export type GetContextCommandItemsRequest = string
82+
export const GetContextCommandItemsRequestType: RequestType<GetContextCommandItemsRequest, any, any> = new RequestType(
83+
'lsp/getContextCommandItems'
84+
)
85+
86+
export type GetIndexSequenceNumberRequest = string
87+
export const GetIndexSequenceNumberRequestType: RequestType<GetRepomapIndexJSONRequest, any, any> = new RequestType(
88+
'lsp/getIndexSequenceNumber'
89+
)
90+
91+
export type ContextCommandItemType = 'file' | 'folder'
92+
93+
export interface ContextCommandItem {
94+
workspaceFolder: string
95+
type: ContextCommandItemType
96+
relativePath: string
97+
}
98+
99+
export type GetContextCommandPromptRequestPayload = {
100+
contextCommands: {
101+
workspaceFolder: string
102+
type: 'file' | 'folder'
103+
relativePath: string
104+
}[]
105+
}
106+
export type GetContextCommandPromptRequest = string
107+
export const GetContextCommandPromptRequestType: RequestType<GetContextCommandPromptRequest, any, any> =
108+
new RequestType('lsp/getContextCommandPrompt')
109+
110+
export interface AdditionalContextPrompt {
111+
content: string
112+
name: string
113+
description: string
114+
startLine: number
115+
endLine: number
116+
filePath: string
117+
relativePath: string
118+
}

packages/core/src/amazonq/webview/messages/messageDispatcher.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export function dispatchWebViewMessagesToApps(
3636
})
3737
performance.clearMarks(amazonqMark.uiReady)
3838
performance.clearMarks(amazonqMark.open)
39+
// let cwcController know the ui is ready
40+
webViewToAppsMessagePublishers.get('cwc')?.publish(msg)
3941
return
4042
}
4143
case 'start-chat-message-telemetry': {

0 commit comments

Comments
 (0)