diff --git a/package-lock.json b/package-lock.json index 8bf434177b1..b67c9a5aa3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5163,6 +5163,15 @@ "node": ">=14.14" } }, + "node_modules/@aws/chat-client-ui-types": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.0.8.tgz", + "integrity": "sha512-aU8r0FaCKIhMiTWvr/yuWYZmVWPgE2vBAPsVcafhlu7ucubiH/+YodqDw+0Owk0R0kxxZDdjdZghPZSyy0G84A==", + "dev": true, + "dependencies": { + "@aws/language-server-runtimes-types": "^0.0.7" + } + }, "node_modules/@aws/fully-qualified-names": { "version": "2.1.4", "dev": true, @@ -5171,6 +5180,93 @@ "web-tree-sitter": "^0.20.8" } }, + "node_modules/@aws/language-server-runtimes": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.27.tgz", + "integrity": "sha512-qWog7upRVc09xLcuL0HladoxO3JbkgdtgkI/RUWRDcr6YB8hBvmSCADGWjUGbOyvK4CpaXqHIr883PAqnosoXg==", + "dev": true, + "dependencies": { + "@aws/language-server-runtimes-types": "^0.0.7", + "jose": "^5.9.6", + "rxjs": "^7.8.1", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/language-server-runtimes-types": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.0.7.tgz", + "integrity": "sha512-P83YkgWITcUGHaZvYFI0N487nWErgRpejALKNm/xs8jEcHooDfjigOpliN8TgzfF9BGvGeQnnAzIG16UBXc9ig==", + "dev": true, + "dependencies": { + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5" + } + }, + "node_modules/@aws/language-server-runtimes-types/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true + }, + "node_modules/@aws/language-server-runtimes/node_modules/jose": { + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@aws/language-server-runtimes/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@aws/language-server-runtimes/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws/language-server-runtimes/node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/@aws/language-server-runtimes/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/@aws/language-server-runtimes/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true + }, "node_modules/@aws/mynah-ui": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.21.0.tgz", @@ -18861,8 +18957,9 @@ "license": "MIT" }, "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.8", - "license": "MIT" + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" }, "node_modules/vscode-languageserver-types": { "version": "3.17.3", @@ -20086,7 +20183,9 @@ }, "devDependencies": { "@aws-sdk/types": "^3.13.1", + "@aws/chat-client-ui-types": "^0.0.8", "@aws/fully-qualified-names": "^2.1.4", + "@aws/language-server-runtimes": "^0.2.27", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", diff --git a/packages/amazonq/src/chat/activation.ts b/packages/amazonq/src/chat/activation.ts new file mode 100644 index 00000000000..8a7bb8c9736 --- /dev/null +++ b/packages/amazonq/src/chat/activation.ts @@ -0,0 +1,94 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CancellationToken, + Uri, + Webview, + WebviewView, + WebviewViewProvider, + WebviewViewResolveContext, + window, +} from 'vscode' +import { LanguageClient } from 'vscode-languageclient' +import { globals } from 'aws-core-vscode/shared' +import { handle } from './handler' + +export class AmazonQChatViewProvider implements WebviewViewProvider { + public static readonly viewType = 'aws.AmazonQChatView' + + constructor(private readonly client: LanguageClient) {} + + public async resolveWebviewView( + webviewView: WebviewView, + context: WebviewViewResolveContext, + _token: CancellationToken + ) { + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [Uri.joinPath(globals.context.extensionUri, 'resources', 'qdeveloperclient')], + } + + webviewView.webview.html = this.getWebviewContent(webviewView.webview, globals.context.extensionUri) + handle(this.client, webviewView.webview) + } + + private getWebviewContent(webView: Webview, extensionUri: Uri) { + return ` + + + + + + Chat UI + ${this.generateCss()} + + + ${this.generateJS(webView, extensionUri)} + + ` + } + + private generateCss() { + return ` + ` + } + + private generateJS(webView: Webview, extensionUri: Uri): string { + const assetsPath = Uri.joinPath(extensionUri) + const chatUri = Uri.joinPath(assetsPath, 'resources', 'qdeveloperclient', 'amazonq-ui.js') + + const entrypoint = webView.asWebviewUri(chatUri) + + return ` + + + ` + } +} + +export function registerChat(client: LanguageClient) { + const panel = new AmazonQChatViewProvider(client) + window.registerWebviewViewProvider(AmazonQChatViewProvider.viewType, panel, { + webviewOptions: { + retainContextWhenHidden: true, + }, + }) +} diff --git a/packages/amazonq/src/chat/handler.ts b/packages/amazonq/src/chat/handler.ts new file mode 100644 index 00000000000..eedf976016d --- /dev/null +++ b/packages/amazonq/src/chat/handler.ts @@ -0,0 +1,260 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { + isValidAuthFollowUpType, + INSERT_TO_CURSOR_POSITION, + AUTH_FOLLOW_UP_CLICKED, + CHAT_OPTIONS, + COPY_TO_CLIPBOARD, +} from '@aws/chat-client-ui-types' +import { + ChatResult, + chatRequestType, + ChatParams, + followUpClickNotificationType, + quickActionRequestType, + QuickActionResult, + QuickActionParams, + insertToCursorPositionNotificationType, +} from '@aws/language-server-runtimes/protocol' +import { v4 as uuidv4 } from 'uuid' +import { Webview, window } from 'vscode' +import { Disposable, LanguageClient, Position, State, TextDocumentIdentifier } from 'vscode-languageclient' +import * as jose from 'jose' +import { encryptionKey } from '../lsp/auth' +import { Commands } from 'aws-core-vscode/shared' + +export function handle(client: LanguageClient, webview: Webview) { + // Listen for Initialize handshake from LSP server to register quick actions dynamically + client.onDidChangeState(({ oldState, newState }) => { + if (oldState === State.Starting && newState === State.Running) { + client.info( + 'Language client received initializeResult from server:', + JSON.stringify(client.initializeResult) + ) + + const chatOptions = client.initializeResult?.awsServerCapabilities?.chatOptions + + void webview.postMessage({ + command: CHAT_OPTIONS, + params: chatOptions, + }) + } + }) + + client.onTelemetry((e) => { + client.info(`[VSCode Client] Received telemetry event from server ${JSON.stringify(e)}`) + }) + + webview.onDidReceiveMessage(async (message) => { + client.info(`[VSCode Client] Received ${JSON.stringify(message)} from chat`) + + switch (message.command) { + case COPY_TO_CLIPBOARD: + client.info('[VSCode Client] Copy to clipboard event received') + break + case INSERT_TO_CURSOR_POSITION: { + const editor = window.activeTextEditor + let textDocument: TextDocumentIdentifier | undefined = undefined + let cursorPosition: Position | undefined = undefined + if (editor) { + cursorPosition = editor.selection.active + textDocument = { uri: editor.document.uri.toString() } + } + + client.sendNotification(insertToCursorPositionNotificationType.method, { + ...message.params, + cursorPosition, + textDocument, + }) + break + } + case AUTH_FOLLOW_UP_CLICKED: + client.info('[VSCode Client] AuthFollowUp clicked') + break + case chatRequestType.method: { + const partialResultToken = uuidv4() + const chatDisposable = client.onProgress(chatRequestType, partialResultToken, (partialResult) => + handlePartialResult(partialResult, encryptionKey, message.params.tabId, webview) + ) + + const editor = + window.activeTextEditor || + window.visibleTextEditors.find((editor) => editor.document.languageId !== 'Log') + if (editor) { + message.params.cursorPosition = [editor.selection.active] + message.params.textDocument = { uri: editor.document.uri.toString() } + } + + const chatRequest = await encryptRequest(message.params, encryptionKey) + const chatResult = (await client.sendRequest(chatRequestType.method, { + ...chatRequest, + partialResultToken, + })) as string | ChatResult + void handleCompleteResult( + chatResult, + encryptionKey, + message.params.tabId, + chatDisposable, + webview + ) + break + } + case quickActionRequestType.method: { + const quickActionPartialResultToken = uuidv4() + const quickActionDisposable = client.onProgress( + quickActionRequestType, + quickActionPartialResultToken, + (partialResult) => + handlePartialResult( + partialResult, + encryptionKey, + message.params.tabId, + webview + ) + ) + + const quickActionRequest = await encryptRequest(message.params, encryptionKey) + const quickActionResult = (await client.sendRequest(quickActionRequestType.method, { + ...quickActionRequest, + partialResultToken: quickActionPartialResultToken, + })) as string | ChatResult + void handleCompleteResult( + quickActionResult, + encryptionKey, + message.params.tabId, + quickActionDisposable, + webview + ) + break + } + case followUpClickNotificationType.method: + if (!isValidAuthFollowUpType(message.params.followUp.type)) { + client.sendNotification(followUpClickNotificationType.method, message.params) + } + break + default: + if (isServerEvent(message.command)) { + client.sendNotification(message.command, message.params) + } + break + } + }, undefined) + + registerGenericCommand('aws.amazonq.explainCode', 'Explain', webview) + registerGenericCommand('aws.amazonq.refactorCode', 'Refactor', webview) + registerGenericCommand('aws.amazonq.fixCode', 'Fix', webview) + registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', webview) + + Commands.register('aws.amazonq.sendToPrompt', (data) => { + const triggerType = getCommandTriggerType(data) + const selection = getSelectedText() + + void webview.postMessage({ + command: 'sendToPrompt', + params: { selection: selection, triggerType }, + }) + }) +} + +function getSelectedText(): string { + const editor = window.activeTextEditor + if (editor) { + const selection = editor.selection + const selectedText = editor.document.getText(selection) + return selectedText + } + + return ' ' +} + +function getCommandTriggerType(data: any): string { + // data is undefined when commands triggered from keybinding or command palette. Currently no + // way to differentiate keybinding and command palette, so both interactions are recorded as keybinding + return data === undefined ? 'hotkeys' : 'contextMenu' +} + +function registerGenericCommand(commandName: string, genericCommand: string, webview?: Webview) { + Commands.register(commandName, (data) => { + const triggerType = getCommandTriggerType(data) + const selection = getSelectedText() + + void webview?.postMessage({ + command: 'genericCommand', + params: { genericCommand, selection, triggerType }, + }) + }) +} + +function isServerEvent(command: string) { + return command.startsWith('aws/chat/') || command === 'telemetry/event' +} + +// Encrypt the provided request if encryption key exists otherwise do nothing +async function encryptRequest(params: T, encryptionKey: Buffer | undefined): Promise<{ message: string } | T> { + if (!encryptionKey) { + return params + } + + const payload = new TextEncoder().encode(JSON.stringify(params)) + + const encryptedMessage = await new jose.CompactEncrypt(payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + return { message: encryptedMessage } +} + +async function decodeRequest(request: string, key: Buffer): Promise { + const result = await jose.jwtDecrypt(request, key, { + clockTolerance: 60, // Allow up to 60 seconds to account for clock differences + contentEncryptionAlgorithms: ['A256GCM'], + keyManagementAlgorithms: ['dir'], + }) + + if (!result.payload) { + throw new Error('JWT payload not found') + } + return result.payload as T +} + +async function handlePartialResult( + partialResult: string | T, + encryptionKey: Buffer | undefined, + tabId: string, + webview: Webview +) { + const decryptedMessage = + typeof partialResult === 'string' && encryptionKey + ? await decodeRequest(partialResult, encryptionKey) + : (partialResult as T) + + if (decryptedMessage.body) { + void webview?.postMessage({ + command: chatRequestType.method, + params: decryptedMessage, + isPartialResult: true, + tabId: tabId, + }) + } +} + +async function handleCompleteResult( + result: string | T, + encryptionKey: Buffer | undefined, + tabId: string, + disposable: Disposable, + webview: Webview +) { + const decryptedMessage = + typeof result === 'string' && encryptionKey ? await decodeRequest(result, encryptionKey) : result + + void webview?.postMessage({ + command: chatRequestType.method, + params: decryptedMessage, + tabId: tabId, + }) + disposable.dispose() +} diff --git a/packages/amazonq/src/inline/completion.ts b/packages/amazonq/src/inline/completion.ts new file mode 100644 index 00000000000..c4ff07b9648 --- /dev/null +++ b/packages/amazonq/src/inline/completion.ts @@ -0,0 +1,117 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CancellationToken, + InlineCompletionContext, + InlineCompletionItem, + InlineCompletionItemProvider, + InlineCompletionList, + Position, + TextDocument, + commands, + languages, +} from 'vscode' +import { LanguageClient } from 'vscode-languageclient' +import { + InlineCompletionItemWithReferences, + InlineCompletionListWithReferences, + InlineCompletionWithReferencesParams, + inlineCompletionWithReferencesRequestType, + logInlineCompletionSessionResultsNotificationType, + LogInlineCompletionSessionResultsParams, +} from '@aws/language-server-runtimes/protocol' + +export const CodewhispererInlineCompletionLanguages = [ + { scheme: 'file', language: 'typescript' }, + { scheme: 'file', language: 'javascript' }, + { scheme: 'file', language: 'json' }, + { scheme: 'file', language: 'yaml' }, + { scheme: 'file', language: 'java' }, + { scheme: 'file', language: 'go' }, + { scheme: 'file', language: 'php' }, + { scheme: 'file', language: 'rust' }, + { scheme: 'file', language: 'kotlin' }, + { scheme: 'file', language: 'terraform' }, + { scheme: 'file', language: 'ruby' }, + { scheme: 'file', language: 'shellscript' }, + { scheme: 'file', language: 'dart' }, + { scheme: 'file', language: 'lua' }, + { scheme: 'file', language: 'powershell' }, + { scheme: 'file', language: 'r' }, + { scheme: 'file', language: 'swift' }, + { scheme: 'file', language: 'systemverilog' }, + { scheme: 'file', language: 'scala' }, + { scheme: 'file', language: 'vue' }, + { scheme: 'file', language: 'csharp' }, +] + +export function registerInlineCompletion(languageClient: LanguageClient) { + const inlineCompletionProvider = new AmazonQInlineCompletionItemProvider(languageClient) + languages.registerInlineCompletionItemProvider(CodewhispererInlineCompletionLanguages, inlineCompletionProvider) + + const onInlineAcceptance = async ( + sessionId: string, + itemId: string, + requestStartTime: number, + firstCompletionDisplayLatency?: number + ) => { + const params: LogInlineCompletionSessionResultsParams = { + sessionId: sessionId, + completionSessionResult: { + [itemId]: { + seen: true, + accepted: true, + discarded: false, + }, + }, + totalSessionDisplayTime: Date.now() - requestStartTime, + firstCompletionDisplayLatency: firstCompletionDisplayLatency, + } + languageClient.sendNotification(logInlineCompletionSessionResultsNotificationType as any, params) + } + commands.registerCommand('aws.sample-vscode-ext-amazonq.accept', onInlineAcceptance) +} + +export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider { + constructor(private readonly languageClient: LanguageClient) {} + + async provideInlineCompletionItems( + document: TextDocument, + position: Position, + context: InlineCompletionContext, + token: CancellationToken + ): Promise { + const requestStartTime = Date.now() + const request: InlineCompletionWithReferencesParams = { + textDocument: { + uri: document.uri.toString(), + }, + position, + context, + } + + const response = await this.languageClient.sendRequest( + inlineCompletionWithReferencesRequestType as any, + request, + token + ) + + const list: InlineCompletionListWithReferences = response as InlineCompletionListWithReferences + this.languageClient.info(`Client: Received ${list.items.length} suggestions`) + const firstCompletionDisplayLatency = Date.now() - requestStartTime + + // Add completion session tracking and attach onAcceptance command to each item to record used decision + list.items.forEach((item: InlineCompletionItemWithReferences) => { + item.command = { + command: 'aws.sample-vscode-ext-amazonq.accept', + title: 'On acceptance', + arguments: [list.sessionId, item.itemId, requestStartTime, firstCompletionDisplayLatency], + } + }) + + return list as InlineCompletionList + } +} diff --git a/packages/amazonq/src/lsp/activation.ts b/packages/amazonq/src/lsp/activation.ts index 44465f8659a..c1aa78c5854 100644 --- a/packages/amazonq/src/lsp/activation.ts +++ b/packages/amazonq/src/lsp/activation.ts @@ -4,18 +4,13 @@ */ import vscode from 'vscode' +import path from 'path' import { AmazonQLSPDownloader } from './download' +import { startLanguageServer } from './client' export async function activate(ctx: vscode.ExtensionContext): Promise { const serverPath = ctx.asAbsolutePath('resources/qdeveloperserver') const clientPath = ctx.asAbsolutePath('resources/qdeveloperclient') await new AmazonQLSPDownloader(serverPath, clientPath).tryInstallLsp() - - /** - * at this point the language server should be installed and available - * at serverPath and mynah ui should be available and serveable at - * clientPath - * - * TODO: actually hook up the language server - */ + await startLanguageServer(ctx, path.join(serverPath, 'aws-lsp-codewhisperer.js')) } diff --git a/packages/amazonq/src/lsp/auth.ts b/packages/amazonq/src/lsp/auth.ts new file mode 100644 index 00000000000..70753e75c6b --- /dev/null +++ b/packages/amazonq/src/lsp/auth.ts @@ -0,0 +1,98 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ConnectionMetadata, + NotificationType, + RequestType, + ResponseMessage, +} from '@aws/language-server-runtimes/protocol' +import * as jose from 'jose' +import * as crypto from 'crypto' +import { LanguageClient } from 'vscode-languageclient' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' +import { Writable } from 'stream' + +export const encryptionKey = crypto.randomBytes(32) + +/** + * Sends a json payload to the language server, who is waiting to know what the encryption key is. + * Code reference: https://github.com/aws/language-servers/blob/7da212185a5da75a72ce49a1a7982983f438651a/client/vscode/src/credentialsActivation.ts#L77 + */ +export function writeEncryptionInit(stream: Writable): void { + const request = { + version: '1.0', + mode: 'JWT', + key: encryptionKey.toString('base64'), + } + stream.write(JSON.stringify(request)) + stream.write('\n') +} + +/** + * Request for custom notifications that Update Credentials and tokens. + * See core\aws-lsp-core\src\credentials\updateCredentialsRequest.ts for details + */ +export interface UpdateCredentialsRequest { + /** + * Encrypted token (JWT or PASETO) + * The token's contents differ whether IAM or Bearer token is sent + */ + data: string + /** + * Used by the runtime based language servers. + * Signals that this client will encrypt its credentials payloads. + */ + encrypted: boolean +} + +export const notificationTypes = { + updateBearerToken: new RequestType( + 'aws/credentials/token/update' + ), + deleteBearerToken: new NotificationType('aws/credentials/token/delete'), + getConnectionMetadata: new RequestType( + 'aws/credentials/getConnectionMetadata' + ), +} + +/** + * Facade over our VSCode Auth that does crud operations on the language server auth + */ +export class AmazonQLspAuth { + constructor(private readonly client: LanguageClient) {} + + async init() { + const activeConnection = AuthUtil.instance.auth.activeConnection + if (activeConnection?.type === 'sso') { + // send the token to the language server + const token = await AuthUtil.instance.getBearerToken() + await this.updateBearerToken(token) + } + } + + private async updateBearerToken(token: string) { + const request = await this.createUpdateCredentialsRequest({ + token, + }) + + await this.client.sendRequest(notificationTypes.updateBearerToken.method, request) + + this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`) + } + + private async createUpdateCredentialsRequest(data: any) { + const payload = new TextEncoder().encode(JSON.stringify({ data })) + + const jwt = await new jose.CompactEncrypt(payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + return { + data: jwt, + encrypted: true, + } + } +} diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts new file mode 100644 index 00000000000..bf29bc6dddd --- /dev/null +++ b/packages/amazonq/src/lsp/client.ts @@ -0,0 +1,110 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import vscode, { env, version } from 'vscode' +import * as nls from 'vscode-nls' +import * as cp from 'child_process' +import * as crypto from 'crypto' +import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient' +import { registerInlineCompletion } from '../inline/completion' +import { AmazonQLspAuth, notificationTypes, writeEncryptionInit } from './auth' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' +import { ConnectionMetadata } from '@aws/language-server-runtimes/protocol' +import { registerChat } from '../chat/activation' + +const localize = nls.loadMessageBundle() + +export function startLanguageServer(extensionContext: vscode.ExtensionContext, serverPath: string) { + const toDispose = extensionContext.subscriptions + + // The debug options for the server + // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging + const debugOptions = { + execArgv: [ + '--nolazy', + '--preserve-symlinks', + '--stdio', + '--pre-init-encryption', + '--set-credentials-encryption-key', + ], + } + + // If the extension is launch in debug mode the debug server options are use + // Otherwise the run options are used + let serverOptions: ServerOptions = { + run: { module: serverPath, transport: TransportKind.ipc }, + debug: { module: serverPath, transport: TransportKind.ipc, options: debugOptions }, + } + + const child = cp.spawn('node', [serverPath, ...debugOptions.execArgv]) + writeEncryptionInit(child.stdin) + + serverOptions = () => Promise.resolve(child) + + const documentSelector = [{ scheme: 'file', language: '*' }] + + // Options to control the language client + const clientOptions: LanguageClientOptions = { + // Register the server for json documents + documentSelector, + initializationOptions: { + aws: { + clientInfo: { + name: env.appName, + version: version, + extension: { + name: `AWS IDE Extensions for VSCode`, // TODO change this to C9/Amazon + version: '0.0.1', + }, + clientId: crypto.randomUUID(), + }, + awsClientCapabilities: { + window: { + notifications: true, + }, + }, + }, + credentials: { + providesBearerToken: true, + }, + }, + } + + const client = new LanguageClient( + 'amazonq', + localize('amazonq.server.name', 'Amazon Q Language Server'), + serverOptions, + clientOptions + ) + + const disposable = client.start() + toDispose.push(disposable) + + const auth = new AmazonQLspAuth(client) + + return client.onReady().then(async () => { + await auth.init() + registerInlineCompletion(client) + registerChat(client) + + // Request handler for when the server wants to know about the clients auth connnection + client.onRequest(notificationTypes.getConnectionMetadata.method, () => { + return { + sso: { + startUrl: AuthUtil.instance.auth.startUrl, + }, + } + }) + + toDispose.push( + AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { + await auth.init() + }), + AuthUtil.instance.auth.onDidDeleteConnection(async () => { + client.sendNotification(notificationTypes.deleteBearerToken.method) + }) + ) + }) +} diff --git a/packages/core/package.json b/packages/core/package.json index 3a3fc432a9d..db401e0383c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -438,6 +438,8 @@ "serveVue": "Local server for Vue.js code for development purposes. Provides faster iteration when updating Vue files" }, "devDependencies": { + "@aws/language-server-runtimes": "^0.2.27", + "@aws/chat-client-ui-types": "^0.0.8", "@aws-sdk/types": "^3.13.1", "@aws/fully-qualified-names": "^2.1.4", "@cspotcode/source-map-support": "^0.8.1",