Skip to content

Commit c6269e7

Browse files
committed
merge
2 parents ea8582b + 2befa25 commit c6269e7

File tree

30 files changed

+903
-223
lines changed

30 files changed

+903
-223
lines changed

package-lock.json

Lines changed: 13 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Users might be bound to a customization which they dont have access with the selected profile and it causes service throwing 403 when using inline suggestion and chat features"
4+
}

packages/amazonq/src/lsp/activation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import vscode from 'vscode'
77
import { startLanguageServer } from './client'
88
import { AmazonQLspInstaller } from './lspInstaller'
9-
import { lspSetupStage, ToolkitError } from 'aws-core-vscode/shared'
9+
import { lspSetupStage, ToolkitError, messages } from 'aws-core-vscode/shared'
1010

1111
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1212
try {
@@ -16,6 +16,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
1616
})
1717
} catch (err) {
1818
const e = err as ToolkitError
19-
void vscode.window.showInformationMessage(`Unable to launch amazonq language server: ${e.message}`)
19+
void messages.showViewLogsMessage(`Failed to launch Amazon Q language server: ${e.message}`)
2020
}
2121
}

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

Lines changed: 128 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
COPY_TO_CLIPBOARD,
1212
AuthFollowUpType,
1313
DISCLAIMER_ACKNOWLEDGED,
14+
UiMessageResultParams,
1415
} from '@aws/chat-client-ui-types'
1516
import {
1617
ChatResult,
@@ -21,30 +22,42 @@ import {
2122
QuickActionResult,
2223
QuickActionParams,
2324
insertToCursorPositionNotificationType,
25+
ErrorCodes,
26+
ResponseError,
27+
openTabRequestType,
28+
getSerializedChatRequestType,
29+
listConversationsRequestType,
30+
conversationClickRequestType,
31+
ShowSaveFileDialogRequestType,
32+
ShowSaveFileDialogParams,
33+
LSPErrorCodes,
34+
tabBarActionRequestType,
2435
} from '@aws/language-server-runtimes/protocol'
2536
import { v4 as uuidv4 } from 'uuid'
2637
import * as vscode from 'vscode'
27-
import { Disposable, LanguageClient, Position, State, TextDocumentIdentifier } from 'vscode-languageclient'
38+
import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient'
2839
import * as jose from 'jose'
2940
import { AmazonQChatViewProvider } from './webviewProvider'
3041
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
3142
import { AmazonQPromptSettings, messages } from 'aws-core-vscode/shared'
3243

3344
export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) {
34-
languageClient.onDidChangeState(({ oldState, newState }) => {
35-
if (oldState === State.Starting && newState === State.Running) {
36-
languageClient.info(
37-
'Language client received initializeResult from server:',
38-
JSON.stringify(languageClient.initializeResult)
39-
)
45+
languageClient.info(
46+
'Language client received initializeResult from server:',
47+
JSON.stringify(languageClient.initializeResult)
48+
)
4049

41-
const chatOptions = languageClient.initializeResult?.awsServerCapabilities?.chatOptions
50+
const chatOptions = languageClient.initializeResult?.awsServerCapabilities?.chatOptions
4251

43-
void provider.webview?.postMessage({
44-
command: CHAT_OPTIONS,
45-
params: chatOptions,
46-
})
47-
}
52+
// Enable the history/export feature flags
53+
chatOptions.history = true
54+
chatOptions.export = true
55+
56+
provider.onDidResolveWebview(() => {
57+
void provider.webview?.postMessage({
58+
command: CHAT_OPTIONS,
59+
params: chatOptions,
60+
})
4861
})
4962

5063
languageClient.onTelemetry((e) => {
@@ -60,6 +73,7 @@ export function registerMessageListeners(
6073
provider.webview?.onDidReceiveMessage(async (message) => {
6174
languageClient.info(`[VSCode Client] Received ${JSON.stringify(message)} from chat`)
6275

76+
const webview = provider.webview
6377
switch (message.command) {
6478
case COPY_TO_CLIPBOARD:
6579
languageClient.info('[VSCode Client] Copy to clipboard event received')
@@ -172,6 +186,11 @@ export function registerMessageListeners(
172186
)
173187
break
174188
}
189+
case listConversationsRequestType.method:
190+
case conversationClickRequestType.method:
191+
case tabBarActionRequestType.method:
192+
await resolveChatResponse(message.command, message.params, languageClient, webview)
193+
break
175194
case followUpClickNotificationType.method:
176195
if (!isValidAuthFollowUpType(message.params.followUp.type)) {
177196
languageClient.sendNotification(followUpClickNotificationType.method, message.params)
@@ -184,6 +203,89 @@ export function registerMessageListeners(
184203
break
185204
}
186205
}, undefined)
206+
207+
const registerHandlerWithResponseRouter = (command: string) => {
208+
const handler = async (params: any, _: any) => {
209+
const mapErrorType = (type: string | undefined): number => {
210+
switch (type) {
211+
case 'InvalidRequest':
212+
return ErrorCodes.InvalidRequest
213+
case 'InternalError':
214+
return ErrorCodes.InternalError
215+
case 'UnknownError':
216+
default:
217+
return ErrorCodes.UnknownErrorCode
218+
}
219+
}
220+
const requestId = uuidv4()
221+
222+
void provider.webview?.postMessage({
223+
requestId: requestId,
224+
command: command,
225+
params: params,
226+
})
227+
const responsePromise = new Promise<UiMessageResultParams | undefined>((resolve, reject) => {
228+
const timeout = setTimeout(() => {
229+
disposable?.dispose()
230+
reject(new Error('Request timed out'))
231+
}, 30000)
232+
233+
const disposable = provider.webview?.onDidReceiveMessage((message: any) => {
234+
if (message.requestId === requestId) {
235+
clearTimeout(timeout)
236+
disposable?.dispose()
237+
resolve(message.params)
238+
}
239+
})
240+
})
241+
242+
const result = await responsePromise
243+
244+
if (result?.success) {
245+
return result.result
246+
} else {
247+
return new ResponseError(
248+
mapErrorType(result?.error.type),
249+
result?.error.message ?? 'No response from client'
250+
)
251+
}
252+
}
253+
254+
languageClient.onRequest(command, handler)
255+
}
256+
257+
registerHandlerWithResponseRouter(openTabRequestType.method)
258+
registerHandlerWithResponseRouter(getSerializedChatRequestType.method)
259+
260+
languageClient.onRequest(ShowSaveFileDialogRequestType.method, async (params: ShowSaveFileDialogParams) => {
261+
const filters: Record<string, string[]> = {}
262+
const formatMappings = [
263+
{ format: 'markdown', key: 'Markdown', extensions: ['md'] },
264+
{ format: 'html', key: 'HTML', extensions: ['html'] },
265+
]
266+
267+
for (const format of params.supportedFormats ?? []) {
268+
const mapping = formatMappings.find((m) => m.format === format)
269+
if (mapping) {
270+
filters[mapping.key] = mapping.extensions
271+
}
272+
}
273+
274+
const saveAtUri = params.defaultUri ? vscode.Uri.parse(params.defaultUri) : vscode.Uri.file('export-chat.md')
275+
const targetUri = await vscode.window.showSaveDialog({
276+
filters,
277+
defaultUri: saveAtUri,
278+
title: 'Export',
279+
})
280+
281+
if (!targetUri) {
282+
return new ResponseError(LSPErrorCodes.RequestFailed, 'Export failed')
283+
}
284+
285+
return {
286+
targetUri: targetUri.toString(),
287+
}
288+
})
187289
}
188290

189291
function isServerEvent(command: string) {
@@ -258,3 +360,16 @@ async function handleCompleteResult<T>(
258360
})
259361
disposable.dispose()
260362
}
363+
364+
async function resolveChatResponse(
365+
requestMethod: string,
366+
params: any,
367+
languageClient: LanguageClient,
368+
webview: vscode.Webview | undefined
369+
) {
370+
const result = await languageClient.sendRequest(requestMethod, params)
371+
void webview?.postMessage({
372+
command: requestMethod,
373+
params: result,
374+
})
375+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
5050
) {
5151
this.webview = webviewView.webview
5252

53-
const lspDir = Uri.parse(LanguageServerResolver.defaultDir)
53+
const lspDir = Uri.parse(LanguageServerResolver.defaultDir())
5454
webviewView.webview.options = {
5555
enableScripts: true,
5656
enableCommandUris: true,

packages/amazonq/src/lsp/client.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ import {
2929
Experiments,
3030
Commands,
3131
oneSecond,
32+
validateNodeExe,
33+
getLogger,
3234
} from 'aws-core-vscode/shared'
3335
import { activate } from './chat/activation'
3436
import { AmazonQResourcePaths } from './lspInstaller'
3537

3638
const localize = nls.loadMessageBundle()
39+
const logger = getLogger('amazonqLsp.lspClient')
3740

3841
export async function startLanguageServer(
3942
extensionContext: vscode.ExtensionContext,
@@ -43,24 +46,27 @@ export async function startLanguageServer(
4346

4447
const serverModule = resourcePaths.lsp
4548

49+
const argv = [
50+
'--nolazy',
51+
'--preserve-symlinks',
52+
'--stdio',
53+
'--pre-init-encryption',
54+
'--set-credentials-encryption-key',
55+
]
4656
const serverOptions = createServerOptions({
4757
encryptionKey,
4858
executable: resourcePaths.node,
4959
serverModule,
50-
execArgv: [
51-
'--nolazy',
52-
'--preserve-symlinks',
53-
'--stdio',
54-
'--pre-init-encryption',
55-
'--set-credentials-encryption-key',
56-
],
60+
execArgv: argv,
5761
})
5862

5963
const documentSelector = [{ scheme: 'file', language: '*' }]
6064

6165
const clientId = 'amazonq'
6266
const traceServerEnabled = Settings.instance.isSet(`${clientId}.trace.server`)
6367

68+
await validateNodeExe(resourcePaths.node, resourcePaths.lsp, argv, logger)
69+
6470
// Options to control the language client
6571
const clientOptions: LanguageClientOptions = {
6672
// Register the server for json documents

packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('registerMessageListeners', () => {
2626
info: sandbox.stub(),
2727
error: errorStub,
2828
sendNotification: sandbox.stub(),
29+
onRequest: sandbox.stub(),
2930
} as unknown as LanguageClient
3031

3132
provider = {

packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*/
55
import * as sinon from 'sinon'
66
import assert from 'assert'
7-
import { globals } from 'aws-core-vscode/shared'
8-
import { LspClient } from 'aws-core-vscode/amazonq'
7+
import { globals, getNodeExecutableName } from 'aws-core-vscode/shared'
8+
import { LspClient, lspClient as lspClientModule } from 'aws-core-vscode/amazonq'
99

1010
describe('Amazon Q LSP client', function () {
1111
let lspClient: LspClient
@@ -50,6 +50,22 @@ describe('Amazon Q LSP client', function () {
5050
assert.ok(!encryptedSample.includes('hello'))
5151
})
5252

53+
it('validates node executable + lsp bundle', async () => {
54+
await assert.rejects(async () => {
55+
await lspClientModule.activate(globals.context, {
56+
// Mimic the `LspResolution<ResourcePaths>` type.
57+
node: 'node.bogus.exe',
58+
lsp: 'fake/lsp.js',
59+
})
60+
}, /.*failed to run basic .*node.*exitcode.*node\.bogus\.exe.*/)
61+
await assert.rejects(async () => {
62+
await lspClientModule.activate(globals.context, {
63+
node: getNodeExecutableName(),
64+
lsp: 'fake/lsp.js',
65+
})
66+
}, /.*failed to run .*exitcode.*node.*lsp\.js/)
67+
})
68+
5369
afterEach(() => {
5470
sinon.restore()
5571
})

0 commit comments

Comments
 (0)