diff --git a/package-lock.json b/package-lock.json index f2c10f779b1..42869391d1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10831,18 +10831,16 @@ } }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.49", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.49.tgz", - "integrity": "sha512-GJMxZiDuV4zXGh4RBGDEobVH0CvgmFT3O3ZxYfmVGq3JvNhnKXXNYFNzWDUvF8aUUJZJGv4u4OyHitm8TSjrBg==", + "version": "0.2.58", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.58.tgz", + "integrity": "sha512-gb1oLKACFpmDKkzSdDAqMdpo63m+Kul4B/uVNNO1IFN4+wEP7zPVgmd1dLDPlLKHrxsAEQDxoYDaYVyQ+yJKqQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.9.3", "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-cognito-identity": "^3.758.0", - "@aws/language-server-runtimes-types": "^0.1.6", + "@aws/language-server-runtimes-types": "^0.1.13", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-metrics": "^1.30.1", "@opentelemetry/sdk-node": "^0.57.2", @@ -10855,7 +10853,7 @@ "aws-sdk": "^2.1692.0", "axios": "^1.8.4", "hpagent": "^1.2.0", - "jose": "^5.9.6", + "jose": "^6.0.10", "mac-ca": "^3.1.1", "rxjs": "^7.8.2", "vscode-languageserver": "^9.0.1", @@ -10867,9 +10865,9 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.10.tgz", - "integrity": "sha512-SIzwtSKG0IFrQFDREVdIIzpMOCVbcV45Nk/LufXIux523KlNSvxpQViIIQThqO4KmoyOZZ3ZWsUELdzpxc//iQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.13.tgz", + "integrity": "sha512-+FJREN6qyNcOwbu0fxAKt0QUh6x10xg2a9fLL722yVisXV0p4ElgHuXssOWhwALJrmy47DF7bRunZYNpFR9mqw==", "dev": true, "dependencies": { "vscode-languageserver-textdocument": "^1.0.12", @@ -11893,9 +11891,9 @@ } }, "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==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz", + "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==", "dev": true, "funding": { "url": "https://github.com/sponsors/panva" @@ -26795,8 +26793,8 @@ "devDependencies": { "@aws-sdk/types": "^3.13.1", "@aws/chat-client-ui-types": "^0.1.12", - "@aws/language-server-runtimes": "^0.2.49", - "@aws/language-server-runtimes-types": "^0.1.10", + "@aws/language-server-runtimes": "^0.2.58", + "@aws/language-server-runtimes-types": "^0.1.13", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", @@ -27958,16 +27956,6 @@ } } }, - "packages/core/node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.10.tgz", - "integrity": "sha512-SIzwtSKG0IFrQFDREVdIIzpMOCVbcV45Nk/LufXIux523KlNSvxpQViIIQThqO4KmoyOZZ3ZWsUELdzpxc//iQ==", - "dev": true, - "dependencies": { - "vscode-languageserver-textdocument": "^1.0.12", - "vscode-languageserver-types": "^3.17.5" - } - }, "packages/core/node_modules/@smithy/fetch-http-handler": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz", diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 444555e9950..cf8b4f55940 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -11,6 +11,7 @@ import { COPY_TO_CLIPBOARD, AuthFollowUpType, DISCLAIMER_ACKNOWLEDGED, + UiMessageResultParams, } from '@aws/chat-client-ui-types' import { ChatResult, @@ -21,30 +22,42 @@ import { QuickActionResult, QuickActionParams, insertToCursorPositionNotificationType, + ErrorCodes, + ResponseError, + openTabRequestType, + getSerializedChatRequestType, + listConversationsRequestType, + conversationClickRequestType, + ShowSaveFileDialogRequestType, + ShowSaveFileDialogParams, + LSPErrorCodes, + tabBarActionRequestType, } from '@aws/language-server-runtimes/protocol' import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' -import { Disposable, LanguageClient, Position, State, TextDocumentIdentifier } from 'vscode-languageclient' +import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' import * as jose from 'jose' import { AmazonQChatViewProvider } from './webviewProvider' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { AmazonQPromptSettings, messages } from 'aws-core-vscode/shared' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { - languageClient.onDidChangeState(({ oldState, newState }) => { - if (oldState === State.Starting && newState === State.Running) { - languageClient.info( - 'Language client received initializeResult from server:', - JSON.stringify(languageClient.initializeResult) - ) + languageClient.info( + 'Language client received initializeResult from server:', + JSON.stringify(languageClient.initializeResult) + ) - const chatOptions = languageClient.initializeResult?.awsServerCapabilities?.chatOptions + const chatOptions = languageClient.initializeResult?.awsServerCapabilities?.chatOptions - void provider.webview?.postMessage({ - command: CHAT_OPTIONS, - params: chatOptions, - }) - } + // Enable the history/export feature flags + chatOptions.history = true + chatOptions.export = true + + provider.onDidResolveWebview(() => { + void provider.webview?.postMessage({ + command: CHAT_OPTIONS, + params: chatOptions, + }) }) languageClient.onTelemetry((e) => { @@ -60,6 +73,7 @@ export function registerMessageListeners( provider.webview?.onDidReceiveMessage(async (message) => { languageClient.info(`[VSCode Client] Received ${JSON.stringify(message)} from chat`) + const webview = provider.webview switch (message.command) { case COPY_TO_CLIPBOARD: languageClient.info('[VSCode Client] Copy to clipboard event received') @@ -172,6 +186,11 @@ export function registerMessageListeners( ) break } + case listConversationsRequestType.method: + case conversationClickRequestType.method: + case tabBarActionRequestType.method: + await resolveChatResponse(message.command, message.params, languageClient, webview) + break case followUpClickNotificationType.method: if (!isValidAuthFollowUpType(message.params.followUp.type)) { languageClient.sendNotification(followUpClickNotificationType.method, message.params) @@ -184,6 +203,89 @@ export function registerMessageListeners( break } }, undefined) + + const registerHandlerWithResponseRouter = (command: string) => { + const handler = async (params: any, _: any) => { + const mapErrorType = (type: string | undefined): number => { + switch (type) { + case 'InvalidRequest': + return ErrorCodes.InvalidRequest + case 'InternalError': + return ErrorCodes.InternalError + case 'UnknownError': + default: + return ErrorCodes.UnknownErrorCode + } + } + const requestId = uuidv4() + + void provider.webview?.postMessage({ + requestId: requestId, + command: command, + params: params, + }) + const responsePromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + disposable?.dispose() + reject(new Error('Request timed out')) + }, 30000) + + const disposable = provider.webview?.onDidReceiveMessage((message: any) => { + if (message.requestId === requestId) { + clearTimeout(timeout) + disposable?.dispose() + resolve(message.params) + } + }) + }) + + const result = await responsePromise + + if (result?.success) { + return result.result + } else { + return new ResponseError( + mapErrorType(result?.error.type), + result?.error.message ?? 'No response from client' + ) + } + } + + languageClient.onRequest(command, handler) + } + + registerHandlerWithResponseRouter(openTabRequestType.method) + registerHandlerWithResponseRouter(getSerializedChatRequestType.method) + + languageClient.onRequest(ShowSaveFileDialogRequestType.method, async (params: ShowSaveFileDialogParams) => { + const filters: Record = {} + const formatMappings = [ + { format: 'markdown', key: 'Markdown', extensions: ['md'] }, + { format: 'html', key: 'HTML', extensions: ['html'] }, + ] + + for (const format of params.supportedFormats ?? []) { + const mapping = formatMappings.find((m) => m.format === format) + if (mapping) { + filters[mapping.key] = mapping.extensions + } + } + + const saveAtUri = params.defaultUri ? vscode.Uri.parse(params.defaultUri) : vscode.Uri.file('export-chat.md') + const targetUri = await vscode.window.showSaveDialog({ + filters, + defaultUri: saveAtUri, + title: 'Export', + }) + + if (!targetUri) { + return new ResponseError(LSPErrorCodes.RequestFailed, 'Export failed') + } + + return { + targetUri: targetUri.toString(), + } + }) } function isServerEvent(command: string) { @@ -258,3 +360,16 @@ async function handleCompleteResult( }) disposable.dispose() } + +async function resolveChatResponse( + requestMethod: string, + params: any, + languageClient: LanguageClient, + webview: vscode.Webview | undefined +) { + const result = await languageClient.sendRequest(requestMethod, params) + void webview?.postMessage({ + command: requestMethod, + params: result, + }) +} diff --git a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts index e6f51a1db34..67022cfa80f 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts @@ -26,6 +26,7 @@ describe('registerMessageListeners', () => { info: sandbox.stub(), error: errorStub, sendNotification: sandbox.stub(), + onRequest: sandbox.stub(), } as unknown as LanguageClient provider = { diff --git a/packages/core/package.json b/packages/core/package.json index fedea219112..8df2aa2b653 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -441,8 +441,8 @@ "devDependencies": { "@aws-sdk/types": "^3.13.1", "@aws/chat-client-ui-types": "^0.1.12", - "@aws/language-server-runtimes": "^0.2.49", - "@aws/language-server-runtimes-types": "^0.1.10", + "@aws/language-server-runtimes": "^0.2.58", + "@aws/language-server-runtimes-types": "^0.1.13", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34",