Skip to content

Commit 4a279d8

Browse files
authored
feat(amazonq): add experiment for basic e2e flow of inline chat through Flare. (#7235)
## Problem This is the initial set of work required to get inline chat running through flare. ## Solution - **add a feature flag for inline chat**: this allows testing of the two implementations side-by-side by flipping the feature flag. - **move general utils out of chat**: stuff like encryption and editorState can all be reused. - **render full diff response from inline chat**: this does not include progress updates from the language server. ## Testing and Verification https://github.com/user-attachments/assets/0dff58b7-40f7-487d-9f9e-d58610201041 ## Future Work / Next Steps - ensure telemetry is still being emitted. - add tests for new flow. (there aren't any for the existing one) - handle partial events from the language server. ## Known Bugs - selecting part of a line will cause the text to insert mid-line  - running inline-chat without a selection causes the entire file to be copied (This is in JB, Eclipse Prod, but IMO it makes the feature unusable). --- - 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.
1 parent 059a140 commit 4a279d8

File tree

10 files changed

+161
-48
lines changed

10 files changed

+161
-48
lines changed

packages/amazonq/src/extensionNode.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { DevOptions } from 'aws-core-vscode/dev'
2525
import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth'
2626
import api from './api'
2727
import { activate as activateCWChat } from './app/chat/activation'
28-
import { activate as activateInlineChat } from './inlineChat/activation'
2928
import { beta } from 'aws-core-vscode/dev'
3029
import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications'
3130
import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer'
@@ -73,7 +72,6 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
7372
}
7473
activateAgents()
7574
await activateTransformationHub(extContext as ExtContext)
76-
activateInlineChat(context)
7775

7876
const authProvider = new CommonAuthViewProvider(
7977
context,

packages/amazonq/src/inlineChat/activation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import * as vscode from 'vscode'
66
import { InlineChatController } from './controller/inlineChatController'
77
import { registerInlineCommands } from './command/registerInlineCommands'
8+
import { LanguageClient } from 'vscode-languageclient'
89

9-
export function activate(context: vscode.ExtensionContext) {
10-
const inlineChatController = new InlineChatController(context)
10+
export function activate(context: vscode.ExtensionContext, client: LanguageClient, encryptionKey: Buffer) {
11+
const inlineChatController = new InlineChatController(context, client, encryptionKey)
1112
registerInlineCommands(context, inlineChatController)
1213
}

packages/amazonq/src/inlineChat/controller/inlineChatController.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CodelensProvider } from '../codeLenses/codeLenseProvider'
1414
import { PromptMessage, ReferenceLogController } from 'aws-core-vscode/codewhispererChat'
1515
import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer'
1616
import { UserWrittenCodeTracker } from 'aws-core-vscode/codewhisperer'
17+
import { LanguageClient } from 'vscode-languageclient'
1718
import {
1819
codicon,
1920
getIcon,
@@ -23,6 +24,7 @@ import {
2324
Timeout,
2425
textDocumentUtil,
2526
isSageMaker,
27+
Experiments,
2628
} from 'aws-core-vscode/shared'
2729
import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController'
2830

@@ -33,14 +35,18 @@ export class InlineChatController {
3335
private readonly codeLenseProvider: CodelensProvider
3436
private readonly referenceLogController = new ReferenceLogController()
3537
private readonly inlineLineAnnotationController: InlineLineAnnotationController
38+
private readonly computeDiffAndRenderOnEditor: (query: string) => Promise<void>
3639
private userQuery: string | undefined
3740
private listeners: vscode.Disposable[] = []
3841

39-
constructor(context: vscode.ExtensionContext) {
40-
this.inlineChatProvider = new InlineChatProvider()
42+
constructor(context: vscode.ExtensionContext, client: LanguageClient, encryptionKey: Buffer) {
43+
this.inlineChatProvider = new InlineChatProvider(client, encryptionKey)
4144
this.inlineChatProvider.onErrorOccured(() => this.handleError())
4245
this.codeLenseProvider = new CodelensProvider(context)
4346
this.inlineLineAnnotationController = new InlineLineAnnotationController(context)
47+
this.computeDiffAndRenderOnEditor = Experiments.instance.get('amazonqLSPInlineChat', false)
48+
? this.computeDiffAndRenderOnEditorLSP.bind(this)
49+
: this.computeDiffAndRenderOnEditorLocal.bind(this)
4450
}
4551

4652
public async createTask(
@@ -206,7 +212,7 @@ export class InlineChatController {
206212
await textDocumentUtil.addEofNewline(editor)
207213
this.task = await this.createTask(query, editor.document, editor.selection)
208214
await this.inlineLineAnnotationController.disable(editor)
209-
await this.computeDiffAndRenderOnEditor(query, editor.document).catch(async (err) => {
215+
await this.computeDiffAndRenderOnEditor(query).catch(async (err) => {
210216
getLogger().error('computeDiffAndRenderOnEditor error: %s', (err as Error)?.message)
211217
if (err instanceof Error) {
212218
void vscode.window.showErrorMessage(`Amazon Q: ${err.message}`)
@@ -218,7 +224,46 @@ export class InlineChatController {
218224
})
219225
}
220226

221-
private async computeDiffAndRenderOnEditor(query: string, document: vscode.TextDocument) {
227+
private async computeDiffAndRenderOnEditorLSP(query: string) {
228+
if (!this.task) {
229+
return
230+
}
231+
232+
await this.updateTaskAndLenses(this.task, TaskState.InProgress)
233+
getLogger().info(`inline chat query:\n${query}`)
234+
const uuid = randomUUID()
235+
const message: PromptMessage = {
236+
message: query,
237+
messageId: uuid,
238+
command: undefined,
239+
userIntent: undefined,
240+
tabID: uuid,
241+
}
242+
243+
const response = await this.inlineChatProvider.processPromptMessageLSP(message)
244+
245+
// TODO: add tests for this case.
246+
if (!response.body) {
247+
getLogger().warn('Empty body in inline chat response')
248+
await this.handleError()
249+
return
250+
}
251+
252+
// Update inline diff view
253+
const textDiff = computeDiff(response.body, this.task, false)
254+
const decorations = computeDecorations(this.task)
255+
this.task.decorations = decorations
256+
await this.applyDiff(this.task, textDiff ?? [])
257+
this.decorator.applyDecorations(this.task)
258+
259+
// Update Codelenses
260+
await this.updateTaskAndLenses(this.task, TaskState.WaitingForDecision)
261+
await setContext('amazonq.inline.codelensShortcutEnabled', true)
262+
this.undoListener(this.task)
263+
}
264+
265+
// TODO: remove this implementation in favor of LSP
266+
private async computeDiffAndRenderOnEditorLocal(query: string) {
222267
if (!this.task) {
223268
return
224269
}

packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
CodeWhispererStreamingServiceException,
99
GenerateAssistantResponseCommandOutput,
1010
} from '@amzn/codewhisperer-streaming'
11+
import { LanguageClient } from 'vscode-languageclient'
12+
import { inlineChatRequestType } from '@aws/language-server-runtimes/protocol'
1113
import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer'
1214
import {
1315
ChatSessionStorage,
@@ -25,6 +27,9 @@ import { codeWhispererClient } from 'aws-core-vscode/codewhisperer'
2527
import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer'
2628
import { InlineTask } from '../controller/inlineTask'
2729
import { extractAuthFollowUp } from 'aws-core-vscode/amazonq'
30+
import { InlineChatParams, InlineChatResult } from '@aws/language-server-runtimes-types'
31+
import { decodeRequest, encryptRequest } from '../../lsp/encryption'
32+
import { getCursorState } from '../../lsp/utils'
2833

2934
export class InlineChatProvider {
3035
private readonly editorContextExtractor: EditorContextExtractor
@@ -34,13 +39,52 @@ export class InlineChatProvider {
3439
private errorEmitter = new vscode.EventEmitter<void>()
3540
public onErrorOccured = this.errorEmitter.event
3641

37-
public constructor() {
42+
public constructor(
43+
private readonly client: LanguageClient,
44+
private readonly encryptionKey: Buffer
45+
) {
3846
this.editorContextExtractor = new EditorContextExtractor()
3947
this.userIntentRecognizer = new UserIntentRecognizer()
4048
this.sessionStorage = new ChatSessionStorage()
4149
this.triggerEventsStorage = new TriggerEventsStorage()
4250
}
4351

52+
private getCurrentEditorParams(prompt: string): InlineChatParams {
53+
const editor = vscode.window.activeTextEditor
54+
if (!editor) {
55+
throw new ToolkitError('No active editor')
56+
}
57+
58+
const documentUri = editor.document.uri.toString()
59+
const cursorState = getCursorState(editor.selections)
60+
return {
61+
prompt: {
62+
prompt,
63+
},
64+
cursorState,
65+
textDocument: {
66+
uri: documentUri,
67+
},
68+
}
69+
}
70+
71+
public async processPromptMessageLSP(message: PromptMessage): Promise<InlineChatResult> {
72+
// TODO: handle partial responses.
73+
getLogger().info('Making inline chat request with message %O', message)
74+
const params = this.getCurrentEditorParams(message.message ?? '')
75+
const inlineChatRequest = await encryptRequest<InlineChatParams>(params, this.encryptionKey)
76+
const response = await this.client.sendRequest(inlineChatRequestType.method, inlineChatRequest)
77+
const decryptedMessage =
78+
typeof response === 'string' && this.encryptionKey
79+
? await decodeRequest(response, this.encryptionKey)
80+
: response
81+
const result: InlineChatResult = decryptedMessage as InlineChatResult
82+
this.client.info(`Logging response for inline chat ${JSON.stringify(decryptedMessage)}`)
83+
84+
return result
85+
}
86+
87+
// TODO: remove in favor of LSP implementation.
4488
public async processPromptMessage(message: PromptMessage) {
4589
return this.editorContextExtractor
4690
.extractContextForTrigger('ChatMessage')

packages/amazonq/src/lsp/chat/messages.ts

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ import {
5555
import { v4 as uuidv4 } from 'uuid'
5656
import * as vscode from 'vscode'
5757
import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient'
58-
import * as jose from 'jose'
5958
import { AmazonQChatViewProvider } from './webviewProvider'
6059
import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer'
6160
import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl } from 'aws-core-vscode/shared'
@@ -68,6 +67,8 @@ import {
6867
} from 'aws-core-vscode/amazonq'
6968
import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry'
7069
import { isValidResponseError } from './error'
70+
import { decodeRequest, encryptRequest } from '../encryption'
71+
import { getCursorState } from '../utils'
7172

7273
export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) {
7374
languageClient.info(
@@ -99,21 +100,6 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie
99100
})
100101
}
101102

102-
function getCursorState(selection: readonly vscode.Selection[]) {
103-
return selection.map((s) => ({
104-
range: {
105-
start: {
106-
line: s.start.line,
107-
character: s.start.character,
108-
},
109-
end: {
110-
line: s.end.line,
111-
character: s.end.character,
112-
},
113-
},
114-
}))
115-
}
116-
117103
export function registerMessageListeners(
118104
languageClient: LanguageClient,
119105
provider: AmazonQChatViewProvider,
@@ -487,29 +473,6 @@ function isServerEvent(command: string) {
487473
return command.startsWith('aws/chat/') || command === 'telemetry/event'
488474
}
489475

490-
async function encryptRequest<T>(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> {
491-
const payload = new TextEncoder().encode(JSON.stringify(params))
492-
493-
const encryptedMessage = await new jose.CompactEncrypt(payload)
494-
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
495-
.encrypt(encryptionKey)
496-
497-
return { message: encryptedMessage }
498-
}
499-
500-
async function decodeRequest<T>(request: string, key: Buffer): Promise<T> {
501-
const result = await jose.jwtDecrypt(request, key, {
502-
clockTolerance: 60, // Allow up to 60 seconds to account for clock differences
503-
contentEncryptionAlgorithms: ['A256GCM'],
504-
keyManagementAlgorithms: ['dir'],
505-
})
506-
507-
if (!result.payload) {
508-
throw new Error('JWT payload not found')
509-
}
510-
return result.payload as T
511-
}
512-
513476
/**
514477
* Decodes partial chat responses from the language server before sending them to mynah UI
515478
*/

packages/amazonq/src/lsp/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { processUtils } from 'aws-core-vscode/shared'
3939
import { activate } from './chat/activation'
4040
import { AmazonQResourcePaths } from './lspInstaller'
4141
import { ConfigSection, isValidConfigSection, toAmazonQLSPLogLevel } from './config'
42+
import { activate as activateInlineChat } from '../inlineChat/activation'
4243

4344
const localize = nls.loadMessageBundle()
4445
const logger = getLogger('amazonqLsp.lspClient')
@@ -182,6 +183,8 @@ export async function startLanguageServer(
182183
await activate(client, encryptionKey, resourcePaths.ui)
183184
}
184185

186+
activateInlineChat(extensionContext, client, encryptionKey)
187+
185188
const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond)
186189

187190
const sendProfileToLsp = async () => {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as jose from 'jose'
6+
7+
export async function encryptRequest<T>(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> {
8+
const payload = new TextEncoder().encode(JSON.stringify(params))
9+
10+
const encryptedMessage = await new jose.CompactEncrypt(payload)
11+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
12+
.encrypt(encryptionKey)
13+
14+
return { message: encryptedMessage }
15+
}
16+
17+
export async function decodeRequest<T>(request: string, key: Buffer): Promise<T> {
18+
const result = await jose.jwtDecrypt(request, key, {
19+
clockTolerance: 60, // Allow up to 60 seconds to account for clock differences
20+
contentEncryptionAlgorithms: ['A256GCM'],
21+
keyManagementAlgorithms: ['dir'],
22+
})
23+
24+
if (!result.payload) {
25+
throw new Error('JWT payload not found')
26+
}
27+
return result.payload as T
28+
}

packages/amazonq/src/lsp/utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import { CursorState } from '@aws/language-server-runtimes-types'
7+
8+
/**
9+
* Convert from vscode selection type to the general CursorState expected by the AmazonQLSP.
10+
* @param selection
11+
* @returns
12+
*/
13+
export function getCursorState(selection: readonly vscode.Selection[]): CursorState[] {
14+
return selection.map((s) => ({
15+
range: {
16+
start: {
17+
line: s.start.line,
18+
character: s.start.character,
19+
},
20+
end: {
21+
line: s.end.line,
22+
character: s.end.character,
23+
},
24+
},
25+
}))
26+
}

packages/core/src/shared/settings-toolkit.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const toolkitSettings = {
4444
"jsonResourceModification": {},
4545
"amazonqLSP": {},
4646
"amazonqLSPInline": {},
47+
"amazonqLSPInlineChat": {},
4748
"amazonqChatLSP": {}
4849
},
4950
"aws.resources.enabledResources": {},

packages/toolkit/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@
255255
"type": "boolean",
256256
"default": false
257257
},
258+
"amazonqLSPInlineChat": {
259+
"type": "boolean",
260+
"default": false
261+
},
258262
"amazonqChatLSP": {
259263
"type": "boolean",
260264
"default": true

0 commit comments

Comments
 (0)