packages/amazonq/src/lsp/chat/webviewProvider.ts (140 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { EventEmitter, CancellationToken, WebviewView, WebviewViewProvider, WebviewViewResolveContext, Uri, Webview, } from 'vscode' import * as path from 'path' import { globals, isSageMaker, AmazonQPromptSettings, LanguageServerResolver, amazonqMark, } from 'aws-core-vscode/shared' import { AuthUtil, RegionProfile } from 'aws-core-vscode/codewhisperer' import { featureConfig } from 'aws-core-vscode/amazonq' import { getAmazonQLspConfig } from '../config' export class AmazonQChatViewProvider implements WebviewViewProvider { public static readonly viewType = 'aws.amazonq.AmazonQChatView' private readonly onDidResolveWebviewEmitter = new EventEmitter<void>() public readonly onDidResolveWebview = this.onDidResolveWebviewEmitter.event webviewView?: WebviewView webview?: Webview connectorAdapterPath?: string uiPath?: string constructor(private readonly mynahUIPath: string) {} public async resolveWebviewView( webviewView: WebviewView, context: WebviewViewResolveContext, _token: CancellationToken ) { const lspDir = Uri.file(LanguageServerResolver.defaultDir()) const dist = Uri.joinPath(globals.context.extensionUri, 'dist') const resourcesRoots = [lspDir, dist] /** * if the mynah chat client is defined, then make sure to add it to the resource roots, otherwise * it will 401 when trying to load */ const mynahUIPath = getAmazonQLspConfig().ui if (process.env.WEBPACK_DEVELOPER_SERVER && mynahUIPath) { const dir = path.dirname(mynahUIPath) resourcesRoots.push(Uri.file(dir)) } webviewView.webview.options = { enableScripts: true, enableCommandUris: true, localResourceRoots: resourcesRoots, } const source = 'vue/src/amazonq/webview/ui/amazonq-ui-connector-adapter.js' // Sent to dist/vue folder in webpack. const serverHostname = process.env.WEBPACK_DEVELOPER_SERVER this.connectorAdapterPath = serverHostname !== undefined ? `${serverHostname}/${source}` : webviewView.webview.asWebviewUri(Uri.joinPath(dist, source)).toString() this.uiPath = webviewView.webview.asWebviewUri(Uri.file(this.mynahUIPath)).toString() webviewView.webview.html = await this.getWebviewContent() this.webviewView = webviewView this.webview = this.webviewView.webview this.onDidResolveWebviewEmitter.fire() performance.mark(amazonqMark.open) } private async getWebviewContent() { const featureConfigData = await featureConfig.getFeatureConfigs() const isSM = isSageMaker('SMAI') const isSMUS = isSageMaker('SMUS') const disabledCommands = isSM ? `['/dev', '/transform', '/test', '/review', '/doc']` : '[]' const disclaimerAcknowledged = !AmazonQPromptSettings.instance.isPromptEnabled('amazonQChatDisclaimer') const pairProgrammingAcknowledged = !AmazonQPromptSettings.instance.isPromptEnabled('amazonQChatPairProgramming') const welcomeCount = globals.globalState.tryGet('aws.amazonq.welcomeChatShowCount', Number, 0) // only show profile card when the two conditions // 1. profile count >= 2 // 2. not default (fallback) which has empty arn let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile if (AuthUtil.instance.regionProfileManager.profiles.length === 1) { regionProfile = undefined } const regionProfileString: string = JSON.stringify(regionProfile) const entrypoint = process.env.WEBPACK_DEVELOPER_SERVER ? 'http://localhost:8080' : 'https://file+.vscode-resource.vscode-cdn.net' const contentPolicy = `default-src ${entrypoint} data: blob: 'unsafe-inline'; script-src ${entrypoint} filesystem: file: vscode-resource: https: ws: wss: 'unsafe-inline';` return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" content="${contentPolicy}"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chat</title> <style> body, html { background-color: var(--mynah-color-bg); color: var(--mynah-color-text-default); height: 100%; width: 100%; overflow: hidden; margin: 0; padding: 0; } </style> </head> <body> <script type="text/javascript" src="${this.uiPath?.toString()}" defer onload="init()"></script> <script type="text/javascript" src="${this.connectorAdapterPath?.toString()}"></script> <script type="text/javascript"> let qChat = undefined const init = () => { const vscodeApi = acquireVsCodeApi() const hybridChatConnector = new HybridChatAdapter(${(await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected'},${featureConfigData},${welcomeCount},${disclaimerAcknowledged},${regionProfileString},${disabledCommands},${isSMUS},${isSM},vscodeApi.postMessage) const commands = [hybridChatConnector.initialQuickActions[0]] qChat = amazonQChat.createChat(vscodeApi, {disclaimerAcknowledged: ${disclaimerAcknowledged}, pairProgrammingAcknowledged: ${pairProgrammingAcknowledged}, agenticMode: true, quickActionCommands: commands}, hybridChatConnector, ${JSON.stringify(featureConfigData)}); } window.addEventListener('message', (event) => { /** * special handler that "simulates" reloading the webview when a profile changes. * required because chat-client relies on initializedResult from the lsp that * are only sent once * * References: * closing tabs: https://github.com/aws/mynah-ui/blob/de736b52f369ba885cd19f33ac86c6f57b4a3134/docs/USAGE.md#removing-a-tab-programmatically- * opening tabs: https://github.com/aws/aws-toolkit-vscode/blob/c22efa03e73b241564c8051c35761eb8620edb83/packages/amazonq/test/e2e/amazonq/framework/framework.ts#L98 */ if (event.data.command === 'reload' && qChat) { // close all previous tabs Object.keys(qChat.getAllTabs()).forEach(tabId => qChat.removeTab(tabId, qChat.lastEventId)); // open a new "initial" tab ;(document.querySelectorAll('.mynah-nav-tabs-wrapper > button.mynah-button')[0]).click() } }); </script> </body> </html>` } async refreshWebview() { if (this.webview) { // post a message to the webview telling it to reload void this.webview?.postMessage({ command: 'reload', }) } } }