Skip to content

fix(amazonq): Profile needing to be selected on server restart #7316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Bug Fix",
"description": "Fix Error: 'Amazon Q service is not signed in'"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Bug Fix",
"description": "Fix Error: 'Amazon Q Profile is not selected for IDC connection type'"
}
7 changes: 4 additions & 3 deletions packages/amazonq/src/lsp/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { AuthUtil } from 'aws-core-vscode/codewhisperer'
import { Writable } from 'stream'
import { onceChanged } from 'aws-core-vscode/utils'
import { getLogger, oneMinute } from 'aws-core-vscode/shared'
import { isSsoConnection } from 'aws-core-vscode/auth'

export const encryptionKey = crypto.randomBytes(32)

Expand Down Expand Up @@ -76,8 +77,8 @@ export class AmazonQLspAuth {
* @param force bypass memoization, and forcefully update the bearer token
*/
async refreshConnection(force: boolean = false) {
const activeConnection = this.authUtil.auth.activeConnection
if (activeConnection?.state === 'valid' && activeConnection?.type === 'sso') {
const activeConnection = this.authUtil.conn
if (this.authUtil.isConnectionValid() && isSsoConnection(activeConnection)) {
// send the token to the language server
const token = await this.authUtil.getBearerToken()
await (force ? this._updateBearerToken(token) : this.updateBearerToken(token))
Expand Down Expand Up @@ -118,7 +119,7 @@ export class AmazonQLspAuth {
data: jwt,
metadata: {
sso: {
startUrl: AuthUtil.instance.auth.startUrl,
startUrl: AuthUtil.instance.startUrl,
},
},
encrypted: true,
Expand Down
74 changes: 15 additions & 59 deletions packages/amazonq/src/lsp/chat/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,26 @@ import { Commands, getLogger, globals, undefinedIfEmpty } from 'aws-core-vscode/
import { activate as registerLegacyChatListeners } from '../../app/chat/activation'
import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq'
import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer'
import {
DidChangeConfigurationNotification,
updateConfigurationRequestType,
} from '@aws/language-server-runtimes/protocol'
import { pushConfigUpdate } from '../config'

export async function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) {
const disposables = globals.context.subscriptions

// Make sure we've sent an auth profile to the language server before even initializing the UI
await pushConfigUpdate(languageClient, {
type: 'profile',
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
})
// We need to push the cached customization on startup explicitly
await pushConfigUpdate(languageClient, {
type: 'customization',
customization: getSelectedCustomization(),
})
//
// Ideally the handler for onDidChangeRegionProfile would trigger this, but because the handler is set up too late (due to the current structure), the initial event is missed.
// So we have to explicitly do it here on startup.
if (AuthUtil.instance.isConnectionValid()) {
await pushConfigUpdate(languageClient, {
type: 'profile',
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
})

await pushConfigUpdate(languageClient, {
type: 'customization',
customization: getSelectedCustomization(),
})
}

const provider = new AmazonQChatViewProvider(mynahUIPath)

Expand Down Expand Up @@ -79,10 +81,6 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu

disposables.push(
AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => {
void pushConfigUpdate(languageClient, {
type: 'profile',
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
})
await provider.refreshWebview()
}),
Commands.register('aws.amazonq.updateCustomizations', () => {
Expand All @@ -99,45 +97,3 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu
})
)
}

/**
* Push a config value to the language server, effectively updating it with the
* latest configuration from the client.
*
* The issue is we need to push certain configs to different places, since there are
* different handlers for specific configs. So this determines the correct place to
* push the given config.
*/
async function pushConfigUpdate(client: LanguageClient, config: QConfigs) {
switch (config.type) {
case 'profile':
await client.sendRequest(updateConfigurationRequestType.method, {
section: 'aws.q',
settings: { profileArn: config.profileArn },
})
break
case 'customization':
client.sendNotification(DidChangeConfigurationNotification.type.method, {
section: 'aws.q',
settings: { customization: config.customization },
})
break
case 'logLevel':
client.sendNotification(DidChangeConfigurationNotification.type.method, {
section: 'aws.logLevel',
})
break
}
}
type ProfileConfig = {
type: 'profile'
profileArn: string | undefined
}
type CustomizationConfig = {
type: 'customization'
customization: string | undefined
}
type LogLevelConfig = {
type: 'logLevel'
}
type QConfigs = ProfileConfig | CustomizationConfig | LogLevelConfig
229 changes: 125 additions & 104 deletions packages/amazonq/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
import { processUtils } from 'aws-core-vscode/shared'
import { activate } from './chat/activation'
import { AmazonQResourcePaths } from './lspInstaller'
import { ConfigSection, isValidConfigSection, toAmazonQLSPLogLevel } from './config'
import { ConfigSection, isValidConfigSection, pushConfigUpdate, toAmazonQLSPLogLevel } from './config'
import { telemetry } from 'aws-core-vscode/telemetry'

const localize = nls.loadMessageBundle()
Expand Down Expand Up @@ -160,120 +160,141 @@ export async function startLanguageServer(

const disposable = client.start()
toDispose.push(disposable)
await client.onReady()

const auth = new AmazonQLspAuth(client)
const auth = await initializeAuth(client)

return client.onReady().then(async () => {
await auth.refreshConnection()
await postStartLanguageServer(auth, client, resourcePaths, toDispose)

if (Experiments.instance.get('amazonqLSPInline', false)) {
const inlineManager = new InlineCompletionManager(client)
inlineManager.registerInlineCompletion()
toDispose.push(
inlineManager,
Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => {
await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger')
}),
vscode.workspace.onDidCloseTextDocument(async () => {
await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion')
})
)
}
return client
}

if (Experiments.instance.get('amazonqChatLSP', true)) {
await activate(client, encryptionKey, resourcePaths.ui)
}
async function initializeAuth(client: LanguageClient): Promise<AmazonQLspAuth> {
AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically this listener could trigger before we push the bearer token below in await auth.refreshConnection(true) right? Wouldn't that be problematic?

Copy link
Contributor Author

@nkomonen-amazon nkomonen-amazon May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From testing I haven't found a code path where this happens. This flow will be simplified in flare and I will revisit

void pushConfigUpdate(client, {
type: 'profile',
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
})
})

const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond)
const auth = new AmazonQLspAuth(client)
await auth.refreshConnection(true)
return auth
}

const sendProfileToLsp = async () => {
try {
const result = await client.sendRequest(updateConfigurationRequestType.method, {
section: 'aws.q',
settings: {
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
},
})
client.info(
`Client: Updated Amazon Q Profile ${AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn} to Amazon Q LSP`,
result
)
} catch (err) {
client.error('Error when setting Q Developer Profile to Amazon Q LSP', err)
}
async function postStartLanguageServer(
auth: AmazonQLspAuth,
client: LanguageClient,
resourcePaths: AmazonQResourcePaths,
toDispose: vscode.Disposable[]
) {
if (Experiments.instance.get('amazonqLSPInline', false)) {
const inlineManager = new InlineCompletionManager(client)
inlineManager.registerInlineCompletion()
toDispose.push(
inlineManager,
Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => {
await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger')
}),
vscode.workspace.onDidCloseTextDocument(async () => {
await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion')
})
)
}

if (Experiments.instance.get('amazonqChatLSP', true)) {
await activate(client, encryptionKey, resourcePaths.ui)
}

const refreshInterval = auth.startTokenRefreshInterval(10 * oneSecond)

const sendProfileToLsp = async () => {
try {
const result = await client.sendRequest(updateConfigurationRequestType.method, {
section: 'aws.q',
settings: {
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
},
})
client.info(
`Client: Updated Amazon Q Profile ${AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn} to Amazon Q LSP`,
result
)
} catch (err) {
client.error('Error when setting Q Developer Profile to Amazon Q LSP', err)
}
}

// send profile to lsp once.
void sendProfileToLsp()
// send profile to lsp once.
void sendProfileToLsp()

toDispose.push(
AuthUtil.instance.auth.onDidChangeActiveConnection(async () => {
await auth.refreshConnection()
}),
AuthUtil.instance.auth.onDidDeleteConnection(async () => {
client.sendNotification(notificationTypes.deleteBearerToken.method)
}),
AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(sendProfileToLsp),
vscode.commands.registerCommand('aws.amazonq.getWorkspaceId', async () => {
const requestType = new RequestType<GetConfigurationFromServerParams, ResponseMessage, Error>(
'aws/getConfigurationFromServer'
)
const workspaceIdResp = await client.sendRequest(requestType.method, {
section: 'aws.q.workspaceContext',
})
return workspaceIdResp
}),
vscode.workspace.onDidCreateFiles((e) => {
client.sendNotification('workspace/didCreateFiles', {
files: e.files.map((it) => {
return { uri: it.fsPath }
toDispose.push(
AuthUtil.instance.auth.onDidChangeActiveConnection(async () => {
await auth.refreshConnection()
}),
AuthUtil.instance.auth.onDidDeleteConnection(async () => {
client.sendNotification(notificationTypes.deleteBearerToken.method)
}),
AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(sendProfileToLsp),
vscode.commands.registerCommand('aws.amazonq.getWorkspaceId', async () => {
const requestType = new RequestType<GetConfigurationFromServerParams, ResponseMessage, Error>(
'aws/getConfigurationFromServer'
)
const workspaceIdResp = await client.sendRequest(requestType.method, {
section: 'aws.q.workspaceContext',
})
return workspaceIdResp
}),
vscode.workspace.onDidCreateFiles((e) => {
client.sendNotification('workspace/didCreateFiles', {
files: e.files.map((it) => {
return { uri: it.fsPath }
}),
} as CreateFilesParams)
}),
vscode.workspace.onDidDeleteFiles((e) => {
client.sendNotification('workspace/didDeleteFiles', {
files: e.files.map((it) => {
return { uri: it.fsPath }
}),
} as DeleteFilesParams)
}),
vscode.workspace.onDidRenameFiles((e) => {
client.sendNotification('workspace/didRenameFiles', {
files: e.files.map((it) => {
return { oldUri: it.oldUri.fsPath, newUri: it.newUri.fsPath }
}),
} as RenameFilesParams)
}),
vscode.workspace.onDidSaveTextDocument((e) => {
client.sendNotification('workspace/didSaveTextDocument', {
textDocument: {
uri: e.uri.fsPath,
},
} as DidSaveTextDocumentParams)
}),
vscode.workspace.onDidChangeWorkspaceFolders((e) => {
client.sendNotification('workspace/didChangeWorkspaceFolder', {
event: {
added: e.added.map((it) => {
return {
name: it.name,
uri: it.uri.fsPath,
} as WorkspaceFolder
}),
} as CreateFilesParams)
}),
vscode.workspace.onDidDeleteFiles((e) => {
client.sendNotification('workspace/didDeleteFiles', {
files: e.files.map((it) => {
return { uri: it.fsPath }
removed: e.removed.map((it) => {
return {
name: it.name,
uri: it.uri.fsPath,
} as WorkspaceFolder
}),
} as DeleteFilesParams)
}),
vscode.workspace.onDidRenameFiles((e) => {
client.sendNotification('workspace/didRenameFiles', {
files: e.files.map((it) => {
return { oldUri: it.oldUri.fsPath, newUri: it.newUri.fsPath }
}),
} as RenameFilesParams)
}),
vscode.workspace.onDidSaveTextDocument((e) => {
client.sendNotification('workspace/didSaveTextDocument', {
textDocument: {
uri: e.uri.fsPath,
},
} as DidSaveTextDocumentParams)
}),
vscode.workspace.onDidChangeWorkspaceFolders((e) => {
client.sendNotification('workspace/didChangeWorkspaceFolder', {
event: {
added: e.added.map((it) => {
return {
name: it.name,
uri: it.uri.fsPath,
} as WorkspaceFolder
}),
removed: e.removed.map((it) => {
return {
name: it.name,
uri: it.uri.fsPath,
} as WorkspaceFolder
}),
},
} as DidChangeWorkspaceFoldersParams)
}),
{ dispose: () => clearInterval(refreshInterval) },
// Set this inside onReady so that it only triggers on subsequent language server starts (not the first)
onServerRestartHandler(client, auth)
)
})
},
} as DidChangeWorkspaceFoldersParams)
}),
{ dispose: () => clearInterval(refreshInterval) },
// Set this inside onReady so that it only triggers on subsequent language server starts (not the first)
onServerRestartHandler(client, auth)
)
}

/**
Expand Down
Loading
Loading