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",