Skip to content

Commit 8d8c5c5

Browse files
committed
feat(amazonq): Add initial hybrid chat implementation
1 parent dc43ad2 commit 8d8c5c5

File tree

14 files changed

+404
-168
lines changed

14 files changed

+404
-168
lines changed

packages/amazonq/src/app/chat/activation.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import * as vscode from 'vscode'
7-
import { ExtensionContext, window } from 'vscode'
7+
import { ExtensionContext } from 'vscode'
88
import { telemetry } from 'aws-core-vscode/telemetry'
99
import { AuthUtil, CodeWhispererSettings } from 'aws-core-vscode/codewhisperer'
1010
import { Commands, placeholder, funcUtil } from 'aws-core-vscode/shared'
@@ -16,13 +16,6 @@ export async function activate(context: ExtensionContext) {
1616

1717
registerApps(appInitContext, context)
1818

19-
const provider = new amazonq.AmazonQChatViewProvider(
20-
context,
21-
appInitContext.getWebViewToAppsMessagePublishers(),
22-
appInitContext.getAppsToWebViewMessageListener(),
23-
appInitContext.onDidChangeAmazonQVisibility
24-
)
25-
2619
await amazonq.TryChatCodeLensProvider.register(appInitContext.onDidChangeAmazonQVisibility.event)
2720

2821
const setupLsp = funcUtil.debounce(async () => {
@@ -34,11 +27,6 @@ export async function activate(context: ExtensionContext) {
3427
}, 5000)
3528

3629
context.subscriptions.push(
37-
window.registerWebviewViewProvider(amazonq.AmazonQChatViewProvider.viewType, provider, {
38-
webviewOptions: {
39-
retainContextWhenHidden: true,
40-
},
41-
}),
4230
amazonq.focusAmazonQChatWalkthrough.register(),
4331
amazonq.walkthroughInlineSuggestionsExample.register(),
4432
amazonq.walkthroughSecurityScanExample.register(),

packages/amazonq/src/extensionNode.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as vscode from 'vscode'
77
import { activateAmazonQCommon, amazonQContextPrefix, deactivateCommon } from './extension'
8-
import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq'
8+
import { DefaultAmazonQAppInitContext, AmazonQChatViewProvider } from 'aws-core-vscode/amazonq'
99
import { activate as activateQGumby } from 'aws-core-vscode/amazonqGumby'
1010
import {
1111
ExtContext,
@@ -53,6 +53,20 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
5353
}
5454

5555
if (!Experiments.instance.get('amazonqChatLSP', false)) {
56+
const appInitContext = DefaultAmazonQAppInitContext.instance
57+
const provider = new AmazonQChatViewProvider(
58+
context,
59+
appInitContext.getWebViewToAppsMessagePublishers(),
60+
appInitContext.getAppsToWebViewMessageListener(),
61+
appInitContext.onDidChangeAmazonQVisibility
62+
)
63+
context.subscriptions.push(
64+
vscode.window.registerWebviewViewProvider(AmazonQChatViewProvider.viewType, provider, {
65+
webviewOptions: {
66+
retainContextWhenHidden: true,
67+
},
68+
})
69+
)
5670
await activateCWChat(context)
5771
await activateQGumby(extContext as ExtContext)
5872
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import { AmazonQChatViewProvider } from './webviewProvider'
99
import { registerCommands } from './commands'
1010
import { registerLanguageServerEventListener, registerMessageListeners } from './messages'
1111
import { globals } from 'aws-core-vscode/shared'
12+
import { activate as registerLegacyChatListeners } from '../../app/chat/activation'
13+
import { DefaultAmazonQAppInitContext, messageDispatcher } from 'aws-core-vscode/amazonq'
1214

13-
export function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) {
15+
export async function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) {
1416
const provider = new AmazonQChatViewProvider(mynahUIPath)
1517

1618
globals.context.subscriptions.push(
@@ -29,6 +31,16 @@ export function activate(languageClient: LanguageClient, encryptionKey: Buffer,
2931
registerLanguageServerEventListener(languageClient, provider)
3032

3133
provider.onDidResolveWebview(() => {
34+
if (provider.webview) {
35+
messageDispatcher.dispatchAppsMessagesToWebView(
36+
provider.webview,
37+
DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessageListener()
38+
)
39+
}
40+
3241
registerMessageListeners(languageClient, provider, encryptionKey)
3342
})
43+
44+
// register event listeners from the legacy agent flow
45+
await registerLegacyChatListeners(globals.context)
3446
}

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

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,30 @@
44
*/
55

66
import { Commands, globals } from 'aws-core-vscode/shared'
7-
import { window } from 'vscode'
7+
// import { window } from 'vscode'
88
import { AmazonQChatViewProvider } from './webviewProvider'
99

10+
/**
11+
* TODO: Re-enable these once we can figure out which path they're going to live in
12+
* In hybrid chat mode they were being registered twice causing a registration error
13+
*/
1014
export function registerCommands(provider: AmazonQChatViewProvider) {
1115
globals.context.subscriptions.push(
12-
registerGenericCommand('aws.amazonq.explainCode', 'Explain', provider),
13-
registerGenericCommand('aws.amazonq.refactorCode', 'Refactor', provider),
14-
registerGenericCommand('aws.amazonq.fixCode', 'Fix', provider),
15-
registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', provider),
16-
Commands.register('aws.amazonq.sendToPrompt', (data) => {
17-
const triggerType = getCommandTriggerType(data)
18-
const selection = getSelectedText()
16+
// registerGenericCommand('aws.amazonq.explainCode', 'Explain', provider),
17+
// registerGenericCommand('aws.amazonq.refactorCode', 'Refactor', provider),
18+
// registerGenericCommand('aws.amazonq.fixCode', 'Fix', provider),
19+
// registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', provider),
20+
// Commands.register('aws.amazonq.sendToPrompt', (data) => {
21+
// const triggerType = getCommandTriggerType(data)
22+
// const selection = getSelectedText()
1923

20-
void focusAmazonQPanel().then(() => {
21-
void provider.webview?.postMessage({
22-
command: 'sendToPrompt',
23-
params: { selection: selection, triggerType },
24-
})
25-
})
26-
}),
24+
// void focusAmazonQPanel().then(() => {
25+
// void provider.webview?.postMessage({
26+
// command: 'sendToPrompt',
27+
// params: { selection: selection, triggerType },
28+
// })
29+
// })
30+
// }),
2731
Commands.register('aws.amazonq.openTab', () => {
2832
void focusAmazonQPanel().then(() => {
2933
void provider.webview?.postMessage({
@@ -35,36 +39,36 @@ export function registerCommands(provider: AmazonQChatViewProvider) {
3539
)
3640
}
3741

38-
function getSelectedText(): string {
39-
const editor = window.activeTextEditor
40-
if (editor) {
41-
const selection = editor.selection
42-
const selectedText = editor.document.getText(selection)
43-
return selectedText
44-
}
42+
// function getSelectedText(): string {
43+
// const editor = window.activeTextEditor
44+
// if (editor) {
45+
// const selection = editor.selection
46+
// const selectedText = editor.document.getText(selection)
47+
// return selectedText
48+
// }
4549

46-
return ' '
47-
}
50+
// return ' '
51+
// }
4852

49-
function getCommandTriggerType(data: any): string {
50-
// data is undefined when commands triggered from keybinding or command palette. Currently no
51-
// way to differentiate keybinding and command palette, so both interactions are recorded as keybinding
52-
return data === undefined ? 'hotkeys' : 'contextMenu'
53-
}
53+
// function getCommandTriggerType(data: any): string {
54+
// // data is undefined when commands triggered from keybinding or command palette. Currently no
55+
// // way to differentiate keybinding and command palette, so both interactions are recorded as keybinding
56+
// return data === undefined ? 'hotkeys' : 'contextMenu'
57+
// }
5458

55-
function registerGenericCommand(commandName: string, genericCommand: string, provider: AmazonQChatViewProvider) {
56-
return Commands.register(commandName, (data) => {
57-
const triggerType = getCommandTriggerType(data)
58-
const selection = getSelectedText()
59+
// function registerGenericCommand(commandName: string, genericCommand: string, provider: AmazonQChatViewProvider) {
60+
// return Commands.register(commandName, (data) => {
61+
// const triggerType = getCommandTriggerType(data)
62+
// const selection = getSelectedText()
5963

60-
void focusAmazonQPanel().then(() => {
61-
void provider.webview?.postMessage({
62-
command: 'genericCommand',
63-
params: { genericCommand, selection, triggerType },
64-
})
65-
})
66-
})
67-
}
64+
// void focusAmazonQPanel().then(() => {
65+
// void provider.webview?.postMessage({
66+
// command: 'genericCommand',
67+
// params: { genericCommand, selection, triggerType },
68+
// })
69+
// })
70+
// })
71+
// }
6872

6973
/**
7074
* Importing focusAmazonQPanel from aws-core-vscode/amazonq leads to several dependencies down the chain not resolving since AmazonQ chat

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import * as jose from 'jose'
2929
import { AmazonQChatViewProvider } from './webviewProvider'
3030
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
3131
import { AmazonQPromptSettings, messages } from 'aws-core-vscode/shared'
32+
import { DefaultAmazonQAppInitContext, messageDispatcher } from 'aws-core-vscode/amazonq'
3233

3334
export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) {
3435
languageClient.onDidChangeState(({ oldState, newState }) => {
@@ -60,6 +61,15 @@ export function registerMessageListeners(
6061
provider.webview?.onDidReceiveMessage(async (message) => {
6162
languageClient.info(`[VSCode Client] Received ${JSON.stringify(message)} from chat`)
6263

64+
if ((message.tabType && message.tabType !== 'cwc') || messageDispatcher.isLegacyEvent(message.command)) {
65+
// handle the mynah ui -> agent legacy flow
66+
messageDispatcher.handleWebviewEvent(
67+
message,
68+
DefaultAmazonQAppInitContext.instance.getWebViewToAppsMessagePublishers()
69+
)
70+
return
71+
}
72+
6373
switch (message.command) {
6474
case COPY_TO_CLIPBOARD:
6575
languageClient.info('[VSCode Client] Copy to clipboard event received')

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

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
} from 'vscode'
1515
import { QuickActionCommandGroup } from '@aws/mynah-ui'
1616
import * as path from 'path'
17-
import { AmazonQPromptSettings, LanguageServerResolver } from 'aws-core-vscode/shared'
17+
import { globals, isSageMaker, AmazonQPromptSettings, LanguageServerResolver } from 'aws-core-vscode/shared'
18+
import { AuthUtil, RegionProfile } from 'aws-core-vscode/codewhisperer'
19+
import { featureConfig } from 'aws-core-vscode/amazonq'
1820

1921
export class AmazonQChatViewProvider implements WebviewViewProvider {
2022
public static readonly viewType = 'aws.amazonq.AmazonQChatView'
@@ -51,25 +53,59 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
5153
this.webview = webviewView.webview
5254

5355
const lspDir = Uri.parse(LanguageServerResolver.defaultDir)
56+
const dist = Uri.joinPath(globals.context.extensionUri, 'dist')
5457
webviewView.webview.options = {
5558
enableScripts: true,
5659
enableCommandUris: true,
57-
localResourceRoots: [lspDir, Uri.parse(path.dirname(this.mynahUIPath))],
60+
localResourceRoots: [lspDir, dist],
5861
}
5962

63+
const source = 'vue/src/amazonq/webview/ui/amazonq-ui-connector-adapter.js' // Sent to dist/vue folder in webpack.
64+
const serverHostname = process.env.WEBPACK_DEVELOPER_SERVER
65+
const connectorAdapterPath =
66+
serverHostname !== undefined
67+
? Uri.parse(serverHostname)
68+
.with({ path: `/${source}` })
69+
.toString()
70+
: webviewView.webview.asWebviewUri(Uri.parse(path.join(dist.fsPath, source))).toString()
6071
const uiPath = webviewView.webview.asWebviewUri(Uri.parse(this.mynahUIPath)).toString()
61-
webviewView.webview.html = await this.getWebviewContent(uiPath)
72+
webviewView.webview.html = await this.getWebviewContent(uiPath, connectorAdapterPath)
6273

6374
this.onDidResolveWebviewEmitter.fire()
6475
}
6576

66-
private async getWebviewContent(mynahUIPath: string) {
77+
private async getWebviewContent(mynahUIPath: string, hybridChatConnector: string) {
78+
const featureConfigData = await featureConfig.getFeatureConfigs()
79+
80+
const isSM = isSageMaker('SMAI')
81+
const isSMUS = isSageMaker('SMUS')
82+
const disabledCommands = isSM ? `['/dev', '/transform', '/test', '/review', '/doc']` : '[]'
6783
const disclaimerAcknowledged = AmazonQPromptSettings.instance.isPromptEnabled('amazonQChatDisclaimer')
84+
const welcomeCount = globals.globalState.tryGet('aws.amazonq.welcomeChatShowCount', Number, 0)
85+
86+
// only show profile card when the two conditions
87+
// 1. profile count >= 2
88+
// 2. not default (fallback) which has empty arn
89+
let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile
90+
if (AuthUtil.instance.regionProfileManager.profiles.length === 1) {
91+
regionProfile = undefined
92+
}
93+
94+
const regionProfileString: string = JSON.stringify(regionProfile)
95+
96+
const entrypoint = process.env.WEBPACK_DEVELOPER_SERVER
97+
? 'http: localhost'
98+
: 'https: file+.vscode-resources.vscode-cdn.net'
99+
100+
const contentPolicy = `default-src ${entrypoint} data: blob: 'unsafe-inline';
101+
script-src ${entrypoint} filesystem: ws: wss: 'unsafe-inline';`
102+
68103
return `
69104
<!DOCTYPE html>
70105
<html lang="en">
71106
<head>
72107
<meta charset="UTF-8">
108+
<meta http-equiv="Content-Security-Policy" content="${contentPolicy}">
73109
<meta name="viewport" content="width=device-width, initial-scale=1.0">
74110
<title>Chat</title>
75111
<style>
@@ -87,9 +123,14 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
87123
</head>
88124
<body>
89125
<script type="text/javascript" src="${mynahUIPath.toString()}" defer onload="init()"></script>
126+
<script type="text/javascript" src="${hybridChatConnector.toString()}"></script>
90127
<script type="text/javascript">
91128
const init = () => {
92-
amazonQChat.createChat(acquireVsCodeApi(), { disclaimerAcknowledged: ${disclaimerAcknowledged}, quickActionCommands: ${JSON.stringify(this.quickActionCommands)}});
129+
const hybridChatConnector = new HybridChatAdapter(${(await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected'},${featureConfigData},${welcomeCount},${disclaimerAcknowledged},${regionProfileString},${disabledCommands},${isSMUS},${isSM})
130+
const connectorsConfig = {
131+
quickActionCommands: [hybridChatConnector.initialQuickActions[0]]
132+
}
133+
amazonQChat.createChat(acquireVsCodeApi(), {disclaimerAcknowledged: ${disclaimerAcknowledged}, quickActionCommands: ${JSON.stringify(this.quickActionCommands)}}, connectorsConfig, hybridChatConnector);
93134
}
94135
</script>
95136
</body>

packages/amazonq/src/lsp/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export async function startLanguageServer(
118118
}
119119

120120
if (Experiments.instance.get('amazonqChatLSP', false)) {
121-
activate(client, encryptionKey, resourcePaths.ui)
121+
await activate(client, encryptionKey, resourcePaths.ui)
122122
}
123123

124124
const refreshInterval = auth.startTokenRefreshInterval()

packages/core/src/amazonq/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export * from './lsp/config'
4848
export * as WorkspaceLspInstaller from './lsp/workspaceInstaller'
4949
export * as secondaryAuth from '../auth/secondaryAuth'
5050
export * as authConnection from '../auth/connection'
51+
export * as featureConfig from './webview/generators/featureConfig'
52+
export * as messageDispatcher from './webview/messages/messageDispatcher'
5153
import { FeatureContext } from '../shared/featureConfig'
5254

5355
/**
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { FeatureConfigProvider, FeatureContext } from '../../../shared/featureConfig'
7+
8+
export async function getFeatureConfigs(): Promise<string> {
9+
let featureConfigs = new Map<string, FeatureContext>()
10+
try {
11+
await FeatureConfigProvider.instance.fetchFeatureConfigs()
12+
featureConfigs = FeatureConfigProvider.getFeatureConfigs()
13+
} catch (error) {
14+
// eslint-disable-next-line aws-toolkits/no-console-log
15+
console.error('Error fetching feature configs:', error)
16+
}
17+
18+
// Convert featureConfigs to a string suitable for data-features
19+
return JSON.stringify(Array.from(featureConfigs.entries()))
20+
}
21+
22+
export function serialize(configs: string) {
23+
let featureDataAttributes = ''
24+
try {
25+
// Fetch and parse featureConfigs
26+
const featureConfigs = JSON.parse(configs)
27+
featureDataAttributes = featureConfigs
28+
.map((config: FeatureContext[]) => `data-feature-${config[1].name}="${config[1].variation}"`)
29+
.join(' ')
30+
} catch (error) {
31+
// eslint-disable-next-line aws-toolkits/no-console-log
32+
console.error('Error setting data-feature attribute for featureConfigs:', error)
33+
}
34+
return featureDataAttributes
35+
}

0 commit comments

Comments
 (0)