Skip to content
Merged
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions packages/amazonq/.changes/1.62.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"date": "2025-04-25",
"version": "1.62.0",
"entries": [
{
"type": "Bug Fix",
"description": "Toast message to warn users if Developer Profile is not selected"
},
{
"type": "Bug Fix",
"description": "Fix users can not log in successfully with 2+ IDE instnaces open due to throttle error throw by the service"
}
]
}
5 changes: 5 additions & 0 deletions packages/amazonq/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.62.0 2025-04-25

- **Bug Fix** Toast message to warn users if Developer Profile is not selected
- **Bug Fix** Fix users can not log in successfully with 2+ IDE instnaces open due to throttle error throw by the service

## 1.61.0 2025-04-22

- **Bug Fix** Some users not signaled they needed to select a Region Profile to get features working
Expand Down
6 changes: 5 additions & 1 deletion packages/amazonq/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
"description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI",
"version": "1.62.0-SNAPSHOT",
"version": "1.63.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
Expand Down Expand Up @@ -131,6 +131,10 @@
"amazonQChatDisclaimer": {
"type": "boolean",
"default": false
},
"amazonQSelectDeveloperProfile": {
"type": "boolean",
"default": false
}
},
"additionalProperties": false
Expand Down
2 changes: 1 addition & 1 deletion packages/amazonq/test/e2e/inline/inline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('Amazon Q Inline', async function () {
.query({
metricName: 'codewhisperer_userTriggerDecision',
})
.map((e) => collectionUtil.partialClone(e, 3, ['credentialStartUrl'], '[omitted]'))
.map((e) => collectionUtil.partialClone(e, 3, ['credentialStartUrl'], { replacement: '[omitted]' }))
}

for (const [name, invokeCompletion] of [
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/auth/sso/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function addLoggingMiddleware(client: SSOOIDCClient) {
args.input as unknown as Record<string, unknown>,
3,
['clientSecret', 'accessToken', 'refreshToken'],
'[omitted]'
{ replacement: '[omitted]' }
)
getLogger().debug('API request (%s %s): %O', hostname, path, input)
}
Expand Down Expand Up @@ -288,7 +288,7 @@ function addLoggingMiddleware(client: SSOOIDCClient) {
result.output as unknown as Record<string, unknown>,
3,
['clientSecret', 'accessToken', 'refreshToken'],
'[omitted]'
{ replacement: '[omitted]' }
)
getLogger().debug('API response (%s %s): %O', hostname, path, output)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/codewhisperer/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { SecurityIssueTreeViewProvider } from './service/securityIssueTreeViewPr
import { setContext } from '../shared/vscode/setContext'
import { syncSecurityIssueWebview } from './views/securityIssue/securityIssueWebview'
import { detectCommentAboveLine } from '../shared/utilities/commentUtils'
import { notifySelectDeveloperProfile } from './region/utils'

let localize: nls.LocalizeFunc

Expand Down Expand Up @@ -380,6 +381,10 @@ export async function activate(context: ExtContext): Promise<void> {
await auth.notifySessionConfiguration()
}
}

if (auth.requireProfileSelection()) {
await notifySelectDeveloperProfile()
}
},
{ emit: false, functionId: { name: 'activateCwCore' } }
)
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/codewhisperer/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const firstStartUpSource = ExtStartUpSources.firstStartUp
export const cwEllipsesMenu = 'ellipsesMenu'
/** Indicates a CodeWhisperer command was executed from the command palette */
export const commandPalette = 'commandPalette'
/** Indicates a CodeWhisperer command was executed as a result of a toast message interaction */
export const toastMessage = 'toastMessage'

/**
* Indicates what caused the CodeWhisperer command to be executed, since a command can be executed from different "sources"
Expand All @@ -35,3 +37,4 @@ export type CodeWhispererSource =
| typeof firstStartUpSource
| typeof cwEllipsesMenu
| typeof commandPalette
| typeof toastMessage
35 changes: 33 additions & 2 deletions packages/core/src/codewhisperer/region/regionProfileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { isAwsError, ToolkitError } from '../../shared/errors'
import { telemetry } from '../../shared/telemetry/telemetry'
import { localize } from '../../shared/utilities/vsCodeUtils'
import { Commands } from '../../shared/vscode/commands2'
import { CachedResource } from '../../shared/utilities/resourceCache'

// TODO: is there a better way to manage all endpoint strings in one place?
export const defaultServiceConfig: CodeWhispererConfig = {
Expand Down Expand Up @@ -59,6 +60,27 @@ export class RegionProfileManager {
// Store the last API results (for UI propuse) so we don't need to call service again if doesn't require "latest" result
private _profiles: RegionProfile[] = []

private readonly cache = new (class extends CachedResource<RegionProfile[]> {
constructor(private readonly profileProvider: () => Promise<RegionProfile[]>) {
super(
'aws.amazonq.regionProfiles.cache',
60000,
{
resource: {
locked: false,
timestamp: 0,
result: undefined,
},
},
{ timeout: 15000, interval: 1500, truthy: true }
)
}

override resourceProvider(): Promise<RegionProfile[]> {
return this.profileProvider()
}
})(this.listRegionProfile.bind(this))

get activeRegionProfile() {
const conn = this.connectionProvider()
if (isBuilderIdConnection(conn)) {
Expand Down Expand Up @@ -104,6 +126,10 @@ export class RegionProfileManager {

constructor(private readonly connectionProvider: () => Connection | undefined) {}

async getProfiles(): Promise<RegionProfile[]> {
return this.cache.getResource()
}

async listRegionProfile(): Promise<RegionProfile[]> {
this._profiles = []

Expand Down Expand Up @@ -238,7 +264,7 @@ export class RegionProfileManager {
return
}
// cross-validation
this.listRegionProfile()
this.getProfiles()
.then(async (profiles) => {
const r = profiles.find((it) => it.arn === previousSelected.arn)
if (!r) {
Expand Down Expand Up @@ -300,7 +326,7 @@ export class RegionProfileManager {
const selected = this.activeRegionProfile
let profiles: RegionProfile[] = []
try {
profiles = await this.listRegionProfile()
profiles = await this.getProfiles()
} catch (e) {
return [
{
Expand Down Expand Up @@ -347,6 +373,11 @@ export class RegionProfileManager {
}
}

// Should be called on connection changed in case users change to a differnet connection and use the wrong resultset.
async clearCache() {
await this.cache.clearCache()
}

async createQClient(region: string, endpoint: string, conn: SsoConnection): Promise<CodeWhispererUserClient> {
const token = (await conn.getToken()).accessToken
const serviceOption: ServiceOptions = {
Expand Down
49 changes: 49 additions & 0 deletions packages/core/src/codewhisperer/region/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as nls from 'vscode-nls'
const localize = nls.loadMessageBundle()
import { AmazonQPromptSettings } from '../../shared/settings'
import { telemetry } from '../../shared/telemetry/telemetry'
import vscode from 'vscode'
import { selectRegionProfileCommand } from '../commands/basicCommands'
import { placeholder } from '../../shared/vscode/commands2'
import { toastMessage } from '../commands/types'

/**
* Creates a toast message telling the user they need to select a Developer Profile
*/
export async function notifySelectDeveloperProfile() {
const suppressId = 'amazonQSelectDeveloperProfile'
const settings = AmazonQPromptSettings.instance
const shouldShow = settings.isPromptEnabled(suppressId)
if (!shouldShow) {
return
}

const message = localize(
'aws.amazonq.profile.mustSelectMessage',
'You must select a Q Developer Profile for Amazon Q features to work.'
)
const selectProfile = 'Select Profile'
const dontShowAgain = 'Dont Show Again'

await telemetry.toolkit_showNotification.run(async () => {
telemetry.record({ id: 'mustSelectDeveloperProfileMessage' })
void vscode.window.showWarningMessage(message, selectProfile, dontShowAgain).then(async (resp) => {
await telemetry.toolkit_invokeAction.run(async () => {
if (resp === selectProfile) {
// Show Profile
telemetry.record({ action: 'select' })
void selectRegionProfileCommand.execute(placeholder, toastMessage)
} else if (resp === dontShowAgain) {
telemetry.record({ action: 'dontShowAgain' })
await settings.disablePrompt(suppressId)
} else {
telemetry.record({ action: 'ignore' })
}
})
})
})
}
2 changes: 2 additions & 0 deletions packages/core/src/codewhisperer/util/authUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { withTelemetryContext } from '../../shared/telemetry/util'
import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands'
import { throttle } from 'lodash'
import { RegionProfileManager } from '../region/regionProfileManager'

/** Backwards compatibility for connections w pre-chat scopes */
export const codeWhispererCoreScopes = [...scopesCodeWhispererCore]
export const codeWhispererChatScopes = [...codeWhispererCoreScopes, ...scopesCodeWhispererChat]
Expand Down Expand Up @@ -142,6 +143,7 @@ export class AuthUtil {

if (!this.isConnected()) {
await this.regionProfileManager.invalidateProfile(this.regionProfileManager.activeRegionProfile?.arn)
await this.regionProfileManager.clearCache()
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
*/
override async listRegionProfiles(): Promise<RegionProfile[] | string> {
try {
return await AuthUtil.instance.regionProfileManager.listRegionProfile()
return await AuthUtil.instance.regionProfileManager.getProfiles()
} catch (e) {
const conn = AuthUtil.instance.conn as SsoConnection | undefined
telemetry.amazonq_didSelectProfile.emit({
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/shared/globalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type globalKey =
| 'aws.toolkit.lsp.manifest'
| 'aws.amazonq.customization.overrideV2'
| 'aws.amazonq.regionProfiles'
| 'aws.amazonq.regionProfiles.cache'
// Deprecated/legacy names. New keys should start with "aws.".
| '#sessionCreationDates' // Legacy name from `ssoAccessTokenProvider.ts`.
| 'CODECATALYST_RECONNECT'
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/shared/logger/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type LogTopic =
| 'chat'
| 'stepfunctions'
| 'unknown'
| 'resourceCache'

class ErrorLog {
constructor(
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/shared/settings-amazonq.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export const amazonqSettings = {
"ssoCacheError": {},
"amazonQLspManifestMessage": {},
"amazonQWorkspaceLspManifestMessage": {},
"amazonQChatDisclaimer": {}
"amazonQChatDisclaimer": {},
"amazonQSelectDeveloperProfile": {}
},
"amazonQ.showCodeWithReferences": {},
"amazonQ.allowFeatureDevelopmentToRunCodeAndTests": {},
Expand Down
21 changes: 17 additions & 4 deletions packages/core/src/shared/utilities/collectionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { isWeb } from '../extensionGlobals'
import { inspect as nodeInspect } from 'util'
import { AsyncCollection, toCollection } from './asyncCollection'
import { SharedProp, AccumulableKeys, Coalesce, isNonNullable } from './tsUtils'
import { truncate } from './textUtilities'

export function union<T>(a: Iterable<T>, b: Iterable<T>): Set<T> {
const result = new Set<T>()
Expand Down Expand Up @@ -304,26 +305,38 @@ export function assign<T extends Record<any, any>, U extends Partial<T>>(data: T
* @param depth
* @param omitKeys Omit properties matching these names (at any depth).
* @param replacement Replacement for object whose fields extend beyond `depth`, and properties matching `omitKeys`.
* @param maxStringLength truncates string values that exceed this threshold (includes values in nested arrays)
*/
export function partialClone(obj: any, depth: number = 3, omitKeys: string[] = [], replacement?: any): any {
export function partialClone(
obj: any,
depth: number = 3,
omitKeys: string[] = [],
options?: {
replacement?: any
maxStringLength?: number
}
): any {
// Base case: If input is not an object or has no children, return it.
if (typeof obj !== 'object' || obj === null || 0 === Object.getOwnPropertyNames(obj).length) {
if (typeof obj === 'string' && options?.maxStringLength) {
return truncate(obj, options?.maxStringLength, '...')
}
return obj
}

// Create a new object of the same type as the input object.
const clonedObj = Array.isArray(obj) ? [] : {}

if (depth === 0) {
return replacement ? replacement : clonedObj
return options?.replacement ? options.replacement : clonedObj
}

// Recursively clone properties of the input object
for (const key in obj) {
if (omitKeys.includes(key)) {
;(clonedObj as any)[key] = replacement ? replacement : Array.isArray(obj) ? [] : {}
;(clonedObj as any)[key] = options?.replacement ? options.replacement : Array.isArray(obj) ? [] : {}
} else if (Object.prototype.hasOwnProperty.call(obj, key)) {
;(clonedObj as any)[key] = partialClone(obj[key], depth - 1, omitKeys, replacement)
;(clonedObj as any)[key] = partialClone(obj[key], depth - 1, omitKeys, options)
}
}

Expand Down
Loading