From a670ff00be4672a0bf7716f2f3de2e714f87d787 Mon Sep 17 00:00:00 2001
From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Date: Tue, 11 Mar 2025 07:02:04 -0700
Subject: [PATCH 01/49] config(amazonq): update service sdk model (#2096)
## Problem
#2095
## Solution
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../codewhisperer/client/user-service-2.json | 743 +++++++++++++++++-
1 file changed, 707 insertions(+), 36 deletions(-)
diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json
index 1f33cb8c98c..c7d66d368a7 100644
--- a/packages/core/src/codewhisperer/client/user-service-2.json
+++ b/packages/core/src/codewhisperer/client/user-service-2.json
@@ -66,6 +66,23 @@
"documentation": "
Creates a pre-signed, S3 write URL for uploading a repository zip archive.
Represents a Workspace state uploaded to S3 for Async Code Actions
"
},
+ "WorkspaceStatus": {
+ "type": "string",
+ "enum": ["CREATED", "PENDING", "READY", "CONNECTED", "DELETING"]
+ },
"timeBetweenChunks": {
"type": "list",
"member": { "shape": "Double" },
From fc98b4735f5b2cd5354e6fcc996377fd01b79f0a Mon Sep 17 00:00:00 2001
From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Date: Fri, 14 Mar 2025 11:51:37 -0700
Subject: [PATCH 02/49] feat(amazonq): region profile quickpick impl (#2098)
## Problem
Profile selection quickpick
parent pr #2094
## Solution
## IdC
https://github.com/user-attachments/assets/a0c15e1d-17b5-4f7d-b51b-0e2f420c8a40
## BuilderID

---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
packages/core/src/codewhisperer/activation.ts | 2 +
.../codewhisperer/commands/basicCommands.ts | 16 ++++
.../core/src/codewhisperer/models/model.ts | 7 ++
.../region/regionProfileNamager.ts | 90 +++++++++++++++++++
.../codewhisperer/ui/codeWhispererNodes.ts | 18 ++++
.../src/codewhisperer/ui/statusBarMenu.ts | 2 +
.../core/src/codewhisperer/util/authUtil.ts | 7 +-
7 files changed, 140 insertions(+), 2 deletions(-)
create mode 100644 packages/core/src/codewhisperer/region/regionProfileNamager.ts
diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts
index 41141666e12..a47d7e7c83c 100644
--- a/packages/core/src/codewhisperer/activation.ts
+++ b/packages/core/src/codewhisperer/activation.ts
@@ -58,6 +58,7 @@ import {
focusIssue,
showExploreAgentsView,
showCodeIssueGroupingQuickPick,
+ selectRegionProfileCommand,
} from './commands/basicCommands'
import { sleep } from '../shared/utilities/timeoutUtils'
import { ReferenceLogViewProvider } from './service/referenceLogViewProvider'
@@ -312,6 +313,7 @@ export async function activate(context: ExtContext): Promise {
selectCustomizationPrompt.register(),
// notify new customizations
notifyNewCustomizationsCmd.register(),
+ selectRegionProfileCommand.register(),
/**
* On recommendation acceptance
*/
diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts
index abc69fb65ef..781ab2dae22 100644
--- a/packages/core/src/codewhisperer/commands/basicCommands.ts
+++ b/packages/core/src/codewhisperer/commands/basicCommands.ts
@@ -69,6 +69,8 @@ import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker'
import { parsePatch } from 'diff'
import { createCodeIssueGroupingStrategyPrompter } from '../ui/prompters'
import { cancel, confirm } from '../../shared/localizedText'
+import { DataQuickPickItem, showQuickPick } from '../../shared/ui/pickerPrompter'
+import { i18n } from '../../shared/i18n-helper'
const MessageTimeOut = 5_000
@@ -248,6 +250,20 @@ export const selectCustomizationPrompt = Commands.declare(
}
)
+export const selectRegionProfileCommand = Commands.declare(
+ { id: 'aws.amazonq.selectRegionProfile', compositeKey: { 1: 'source' } },
+ () => async (_: VsCodeCommandArg, source: CodeWhispererSource) => {
+ const quickPickItems: DataQuickPickItem[] =
+ await AuthUtil.instance.regionProfileManager.generateQuickPickItem()
+
+ await showQuickPick(quickPickItems, {
+ title: localize('AWS.q.profile.quickPick.title', 'Select a Profile'),
+ placeholder: localize('AWS.q.profile.quickPick.placeholder', 'You have access to the following profiles'),
+ recentlyUsed: i18n('AWS.codewhisperer.customization.selected'),
+ })
+ }
+)
+
export const reconnect = Commands.declare(
{ id: 'aws.amazonq.reconnect', compositeKey: { 1: 'source' } },
() => async (_: VsCodeCommandArg, source: CodeWhispererSource) => await AuthUtil.instance.reauthenticate()
diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts
index f7d1fe60f1b..3e632d94215 100644
--- a/packages/core/src/codewhisperer/models/model.ts
+++ b/packages/core/src/codewhisperer/models/model.ts
@@ -48,6 +48,13 @@ export const vsCodeState: VsCodeState = {
isFreeTierLimitReached: false,
}
+export interface RegionProfile {
+ name: string
+ region: string
+ arn: string
+ description: string
+}
+
export type UtgStrategy = 'byName' | 'byContent'
export type CrossFileStrategy = 'opentabs' | 'codemap' | 'bm25' | 'default'
diff --git a/packages/core/src/codewhisperer/region/regionProfileNamager.ts b/packages/core/src/codewhisperer/region/regionProfileNamager.ts
new file mode 100644
index 00000000000..6ee71d1e9af
--- /dev/null
+++ b/packages/core/src/codewhisperer/region/regionProfileNamager.ts
@@ -0,0 +1,90 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { getIcon } from '../../shared/icons'
+import { DataQuickPickItem } from '../../shared/ui/pickerPrompter'
+import { RegionProfile } from '../models/model'
+import { showConfirmationMessage } from '../../shared/utilities/messages'
+
+// TODO: Implementation
+export class RegionProfileManager {
+ private _activeRegionProfile: RegionProfile | undefined
+
+ get activeRegionProfile() {
+ return this._activeRegionProfile
+ }
+
+ // TODO: Implementation
+ async listRegionProfile(): Promise {
+ return [
+ {
+ name: 'ACME platform work',
+ region: 'us-east-1',
+ arn: 'foo',
+ description: 'Some description for ACME Platform Work',
+ },
+ {
+ name: 'EU payments TEAM',
+ region: 'us-east-1',
+ arn: 'bar',
+ description: 'Some description for EU payments TEAM',
+ },
+ {
+ name: 'CodeWhisperer TEAM',
+ region: 'us-east-1',
+ arn: 'baz',
+ description: 'Some description for CodeWhisperer TEAM',
+ },
+ ]
+ }
+
+ // TODO: Implementation
+ async switchRegionProfile(regionProfile: RegionProfile | undefined) {
+ if (regionProfile === this.activeRegionProfile) {
+ return
+ }
+
+ // only prompt to users when users switch from A profile to B profile
+ if (this.activeRegionProfile !== undefined && regionProfile !== undefined) {
+ const response = await showConfirmationMessage({
+ prompt: `Do you want to switch Amazon Q profiles to ${regionProfile?.name}`,
+ confirm: 'Switch profiles',
+ cancel: 'Cancel',
+ })
+
+ if (!response) {
+ return
+ }
+ }
+
+ this._activeRegionProfile = regionProfile
+ }
+
+ async generateQuickPickItem(): Promise[]> {
+ const selected = this.activeRegionProfile
+ const profiles = await this.listRegionProfile()
+ const icon = getIcon('vscode-account')
+ const quickPickItems: DataQuickPickItem[] = profiles.map((it) => {
+ const label = it.name
+ const onClick = async () => {
+ await this.switchRegionProfile(it)
+ }
+ const data = it.arn
+ const description = it.region
+ const isRecentlyUsed = selected ? selected.arn === it.arn : false
+
+ return {
+ label: `${icon} ${label}`,
+ onClick: onClick,
+ data: data,
+ description: description,
+ recentlyUsed: isRecentlyUsed,
+ detail: it.description,
+ }
+ })
+
+ return quickPickItems
+ }
+}
diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
index 5d8382cbec9..67b21f4362a 100644
--- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
+++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
@@ -18,6 +18,7 @@ import {
signoutCodeWhisperer,
showIntroduction,
toggleCodeScans,
+ selectRegionProfileCommand,
} from '../commands/basicCommands'
import { CodeWhispererCommandDeclarations } from '../commands/gettingStartedPageCommands'
import { CodeScansState, codeScanState } from '../models/model'
@@ -137,6 +138,23 @@ export function createSelectCustomization(): DataQuickPickItem<'selectCustomizat
} as DataQuickPickItem<'selectCustomization'>
}
+export function createSelectRegionProfileNode(): DataQuickPickItem<'selectRegionProfile'> {
+ const selectedRegionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile
+
+ const label = 'Switch Profile'
+ const icon = getIcon('vscode-arrow-swap')
+ const description = selectedRegionProfile ? `Current profile: ${selectedRegionProfile.name}` : ''
+
+ return {
+ data: 'selectRegionProfile',
+ label: codicon`${icon} ${label}`,
+ onClick: async () => {
+ await selectRegionProfileCommand.execute(placeholder, cwQuickPickSource)
+ },
+ description: description,
+ }
+}
+
/* Opens the Learn CodeWhisperer Page */
export function createGettingStarted(): DataQuickPickItem<'gettingStarted'> {
const label = localize('AWS.codewhisperer.gettingStartedNode.label', 'Try inline suggestion examples')
diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts
index 9b5fa43672e..190c3c09d80 100644
--- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts
+++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts
@@ -21,6 +21,7 @@ import {
createSignIn,
switchToAmazonQNode,
createSecurityScan,
+ createSelectRegionProfileNode,
} from './codeWhispererNodes'
import { hasVendedIamCredentials } from '../../auth/auth'
import { AuthUtil } from '../util/authUtil'
@@ -92,6 +93,7 @@ export function getQuickPickItems(): DataQuickPickItem[] {
// Add settings and signout
createSeparator(),
createSettingsNode(),
+ ...(AuthUtil.instance.isValidEnterpriseSsoInUse() ? [createSelectRegionProfileNode()] : []),
...(AuthUtil.instance.isConnected() && !hasVendedIamCredentials() ? [createSignout()] : []),
]
diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts
index 1e350384e73..a69f4bb75b5 100644
--- a/packages/core/src/codewhisperer/util/authUtil.ts
+++ b/packages/core/src/codewhisperer/util/authUtil.ts
@@ -45,7 +45,7 @@ import { asStringifiedStack } from '../../shared/telemetry/spans'
import { withTelemetryContext } from '../../shared/telemetry/util'
import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands'
import { throttle } from 'lodash'
-
+import { RegionProfileManager } from '../region/regionProfileNamager'
/** Backwards compatibility for connections w pre-chat scopes */
export const codeWhispererCoreScopes = [...scopesCodeWhispererCore]
export const codeWhispererChatScopes = [...codeWhispererCoreScopes, ...scopesCodeWhispererChat]
@@ -105,7 +105,10 @@ export class AuthUtil {
)
public readonly restore = () => this.secondaryAuth.restoreConnection()
- public constructor(public readonly auth = Auth.instance) {}
+ public constructor(
+ public readonly auth = Auth.instance,
+ public readonly regionProfileManager = new RegionProfileManager()
+ ) {}
public initCodeWhispererHooks = once(() => {
this.auth.onDidChangeConnectionState(async (e) => {
From 88787d507f329ffc1693fed2d3716e7e88005a3c Mon Sep 17 00:00:00 2001
From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Date: Fri, 14 Mar 2025 11:52:08 -0700
Subject: [PATCH 03/49] feat(amazonq): profile selection webview page (#2100)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
#2094
## Solution

https://github.com/user-attachments/assets/1e055562-e0f8-4a32-b724-a1e1342f33c0
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../core/src/codewhisperer/util/authUtil.ts | 1 +
.../webview/vue/amazonq/backend_amazonq.ts | 35 +++
.../core/src/login/webview/vue/backend.ts | 5 +
.../webview/vue/regionProfileSelector.vue | 251 ++++++++++++++++++
packages/core/src/login/webview/vue/root.vue | 7 +
.../src/login/webview/vue/selectableItem.vue | 16 +-
.../webview/vue/toolkit/backend_toolkit.ts | 9 +
packages/core/src/login/webview/vue/types.ts | 1 +
8 files changed, 324 insertions(+), 1 deletion(-)
create mode 100644 packages/core/src/login/webview/vue/regionProfileSelector.vue
diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts
index a69f4bb75b5..373c6fba2a9 100644
--- a/packages/core/src/codewhisperer/util/authUtil.ts
+++ b/packages/core/src/codewhisperer/util/authUtil.ts
@@ -535,6 +535,7 @@ export const AuthStates = {
* but fetching/refreshing the token resulted in a network error.
*/
connectedWithNetworkError: 'connectedWithNetworkError',
+ pendingProfileSelection: 'pendingProfileSelection',
} as const
const Features = {
codewhispererCore: 'codewhispererCore',
diff --git a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
index 83c511ad9f6..09e3ada0e62 100644
--- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
+++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
@@ -22,6 +22,7 @@ import { AuthError, AuthFlowState, userCancelled } from '../types'
import { ToolkitError } from '../../../../shared/errors'
import { withTelemetryContext } from '../../../../shared/telemetry/util'
import { builderIdStartUrl } from '../../../../auth/sso/constants'
+import { RegionProfile } from '../../../../codewhisperer/models/model'
const className = 'AmazonQLoginWebview'
export class AmazonQLoginWebview extends CommonAuthWebview {
@@ -156,6 +157,8 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
if (featureAuthStates.amazonQ === 'expired') {
this.authState = this.isReauthenticating ? 'REAUTHENTICATING' : 'REAUTHNEEDED'
return
+ } else if (featureAuthStates.amazonQ === 'pendingProfileSelection') {
+ return
}
this.authState = 'LOGIN'
}
@@ -201,6 +204,38 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
/** If users are unauthenticated in Q/CW, we should always display the auth screen. */
async quitLoginScreen() {}
+ override listRegionProfiles(): Promise {
+ // TODO: uncomment
+ // return AuthUtil.instance.regionProfileManager.listRegionProfile()
+
+ return Promise.resolve([
+ {
+ name: 'ACME platform work',
+ region: 'us-east-1',
+ arn: 'foo',
+ description: 'Some description for ACME Platform Work',
+ },
+ {
+ name: 'EU payments TEAM',
+ region: 'us-east-1',
+ arn: 'bar',
+ description: 'Some description for EU payments TEAM',
+ },
+ {
+ name: 'CodeWhisperer TEAM',
+ region: 'us-east-1',
+ arn: 'baz',
+ description: 'Some description for CodeWhisperer TEAM',
+ },
+ ])
+ }
+
+ override selectRegionProfile(profile: RegionProfile): Promise {
+ // TODO: uncomment
+ // return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile)
+ return Promise.resolve()
+ }
+
private setupConnectionEventEmitter(): void {
// allows the frontend to listen to Amazon Q auth events from the backend
const codeWhispererConnectionChanged = createThrottle(() => this.onActiveConnectionModified.fire())
diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts
index 9bc6c5ae339..a03383136d0 100644
--- a/packages/core/src/login/webview/vue/backend.ts
+++ b/packages/core/src/login/webview/vue/backend.ts
@@ -32,6 +32,7 @@ import { DevSettings } from '../../../shared/settings'
import { AuthSSOServer } from '../../../auth/sso/server'
import { getLogger } from '../../../shared/logger/logger'
import { isValidUrl } from '../../../shared/utilities/uriUtils'
+import { RegionProfile } from '../../../codewhisperer/models/model'
export abstract class CommonAuthWebview extends VueWebview {
private readonly className = 'CommonAuthWebview'
@@ -208,6 +209,10 @@ export abstract class CommonAuthWebview extends VueWebview {
/** List current connections known by the extension for the purpose of preventing duplicates. */
abstract listSsoConnections(): Promise
+ abstract listRegionProfiles(): Promise
+
+ abstract selectRegionProfile(profile: RegionProfile): Promise
+
/**
* Emit stored metric metadata. Does not reset the stored metric metadata, because it
* may be used for additional emits (e.g. user cancels multiple times, user cancels then logs in)
diff --git a/packages/core/src/login/webview/vue/regionProfileSelector.vue b/packages/core/src/login/webview/vue/regionProfileSelector.vue
new file mode 100644
index 00000000000..a74e707856b
--- /dev/null
+++ b/packages/core/src/login/webview/vue/regionProfileSelector.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+
+
Select profile
+
+ Profles have different configs defined by your adminstrators. Select the profile that best meets your
+ current working need and switch at any time.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/login/webview/vue/root.vue b/packages/core/src/login/webview/vue/root.vue
index efc34881c9b..2e5c47d1bd9 100644
--- a/packages/core/src/login/webview/vue/root.vue
+++ b/packages/core/src/login/webview/vue/root.vue
@@ -12,12 +12,18 @@ configure app to AMAZONQ if for Amazon Q login
:state="authFlowState"
:key="refreshKey"
>
+
${cssLinks}
@@ -91,7 +106,7 @@ export class WebViewContentGenerator {
const init = () => {
createMynahUI(acquireVsCodeApi(), ${
(await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected'
- },${featureConfigsString},${welcomeLoadCount},${disclaimerAcknowledged},${disabledCommandsString});
+ },${featureConfigsString},${welcomeLoadCount},${disclaimerAcknowledged},${regionProfileString},${disabledCommandsString});
}
`
diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts
index d7285d81ba5..11734999856 100644
--- a/packages/core/src/amazonq/webview/ui/main.ts
+++ b/packages/core/src/amazonq/webview/ui/main.ts
@@ -34,6 +34,7 @@ import { welcomeScreenTabData } from './walkthrough/welcome'
import { agentWalkthroughDataModel } from './walkthrough/agent'
import { createClickTelemetry, createOpenAgentTelemetry } from './telemetry/actions'
import { disclaimerAcknowledgeButtonId, disclaimerCard } from './texts/disclaimer'
+import { RegionProfile } from '../../../codewhisperer/models/model'
/**
* The number of welcome chat tabs that can be opened before the NEXT one will become
@@ -47,6 +48,7 @@ export const createMynahUI = (
featureConfigsSerialized: [string, FeatureContext][],
welcomeCount: number,
disclaimerAcknowledged: boolean,
+ regionProfile: RegionProfile | undefined,
disabledCommands?: string[]
) => {
let disclaimerCardActive = !disclaimerAcknowledged
@@ -122,6 +124,7 @@ export const createMynahUI = (
isDocEnabled,
disabledCommands,
commandHighlight: highlightCommand,
+ regionProfile,
})
// eslint-disable-next-line prefer-const
diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts
index f037e4c56ef..f217e48f243 100644
--- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts
+++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts
@@ -3,13 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { ChatItemType, MynahUIDataModel, QuickActionCommandGroup } from '@aws/mynah-ui'
+import { ChatItem, ChatItemType, MynahUIDataModel, QuickActionCommandGroup } from '@aws/mynah-ui'
import { TabType } from '../storages/tabsStorage'
import { FollowUpGenerator } from '../followUps/generator'
import { QuickActionGenerator } from '../quickActions/generator'
import { TabTypeDataMap } from './constants'
import { agentWalkthroughDataModel } from '../walkthrough/agent'
import { FeatureContext } from '../../../../shared/featureConfig'
+import { RegionProfile } from '../../../../codewhisperer/models/model'
export interface TabDataGeneratorProps {
isFeatureDevEnabled: boolean
@@ -19,12 +20,14 @@ export interface TabDataGeneratorProps {
isDocEnabled: boolean
disabledCommands?: string[]
commandHighlight?: FeatureContext
+ regionProfile?: RegionProfile
}
export class TabDataGenerator {
private followUpsGenerator: FollowUpGenerator
public quickActionsGenerator: QuickActionGenerator
private highlightCommand?: FeatureContext
+ private regionProfile?: RegionProfile
constructor(props: TabDataGeneratorProps) {
this.followUpsGenerator = new FollowUpGenerator()
@@ -37,6 +40,7 @@ export class TabDataGenerator {
disableCommands: props.disabledCommands,
})
this.highlightCommand = props.commandHighlight
+ this.regionProfile = props.regionProfile
}
public getTabData(tabType: TabType, needWelcomeMessages: boolean, taskName?: string): MynahUIDataModel {
@@ -48,6 +52,16 @@ export class TabDataGenerator {
return {}
}
+ const regionProfileCard: ChatItem | undefined =
+ this.regionProfile === undefined
+ ? undefined
+ : {
+ type: ChatItemType.ANSWER,
+ body: `You are using the ${this.regionProfile?.name} profile for this chat`,
+ status: 'info',
+ messageId: 'regionProfile',
+ }
+
const tabData: MynahUIDataModel = {
tabTitle: taskName ?? TabTypeDataMap[tabType].title,
promptInputInfo:
@@ -57,6 +71,7 @@ export class TabDataGenerator {
contextCommands: this.getContextCommands(tabType),
chatItems: needWelcomeMessages
? [
+ ...(regionProfileCard ? [regionProfileCard] : []),
{
type: ChatItemType.ANSWER,
body: TabTypeDataMap[tabType].welcome,
@@ -66,7 +81,7 @@ export class TabDataGenerator {
followUp: this.followUpsGenerator.generateWelcomeBlockForTab(tabType),
},
]
- : [],
+ : [...(regionProfileCard ? [regionProfileCard] : [])],
}
return tabData
}
diff --git a/packages/core/src/amazonq/webview/webView.ts b/packages/core/src/amazonq/webview/webView.ts
index 74f60cbf67b..bb56af9098c 100644
--- a/packages/core/src/amazonq/webview/webView.ts
+++ b/packages/core/src/amazonq/webview/webView.ts
@@ -20,6 +20,7 @@ import { MessageListener } from '../messages/messageListener'
import { MessagePublisher } from '../messages/messagePublisher'
import { TabType } from './ui/storages/tabsStorage'
import { amazonqMark } from '../../shared/performance/marks'
+import { AuthUtil } from '../../codewhisperer/util/authUtil'
export class AmazonQChatViewProvider implements WebviewViewProvider {
public static readonly viewType = 'aws.AmazonQChatView'
@@ -46,6 +47,13 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
this.onDidChangeAmazonQVisibility.fire(webviewView.visible)
})
+ AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => {
+ webviewView.webview.html = await this.webViewContentGenerator.generate(
+ this.extensionContext.extensionUri,
+ webviewView.webview
+ )
+ })
+
const dist = Uri.joinPath(this.extensionContext.extensionUri, 'dist')
const resources = Uri.joinPath(this.extensionContext.extensionUri, 'resources')
webviewView.webview.options = {
diff --git a/packages/core/src/codewhisperer/region/regionProfileNamager.ts b/packages/core/src/codewhisperer/region/regionProfileNamager.ts
index 6ee71d1e9af..e053a1469ce 100644
--- a/packages/core/src/codewhisperer/region/regionProfileNamager.ts
+++ b/packages/core/src/codewhisperer/region/regionProfileNamager.ts
@@ -3,16 +3,26 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import * as vscode from 'vscode'
import { getIcon } from '../../shared/icons'
import { DataQuickPickItem } from '../../shared/ui/pickerPrompter'
import { RegionProfile } from '../models/model'
import { showConfirmationMessage } from '../../shared/utilities/messages'
+import { Connection, isIdcSsoConnection } from '../../auth/connection'
// TODO: Implementation
export class RegionProfileManager {
private _activeRegionProfile: RegionProfile | undefined
+ private _onDidChangeRegionProfile = new vscode.EventEmitter()
+ public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event
+
+ public constructor(private readonly connectionProvider: () => Connection | undefined) {}
get activeRegionProfile() {
+ const conn = this.connectionProvider()
+ if (conn === undefined || !isIdcSsoConnection(conn)) {
+ return undefined
+ }
return this._activeRegionProfile
}
@@ -60,6 +70,7 @@ export class RegionProfileManager {
}
this._activeRegionProfile = regionProfile
+ this._onDidChangeRegionProfile.fire(regionProfile)
}
async generateQuickPickItem(): Promise[]> {
diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts
index 373c6fba2a9..15c878df8cf 100644
--- a/packages/core/src/codewhisperer/util/authUtil.ts
+++ b/packages/core/src/codewhisperer/util/authUtil.ts
@@ -107,7 +107,7 @@ export class AuthUtil {
public constructor(
public readonly auth = Auth.instance,
- public readonly regionProfileManager = new RegionProfileManager()
+ public readonly regionProfileManager = new RegionProfileManager(() => this.conn)
) {}
public initCodeWhispererHooks = once(() => {
@@ -139,13 +139,19 @@ export class AuthUtil {
await showAmazonQWalkthroughOnce()
}
})
+
+ this.regionProfileManager.onDidChangeRegionProfile(async () => {
+ await this.setVscodeContextProps()
+ })
})
public async setVscodeContextProps() {
await setContext('aws.codewhisperer.connected', this.isConnected())
- const doShowAmazonQLoginView = !this.isConnected() || this.isConnectionExpired()
+ const doShowAmazonQLoginView =
+ !this.isConnected() || this.isConnectionExpired() || this.requireProfileSelection()
await setContext('aws.amazonq.showLoginView', doShowAmazonQLoginView)
await setContext('aws.codewhisperer.connectionExpired', this.isConnectionExpired())
+ await setContext('aws.amazonq.connectedSsoIdc', isIdcSsoConnection(this.conn))
}
public reformatStartUrl(startUrl: string | undefined) {
@@ -296,6 +302,10 @@ export class AuthUtil {
return connectionExpired
}
+ private requireProfileSelection(): boolean {
+ return isIdcSsoConnection(this.conn) && this.regionProfileManager.activeRegionProfile === undefined
+ }
+
private logConnection() {
const logStr = indent(
`codewhisperer: connection states
@@ -461,12 +471,23 @@ export class AuthUtil {
}
if (isBuilderIdConnection(conn) || isIdcSsoConnection(conn) || isSageMaker()) {
+ // TODO: refactor
if (isValidCodeWhispererCoreConnection(conn)) {
- state[Features.codewhispererCore] = AuthStates.connected
+ if (this.requireProfileSelection()) {
+ state[Features.codewhispererCore] = AuthStates.pendingProfileSelection
+ } else {
+ state[Features.codewhispererCore] = AuthStates.connected
+ }
}
if (isValidAmazonQConnection(conn)) {
- for (const v of Object.values(Features)) {
- state[v as Feature] = AuthStates.connected
+ if (this.requireProfileSelection()) {
+ for (const v of Object.values(Features)) {
+ state[v as Feature] = AuthStates.pendingProfileSelection
+ }
+ } else {
+ for (const v of Object.values(Features)) {
+ state[v as Feature] = AuthStates.connected
+ }
}
}
}
diff --git a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
index 09e3ada0e62..65659276efb 100644
--- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
+++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
@@ -158,6 +158,7 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
this.authState = this.isReauthenticating ? 'REAUTHENTICATING' : 'REAUTHNEEDED'
return
} else if (featureAuthStates.amazonQ === 'pendingProfileSelection') {
+ this.authState = 'PENDING_PROFILE_SELECTION'
return
}
this.authState = 'LOGIN'
@@ -205,41 +206,18 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
async quitLoginScreen() {}
override listRegionProfiles(): Promise {
- // TODO: uncomment
- // return AuthUtil.instance.regionProfileManager.listRegionProfile()
-
- return Promise.resolve([
- {
- name: 'ACME platform work',
- region: 'us-east-1',
- arn: 'foo',
- description: 'Some description for ACME Platform Work',
- },
- {
- name: 'EU payments TEAM',
- region: 'us-east-1',
- arn: 'bar',
- description: 'Some description for EU payments TEAM',
- },
- {
- name: 'CodeWhisperer TEAM',
- region: 'us-east-1',
- arn: 'baz',
- description: 'Some description for CodeWhisperer TEAM',
- },
- ])
+ return AuthUtil.instance.regionProfileManager.listRegionProfile()
}
- override selectRegionProfile(profile: RegionProfile): Promise {
- // TODO: uncomment
- // return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile)
- return Promise.resolve()
+ override selectRegionProfile(profile: RegionProfile) {
+ return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile)
}
private setupConnectionEventEmitter(): void {
// allows the frontend to listen to Amazon Q auth events from the backend
const codeWhispererConnectionChanged = createThrottle(() => this.onActiveConnectionModified.fire())
AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(codeWhispererConnectionChanged)
+ AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(codeWhispererConnectionChanged)
/**
* Multiple events can be received in rapid succession and if
diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts
index 6768e457333..9ee14e2138c 100644
--- a/packages/core/src/shared/vscode/setContext.ts
+++ b/packages/core/src/shared/vscode/setContext.ts
@@ -19,6 +19,7 @@ export type contextKey =
| 'aws.amazonq.security.noMatches'
| 'aws.amazonq.notifications.show'
| 'aws.codecatalyst.connected'
+ | 'aws.amazonq.connectedSsoIdc'
| 'aws.codewhisperer.connected'
| 'aws.codewhisperer.connectionExpired'
| 'aws.codewhisperer.tutorial.workInProgress'
From 30d7142c1a497dc16a06f947a1cf19b9ea82e3d2 Mon Sep 17 00:00:00 2001
From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Date: Thu, 20 Mar 2025 08:28:47 -0700
Subject: [PATCH 05/49] feat(amazonq): chat panel region profile context menu
item
---
packages/amazonq/package.json | 13 ++++++++++++-
packages/core/src/shared/vscode/setContext.ts | 1 +
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index d46063854b6..48be5556674 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -340,7 +340,7 @@
},
{
"command": "aws.amazonq.openReferencePanel",
- "when": "view == aws.AmazonQChatView",
+ "when": "view == aws.amazonq.AmazonQChatView",
"group": "0_topAmazonQ@1"
},
{
@@ -348,6 +348,11 @@
"when": "view == aws.AmazonQChatView || view == aws.amazonq.AmazonCommonAuth",
"group": "1_amazonQ@1"
},
+ {
+ "command": "aws.amazonq.selectRegionProfile",
+ "when": "view == aws.AmazonQChatView && aws.amazonq.connectedSsoIdc == true",
+ "group": "1_amazonQ@1"
+ },
{
"command": "aws.amazonq.signout",
"when": "(view == aws.AmazonQChatView) && aws.codewhisperer.connected",
@@ -558,6 +563,12 @@
"category": "%AWS.amazonq.title%",
"enablement": "aws.codewhisperer.connected"
},
+ {
+ "command": "aws.amazonq.selectRegionProfile",
+ "title": "Switch Profile",
+ "category": "%AWS.amazonq.title%",
+ "enablement": "aws.codewhisperer.connected"
+ },
{
"command": "aws.amazonq.transformationHub.reviewChanges.acceptChanges",
"title": "%AWS.command.q.transform.acceptChanges%"
diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts
index 9ee14e2138c..c7e8c840521 100644
--- a/packages/core/src/shared/vscode/setContext.ts
+++ b/packages/core/src/shared/vscode/setContext.ts
@@ -18,6 +18,7 @@ export type contextKey =
| 'aws.amazonq.showLoginView'
| 'aws.amazonq.security.noMatches'
| 'aws.amazonq.notifications.show'
+ | 'aws.amazonq.connectedSsoIdc'
| 'aws.codecatalyst.connected'
| 'aws.amazonq.connectedSsoIdc'
| 'aws.codewhisperer.connected'
From 3eb9b78083490513bb2ecfe202b3c18b30e35b9e Mon Sep 17 00:00:00 2001
From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Date: Fri, 21 Mar 2025 14:31:15 -0700
Subject: [PATCH 06/49] feat(amazonq): profileManager implementation (#2102)
## Problem
add `ProfileManager`, a component manages Q Profile (for idc users only
for now)
A Q Profile has the following info
- Q service region
- Q service endpoint
- Arn
- Account
- ProfileName
major API
- getActiveProfile
- listAvailableProfiles
Functionalities
- `getActiveProfile`: return the current selected profile within current
workspace
- `switchProfile`: switch profile of current workspace to the provided
profile. Note that users are "ALLOWED" to select different profiles in
different VSCode instances.
- `persistence`: Q should memorize "last" selected option so that users
don't have to be prompted to select profile everytime
- `getServiceClientConfig`: as said above, profile will also affect the
service endpoint, thus `ProfileManager` will be as the single source of
true to retrieve Q related service endpoint onward.
## Solution
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../region/regionProfileManager.test.ts | 211 +++++++++++++++
.../src/codewhisperer/client/codewhisperer.ts | 15 +-
packages/core/src/codewhisperer/index.ts | 1 +
.../core/src/codewhisperer/models/model.ts | 5 +
.../region/regionProfileManager.ts | 250 ++++++++++++++++++
.../region/regionProfileNamager.ts | 101 -------
.../core/src/codewhisperer/util/authUtil.ts | 3 +-
packages/core/src/shared/globalState.ts | 1 +
packages/core/src/shared/settings.ts | 4 +-
packages/core/src/shared/vscode/setContext.ts | 1 -
10 files changed, 476 insertions(+), 116 deletions(-)
create mode 100644 packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts
create mode 100644 packages/core/src/codewhisperer/region/regionProfileManager.ts
delete mode 100644 packages/core/src/codewhisperer/region/regionProfileNamager.ts
diff --git a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts
new file mode 100644
index 00000000000..b867cbeb5d1
--- /dev/null
+++ b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts
@@ -0,0 +1,211 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as sinon from 'sinon'
+import assert, { fail } from 'assert'
+import { AuthUtil, RegionProfile, RegionProfileManager, defaultServiceConfig } from 'aws-core-vscode/codewhisperer'
+import { globals } from 'aws-core-vscode/shared'
+import { createTestAuth } from 'aws-core-vscode/test'
+import { SsoConnection } from 'aws-core-vscode/auth'
+
+const enterpriseSsoStartUrl = 'https://enterprise.awsapps.com/start'
+
+describe('RegionProfileManager', function () {
+ let sut: RegionProfileManager
+ let auth: ReturnType
+ let authUtil: AuthUtil
+
+ const profileFoo: RegionProfile = {
+ name: 'foo',
+ region: 'us-east-1',
+ arn: 'foo arn',
+ description: 'foo description',
+ }
+
+ async function setupConnection(type: 'builderId' | 'idc') {
+ if (type === 'builderId') {
+ await authUtil.connectToAwsBuilderId()
+ const conn = authUtil.conn
+ assert.strictEqual(conn?.type, 'sso')
+ assert.strictEqual(conn.label, 'AWS Builder ID')
+ } else if (type === 'idc') {
+ await authUtil.connectToEnterpriseSso(enterpriseSsoStartUrl, 'us-east-1')
+ const conn = authUtil.conn
+ assert.strictEqual(conn?.type, 'sso')
+ assert.strictEqual(conn.label, 'IAM Identity Center (enterprise)')
+ }
+ }
+
+ beforeEach(function () {
+ auth = createTestAuth(globals.globalState)
+ authUtil = new AuthUtil(auth)
+ sut = new RegionProfileManager(() => authUtil.conn)
+ })
+
+ afterEach(function () {
+ sinon.restore()
+ })
+
+ describe('list profiles', function () {
+ it('should call list profiles with different region endpoints', async function () {
+ await setupConnection('idc')
+ const listProfilesStub = sinon.stub().returns({
+ promise: () =>
+ Promise.resolve({
+ profiles: [
+ {
+ arn: 'arn',
+ profileName: 'foo',
+ },
+ ],
+ }),
+ })
+ const mockClient = {
+ listAvailableProfiles: listProfilesStub,
+ }
+ const createClientStub = sinon.stub(sut, 'createQClient').resolves(mockClient)
+
+ const r = await sut.listRegionProfile()
+
+ assert.strictEqual(r.length, 2)
+ assert.deepStrictEqual(r, [
+ {
+ name: 'foo',
+ arn: 'arn',
+ region: 'us-east-1',
+ description: '',
+ },
+ {
+ name: 'foo',
+ arn: 'arn',
+ region: 'eu-central-1',
+ description: '',
+ },
+ ])
+
+ assert.ok(createClientStub.calledTwice)
+ assert.ok(listProfilesStub.calledTwice)
+ })
+ })
+
+ describe('switch and get profile', function () {
+ it('should switch if connection is IdC', async function () {
+ await setupConnection('idc')
+ await sut.switchRegionProfile(profileFoo)
+ assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
+ })
+
+ it('should do nothing and return undefined if connection is builder id', async function () {
+ await setupConnection('builderId')
+ await sut.switchRegionProfile(profileFoo)
+ assert.deepStrictEqual(sut.activeRegionProfile, undefined)
+ })
+ })
+
+ describe(`client config`, function () {
+ it(`no valid credential should throw`, async function () {
+ assert.ok(authUtil.conn === undefined)
+
+ assert.throws(() => {
+ sut.clientConfig
+ }, /trying to get client configuration without credential/)
+ })
+
+ it(`builder id should always use default profile IAD`, async function () {
+ await setupConnection('builderId')
+ await sut.switchRegionProfile(profileFoo)
+ assert.deepStrictEqual(sut.activeRegionProfile, undefined)
+ const conn = authUtil.conn
+ if (!conn) {
+ fail('connection should not be undefined')
+ }
+
+ assert.deepStrictEqual(sut.clientConfig, defaultServiceConfig)
+ })
+
+ it(`idc should return correct endpoint corresponding to profile region`, async function () {
+ await setupConnection('idc')
+ await sut.switchRegionProfile({
+ name: 'foo',
+ region: 'eu-central-1',
+ arn: 'foo arn',
+ description: 'foo description',
+ })
+ assert.ok(sut.activeRegionProfile)
+ assert.deepStrictEqual(sut.clientConfig, {
+ region: 'eu-central-1',
+ endpoint: 'https://rts.prod-eu-central-1.codewhisperer.ai.aws.dev/',
+ })
+ })
+
+ it(`idc should throw if corresponding endpoint is not defined`, async function () {
+ await setupConnection('idc')
+ await sut.switchRegionProfile({
+ name: 'foo',
+ region: 'unknown region',
+ arn: 'foo arn',
+ description: 'foo description',
+ })
+
+ assert.throws(() => {
+ sut.clientConfig
+ }, /Q client configuration error, endpoint not found for region*/)
+ })
+ })
+
+ describe('persistence', function () {
+ it('persistSelectedRegionProfile', async function () {
+ await setupConnection('idc')
+ await sut.switchRegionProfile(profileFoo)
+ assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
+ const conn = authUtil.conn
+ if (!conn) {
+ fail('connection should not be undefined')
+ }
+
+ await sut.persistSelectRegionProfile()
+
+ const state = globals.globalState.tryGet<{ [label: string]: string }>(
+ 'aws.amazonq.regionProfiles',
+ Object,
+ {}
+ )
+
+ assert.strictEqual(state[conn.id], profileFoo.arn)
+ })
+
+ it(`restoreRegionProfile`, async function () {
+ sinon.stub(sut, 'listRegionProfile').resolves([profileFoo])
+ await setupConnection('idc')
+ const conn = authUtil.conn
+ if (!conn) {
+ fail('connection should not be undefined')
+ }
+
+ const state = {} as any
+ state[conn.id] = profileFoo.arn
+
+ await globals.globalState.update('aws.amazonq.regionProfiles', state)
+
+ await sut.restoreRegionProfile(conn)
+
+ assert.strictEqual(sut.activeRegionProfile, profileFoo)
+ })
+ })
+
+ describe('createQClient', function () {
+ it(`should configure the endpoint and region correspondingly`, async function () {
+ await setupConnection('idc')
+ await sut.switchRegionProfile(profileFoo)
+ assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
+ const conn = authUtil.conn as SsoConnection
+
+ const client = await sut.createQClient('eu-central-1', 'https://amazon.com/', conn)
+
+ assert.deepStrictEqual(client.config.region, 'eu-central-1')
+ assert.deepStrictEqual(client.endpoint.href, 'https://amazon.com/')
+ })
+ })
+})
diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts
index b2f9808a849..80dc94ee6df 100644
--- a/packages/core/src/codewhisperer/client/codewhisperer.ts
+++ b/packages/core/src/codewhisperer/client/codewhisperer.ts
@@ -23,24 +23,17 @@ import { indent } from '../../shared/utilities/textUtilities'
import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util'
import { extensionVersion, getServiceEnvVarConfig } from '../../shared/vscode/env'
import { DevSettings } from '../../shared/settings'
+import { CodeWhispererConfig } from '../models/model'
const keepAliveHeader = 'keep-alive-codewhisperer'
-export interface CodeWhispererConfig {
- readonly region: string
- readonly endpoint: string
-}
-
-export const defaultServiceConfig: CodeWhispererConfig = {
- region: 'us-east-1',
- endpoint: 'https://codewhisperer.us-east-1.amazonaws.com/',
-}
export function getCodewhispererConfig(): CodeWhispererConfig {
+ const clientConfig = AuthUtil.instance.regionProfileManager.clientConfig
return {
- ...DevSettings.instance.getServiceConfig('codewhispererService', defaultServiceConfig),
+ ...DevSettings.instance.getServiceConfig('codewhispererService', clientConfig),
// Environment variable overrides
- ...getServiceEnvVarConfig('codewhisperer', Object.keys(defaultServiceConfig)),
+ ...getServiceEnvVarConfig('codewhisperer', Object.keys(clientConfig)),
}
}
diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts
index 565e9e3c238..86b352dcfc3 100644
--- a/packages/core/src/codewhisperer/index.ts
+++ b/packages/core/src/codewhisperer/index.ts
@@ -103,3 +103,4 @@ export { Container } from './service/serviceContainer'
export * from './util/gitUtil'
export * from './ui/prompters'
export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker'
+export { RegionProfileManager, defaultServiceConfig } from './region/regionProfileManager'
diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts
index 3e632d94215..d174e2ab20e 100644
--- a/packages/core/src/codewhisperer/models/model.ts
+++ b/packages/core/src/codewhisperer/models/model.ts
@@ -48,6 +48,11 @@ export const vsCodeState: VsCodeState = {
isFreeTierLimitReached: false,
}
+export interface CodeWhispererConfig {
+ readonly region: string
+ readonly endpoint: string
+}
+
export interface RegionProfile {
name: string
region: string
diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts
new file mode 100644
index 00000000000..0b2c5bbe1f9
--- /dev/null
+++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts
@@ -0,0 +1,250 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { getIcon } from '../../shared/icons'
+import { DataQuickPickItem } from '../../shared/ui/pickerPrompter'
+import { CodeWhispererConfig, RegionProfile } from '../models/model'
+import { showConfirmationMessage } from '../../shared/utilities/messages'
+import {
+ Connection,
+ isBuilderIdConnection,
+ isIdcSsoConnection,
+ isSsoConnection,
+ SsoConnection,
+} from '../../auth/connection'
+import globals from '../../shared/extensionGlobals'
+import { once } from '../../shared/utilities/functionUtils'
+import CodeWhispererUserClient from '../client/codewhispereruserclient'
+import { Credentials, Service } from 'aws-sdk'
+import { ServiceOptions } from '../../shared/awsClientBuilder'
+import userApiConfig = require('../client/user-service-2.json')
+import { createConstantMap } from '../../shared/utilities/tsUtils'
+import { getLogger } from '../../shared/logger/logger'
+import { pageableToCollection } from '../../shared/utilities/collectionUtils'
+import { parse } from '@aws-sdk/util-arn-parser'
+import { ToolkitError } from '../../shared/errors'
+
+// TODO: is there a better way to manage all endpoint strings in one place?
+export const defaultServiceConfig: CodeWhispererConfig = {
+ region: 'us-east-1',
+ endpoint: 'https://codewhisperer.us-east-1.amazonaws.com/',
+}
+
+// Hack until we have a single discovery endpoint. We will call each endpoint one by one to fetch profile before then.
+// TODO: update correct endpoint and region
+const endpoints = createConstantMap({
+ 'us-east-1': 'https://codewhisperer.us-east-1.amazonaws.com/',
+ 'eu-central-1': 'https://rts.prod-eu-central-1.codewhisperer.ai.aws.dev/',
+})
+
+export class RegionProfileManager {
+ private static logger = getLogger()
+ private _activeRegionProfile: RegionProfile | undefined
+ private _onDidChangeRegionProfile = new vscode.EventEmitter()
+ public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event
+
+ get activeRegionProfile() {
+ return this._activeRegionProfile
+ }
+
+ get clientConfig(): CodeWhispererConfig {
+ const conn = this.connectionProvider()
+ if (!conn) {
+ throw new ToolkitError('trying to get client configuration without credential')
+ }
+
+ // builder id should simply use default IAD
+ if (isBuilderIdConnection(conn)) {
+ return defaultServiceConfig
+ }
+
+ // idc
+ const p = this.activeRegionProfile
+ if (p) {
+ const region = p.region
+ const endpoint = endpoints.get(p.region)
+ if (endpoint === undefined) {
+ RegionProfileManager.logger.error(
+ `Not found endpoint for region ${region}, not able to initialize a codewhisperer client`
+ )
+ throw new ToolkitError(`Q client configuration error, endpoint not found for region ${region}`)
+ }
+ return {
+ region: region,
+ endpoint: endpoint,
+ }
+ }
+
+ return defaultServiceConfig
+ }
+
+ constructor(private readonly connectionProvider: () => Connection | undefined) {}
+
+ async listRegionProfile(): Promise {
+ const conn = this.connectionProvider()
+ if (conn === undefined || !isSsoConnection(conn)) {
+ return []
+ }
+ const availableProfiles: RegionProfile[] = []
+ for (const [region, endpoint] of endpoints.entries()) {
+ const client = await this.createQClient(region, endpoint, conn as SsoConnection)
+ const requester = async (request: CodeWhispererUserClient.ListAvailableProfilesRequest) =>
+ client.listAvailableProfiles(request).promise()
+ const request: CodeWhispererUserClient.ListAvailableProfilesRequest = {}
+ try {
+ const profiles = await pageableToCollection(requester, request, 'nextToken', 'profiles')
+ .flatten()
+ .promise()
+ const mappedPfs = profiles.map((it) => {
+ let accntId = ''
+ try {
+ accntId = parse(it.arn).accountId
+ } catch (e) {}
+
+ return {
+ name: it.profileName,
+ region: region,
+ arn: it.arn,
+ description: accntId,
+ }
+ })
+
+ availableProfiles.push(...mappedPfs)
+ } catch (e) {
+ RegionProfileManager.logger.error(`failed to listRegionProfile: ${e}`)
+ return []
+ }
+
+ RegionProfileManager.logger.info(`available amazonq profiles: ${availableProfiles.length}`)
+ }
+
+ return availableProfiles
+ }
+
+ async switchRegionProfile(regionProfile: RegionProfile | undefined) {
+ const conn = this.connectionProvider()
+ if (conn === undefined || !isIdcSsoConnection(conn)) {
+ return
+ }
+
+ if (regionProfile === this.activeRegionProfile) {
+ return
+ }
+
+ // only prompt to users when users switch from A profile to B profile
+ if (this.activeRegionProfile !== undefined && regionProfile !== undefined) {
+ const response = await showConfirmationMessage({
+ prompt: `Do you want to switch Amazon Q profiles to ${regionProfile?.name}`,
+ confirm: 'Switch profiles',
+ cancel: 'Cancel',
+ })
+
+ if (!response) {
+ return
+ }
+ }
+
+ this._activeRegionProfile = regionProfile
+ }
+
+ restoreProfileSelection = once(async () => {
+ const conn = this.connectionProvider()
+ if (conn) {
+ await this.restoreRegionProfile(conn)
+ }
+ })
+
+ // Note: should be called after [AuthUtil.instance.conn] returns non null
+ async restoreRegionProfile(conn: Connection) {
+ const previousSelected = this.loadPersistedRegionProfle()[conn.id] || undefined
+ if (!previousSelected) {
+ return
+ }
+ // cross-validation
+ const profiles = this.listRegionProfile()
+ const r = (await profiles).find((it) => it.arn === previousSelected)
+
+ await this.switchRegionProfile(r)
+ }
+
+ private loadPersistedRegionProfle(): { [label: string]: string } {
+ const previousPersistedState = globals.globalState.tryGet<{ [label: string]: string }>(
+ 'aws.amazonq.regionProfiles',
+ Object,
+ {}
+ )
+
+ return previousPersistedState
+ }
+
+ async persistSelectRegionProfile() {
+ const conn = this.connectionProvider()
+ if (!conn || this.activeRegionProfile === undefined) {
+ return
+ }
+
+ // persist connectionId to profileArn
+ const previousPersistedState = globals.globalState.tryGet<{ [label: string]: string }>(
+ 'aws.amazonq.regionProfiles',
+ Object,
+ {}
+ )
+
+ previousPersistedState[conn.id] = this.activeRegionProfile.arn
+ await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState)
+ }
+
+ async generateQuickPickItem(): Promise[]> {
+ const selected = this.activeRegionProfile
+ const profiles = await this.listRegionProfile()
+ const icon = getIcon('vscode-account')
+ const quickPickItems: DataQuickPickItem[] = profiles.map((it) => {
+ const label = it.name
+ const onClick = async () => {
+ await this.switchRegionProfile(it)
+ }
+ const data = it.arn
+ const description = it.region
+ const isRecentlyUsed = selected ? selected.arn === it.arn : false
+
+ return {
+ label: `${icon} ${label}`,
+ onClick: onClick,
+ data: data,
+ description: description,
+ recentlyUsed: isRecentlyUsed,
+ detail: it.description,
+ }
+ })
+
+ return quickPickItems
+ }
+
+ async createQClient(region: string, endpoint: string, conn: SsoConnection): Promise {
+ const token = (await conn.getToken()).accessToken
+ const serviceOption: ServiceOptions = {
+ apiConfig: userApiConfig,
+ region: region,
+ endpoint: endpoint,
+ credentials: new Credentials({ accessKeyId: 'xxx', secretAccessKey: 'xxx' }),
+ onRequestSetup: [
+ (req) => {
+ req.on('build', ({ httpRequest }) => {
+ httpRequest.headers['Authorization'] = `Bearer ${token}`
+ })
+ },
+ ],
+ } as ServiceOptions
+
+ const c = (await globals.sdkClientBuilder.createAwsService(
+ Service,
+ serviceOption,
+ undefined
+ )) as CodeWhispererUserClient
+
+ return c
+ }
+}
diff --git a/packages/core/src/codewhisperer/region/regionProfileNamager.ts b/packages/core/src/codewhisperer/region/regionProfileNamager.ts
deleted file mode 100644
index e053a1469ce..00000000000
--- a/packages/core/src/codewhisperer/region/regionProfileNamager.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/*!
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as vscode from 'vscode'
-import { getIcon } from '../../shared/icons'
-import { DataQuickPickItem } from '../../shared/ui/pickerPrompter'
-import { RegionProfile } from '../models/model'
-import { showConfirmationMessage } from '../../shared/utilities/messages'
-import { Connection, isIdcSsoConnection } from '../../auth/connection'
-
-// TODO: Implementation
-export class RegionProfileManager {
- private _activeRegionProfile: RegionProfile | undefined
- private _onDidChangeRegionProfile = new vscode.EventEmitter()
- public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event
-
- public constructor(private readonly connectionProvider: () => Connection | undefined) {}
-
- get activeRegionProfile() {
- const conn = this.connectionProvider()
- if (conn === undefined || !isIdcSsoConnection(conn)) {
- return undefined
- }
- return this._activeRegionProfile
- }
-
- // TODO: Implementation
- async listRegionProfile(): Promise {
- return [
- {
- name: 'ACME platform work',
- region: 'us-east-1',
- arn: 'foo',
- description: 'Some description for ACME Platform Work',
- },
- {
- name: 'EU payments TEAM',
- region: 'us-east-1',
- arn: 'bar',
- description: 'Some description for EU payments TEAM',
- },
- {
- name: 'CodeWhisperer TEAM',
- region: 'us-east-1',
- arn: 'baz',
- description: 'Some description for CodeWhisperer TEAM',
- },
- ]
- }
-
- // TODO: Implementation
- async switchRegionProfile(regionProfile: RegionProfile | undefined) {
- if (regionProfile === this.activeRegionProfile) {
- return
- }
-
- // only prompt to users when users switch from A profile to B profile
- if (this.activeRegionProfile !== undefined && regionProfile !== undefined) {
- const response = await showConfirmationMessage({
- prompt: `Do you want to switch Amazon Q profiles to ${regionProfile?.name}`,
- confirm: 'Switch profiles',
- cancel: 'Cancel',
- })
-
- if (!response) {
- return
- }
- }
-
- this._activeRegionProfile = regionProfile
- this._onDidChangeRegionProfile.fire(regionProfile)
- }
-
- async generateQuickPickItem(): Promise[]> {
- const selected = this.activeRegionProfile
- const profiles = await this.listRegionProfile()
- const icon = getIcon('vscode-account')
- const quickPickItems: DataQuickPickItem[] = profiles.map((it) => {
- const label = it.name
- const onClick = async () => {
- await this.switchRegionProfile(it)
- }
- const data = it.arn
- const description = it.region
- const isRecentlyUsed = selected ? selected.arn === it.arn : false
-
- return {
- label: `${icon} ${label}`,
- onClick: onClick,
- data: data,
- description: description,
- recentlyUsed: isRecentlyUsed,
- detail: it.description,
- }
- })
-
- return quickPickItems
- }
-}
diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts
index 15c878df8cf..9a83db91383 100644
--- a/packages/core/src/codewhisperer/util/authUtil.ts
+++ b/packages/core/src/codewhisperer/util/authUtil.ts
@@ -45,7 +45,7 @@ import { asStringifiedStack } from '../../shared/telemetry/spans'
import { withTelemetryContext } from '../../shared/telemetry/util'
import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands'
import { throttle } from 'lodash'
-import { RegionProfileManager } from '../region/regionProfileNamager'
+import { RegionProfileManager } from '../region/regionProfileManager'
/** Backwards compatibility for connections w pre-chat scopes */
export const codeWhispererCoreScopes = [...scopesCodeWhispererCore]
export const codeWhispererChatScopes = [...codeWhispererCoreScopes, ...scopesCodeWhispererChat]
@@ -124,6 +124,7 @@ export class AuthUtil {
getLogger().info(`codewhisperer: active connection changed`)
if (this.isValidEnterpriseSsoInUse()) {
void vscode.commands.executeCommand('aws.amazonq.notifyNewCustomizations')
+ await this.regionProfileManager.restoreProfileSelection()
}
vsCodeState.isFreeTierLimitReached = false
await Promise.all([
diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts
index 80f4148d435..810a5a254c8 100644
--- a/packages/core/src/shared/globalState.ts
+++ b/packages/core/src/shared/globalState.ts
@@ -46,6 +46,7 @@ export type globalKey =
| 'aws.toolkit.amazonqInstall.dismissed'
| 'aws.amazonq.workspaceIndexToggleOn'
| 'aws.amazonq.customization.overrideV2'
+ | 'aws.amazonq.regionProfiles'
// Deprecated/legacy names. New keys should start with "aws.".
| '#sessionCreationDates' // Legacy name from `ssoAccessTokenProvider.ts`.
| 'CODECATALYST_RECONNECT'
diff --git a/packages/core/src/shared/settings.ts b/packages/core/src/shared/settings.ts
index a486784fe14..4df116a76db 100644
--- a/packages/core/src/shared/settings.ts
+++ b/packages/core/src/shared/settings.ts
@@ -5,7 +5,6 @@
import * as vscode from 'vscode'
import * as codecatalyst from './clients/codecatalystClient'
-import * as codewhisperer from '../codewhisperer/client/codewhisperer'
import { getLogger } from './logger/logger'
import {
cast,
@@ -23,6 +22,7 @@ import { telemetry } from './telemetry/telemetry'
import globals from './extensionGlobals'
import toolkitSettings from './settings-toolkit.gen'
import amazonQSettings from './settings-amazonq.gen'
+import { CodeWhispererConfig } from '../codewhisperer/models/model'
type Workspace = Pick
@@ -768,7 +768,7 @@ type AwsDevSetting = keyof ResolvedDevSettings
type ServiceClients = keyof ServiceTypeMap
interface ServiceTypeMap {
codecatalystService: codecatalyst.CodeCatalystConfig
- codewhispererService: codewhisperer.CodeWhispererConfig
+ codewhispererService: CodeWhispererConfig
}
/**
diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts
index c7e8c840521..ce308b7cff8 100644
--- a/packages/core/src/shared/vscode/setContext.ts
+++ b/packages/core/src/shared/vscode/setContext.ts
@@ -20,7 +20,6 @@ export type contextKey =
| 'aws.amazonq.notifications.show'
| 'aws.amazonq.connectedSsoIdc'
| 'aws.codecatalyst.connected'
- | 'aws.amazonq.connectedSsoIdc'
| 'aws.codewhisperer.connected'
| 'aws.codewhisperer.connectionExpired'
| 'aws.codewhisperer.tutorial.workInProgress'
From 117f9c646b37b56faab00d80b487c6820a331bec Mon Sep 17 00:00:00 2001
From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Date: Mon, 24 Mar 2025 13:58:23 -0700
Subject: [PATCH 07/49] feat(amazonq): region expansion UX improvement (#2094)
## Problem
- hide profile UI when there is only 1 profile
- handle case where `ListAvailableProfile` call fails, will assume IAD
and allow user to use Q
- cancel ongoing Chat streaming onProfileChanged
## Solution
related PRs
1. quick pick #2098
2. chat panel context menu #2099
3. profile selection page #2100
5. webview integration #2101
6. #2102
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
packages/core/package.nls.json | 1 +
.../webview/generators/webViewContent.ts | 15 +++----
packages/core/src/amazonq/webview/ui/main.ts | 1 +
.../region/regionProfileManager.ts | 45 ++++++++++++++++++-
.../codewhisperer/ui/codeWhispererNodes.ts | 6 ++-
.../core/src/codewhisperer/util/authUtil.ts | 4 ++
.../controllers/chat/controller.ts | 16 ++++++-
.../controllers/chat/messenger/messenger.ts | 7 ++-
.../webview/vue/regionProfileSelector.vue | 5 ++-
9 files changed, 87 insertions(+), 13 deletions(-)
diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json
index 63b623d24a9..31ee1ac9c2d 100644
--- a/packages/core/package.nls.json
+++ b/packages/core/package.nls.json
@@ -265,6 +265,7 @@
"AWS.command.codewhisperer.signout": "Sign Out",
"AWS.command.codewhisperer.reconnect": "Reconnect",
"AWS.command.codewhisperer.openReferencePanel": "Open Code Reference Log",
+ "AWS.command.q.selectRegionProfile": "Select Profile",
"AWS.command.q.transform.acceptChanges": "Accept",
"AWS.command.q.transform.rejectChanges": "Reject",
"AWS.command.q.transform.stopJobInHub": "Stop job",
diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts
index 9fb9e0eb80f..28792e867d3 100644
--- a/packages/core/src/amazonq/webview/generators/webViewContent.ts
+++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts
@@ -88,14 +88,13 @@ export class WebViewContentGenerator {
// only show profile card when the two conditions
// 1. profile count >= 2
// 2. not default (fallback) which has empty arn
- const regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile
- // TODO: uncomment
- // if (
- // regionProfile &&
- // (regionProfile.arn.length === 0 || AuthUtil.instance.regionProfileManager.profiles.length < 2)
- // ) {
- // regionProfile = undefined
- // }
+ let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile
+ if (
+ (regionProfile && AuthUtil.instance.regionProfileManager.isDefault(regionProfile)) ||
+ AuthUtil.instance.regionProfileManager.profiles.length === 1
+ ) {
+ regionProfile = undefined
+ }
const regionProfileString: string = JSON.stringify(regionProfile)
diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts
index 626b3427caa..e6c4d284286 100644
--- a/packages/core/src/amazonq/webview/ui/main.ts
+++ b/packages/core/src/amazonq/webview/ui/main.ts
@@ -209,6 +209,7 @@ export const createMynahUI = (
isDocEnabled,
disabledCommands,
commandHighlight: highlightCommand,
+ regionProfile,
})
featureConfigs = tryNewMap(featureConfigsSerialized)
diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts
index 0b2c5bbe1f9..dbf8740693b 100644
--- a/packages/core/src/codewhisperer/region/regionProfileManager.ts
+++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts
@@ -27,6 +27,13 @@ import { pageableToCollection } from '../../shared/utilities/collectionUtils'
import { parse } from '@aws-sdk/util-arn-parser'
import { ToolkitError } from '../../shared/errors'
+const defaultProfile: RegionProfile = {
+ name: 'default',
+ region: 'us-east-1',
+ arn: '',
+ description: 'defaultProfile when listAvailableProfiles fails',
+}
+
// TODO: is there a better way to manage all endpoint strings in one place?
export const defaultServiceConfig: CodeWhispererConfig = {
region: 'us-east-1',
@@ -46,7 +53,14 @@ export class RegionProfileManager {
private _onDidChangeRegionProfile = new vscode.EventEmitter()
public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event
+ // 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[] = []
+
get activeRegionProfile() {
+ const conn = this.connectionProvider()
+ if (conn === undefined || !isIdcSsoConnection(conn)) {
+ return undefined
+ }
return this._activeRegionProfile
}
@@ -81,6 +95,10 @@ export class RegionProfileManager {
return defaultServiceConfig
}
+ get profiles(): RegionProfile[] {
+ return this._profiles
+ }
+
constructor(private readonly connectionProvider: () => Connection | undefined) {}
async listRegionProfile(): Promise {
@@ -115,12 +133,14 @@ export class RegionProfileManager {
availableProfiles.push(...mappedPfs)
} catch (e) {
RegionProfileManager.logger.error(`failed to listRegionProfile: ${e}`)
+ await this.switchRegionProfile(defaultProfile)
return []
}
RegionProfileManager.logger.info(`available amazonq profiles: ${availableProfiles.length}`)
}
+ this._profiles = availableProfiles
return availableProfiles
}
@@ -147,7 +167,20 @@ export class RegionProfileManager {
}
}
+ await this._switchRegionProfile(regionProfile)
+ }
+
+ private async _switchRegionProfile(regionProfile: RegionProfile | undefined) {
this._activeRegionProfile = regionProfile
+
+ this._onDidChangeRegionProfile.fire(regionProfile)
+ // dont show if it's a default (fallback)
+ if (regionProfile && !this.isDefault(regionProfile) && this.profiles.length > 1) {
+ void vscode.window.showInformationMessage(`You are using the ${regionProfile.name} profile for Q.`).then()
+ }
+
+ // persist to state
+ await this.persistSelectRegionProfile()
}
restoreProfileSelection = once(async () => {
@@ -182,7 +215,9 @@ export class RegionProfileManager {
async persistSelectRegionProfile() {
const conn = this.connectionProvider()
- if (!conn || this.activeRegionProfile === undefined) {
+
+ // default has empty arn and shouldn't be persisted because it's just a fallback
+ if (!conn || this.activeRegionProfile === undefined || this.isDefault(this.activeRegionProfile)) {
return
}
@@ -197,6 +232,14 @@ export class RegionProfileManager {
await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState)
}
+ isDefault(profile: RegionProfile): boolean {
+ return (
+ profile.arn === defaultProfile.arn &&
+ profile.name === defaultProfile.name &&
+ profile.region === defaultProfile.region
+ )
+ }
+
async generateQuickPickItem(): Promise[]> {
const selected = this.activeRegionProfile
const profiles = await this.listRegionProfile()
diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
index 67b21f4362a..0a91d2e8a63 100644
--- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
+++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
@@ -139,7 +139,11 @@ export function createSelectCustomization(): DataQuickPickItem<'selectCustomizat
}
export function createSelectRegionProfileNode(): DataQuickPickItem<'selectRegionProfile'> {
- const selectedRegionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile
+ let selectedRegionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile
+ // default shouldn't be shown as it's saying ListAvailableProfiles fail and we fallback to IAD
+ if (selectedRegionProfile && AuthUtil.instance.regionProfileManager.isDefault(selectedRegionProfile)) {
+ selectedRegionProfile = undefined
+ }
const label = 'Switch Profile'
const icon = getIcon('vscode-arrow-swap')
diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts
index 9a83db91383..a85dc272a71 100644
--- a/packages/core/src/codewhisperer/util/authUtil.ts
+++ b/packages/core/src/codewhisperer/util/authUtil.ts
@@ -139,6 +139,10 @@ export class AuthUtil {
if (this.isValidEnterpriseSsoInUse() || (this.isBuilderIdInUse() && !this.isConnectionExpired())) {
await showAmazonQWalkthroughOnce()
}
+
+ if (!this.isConnected()) {
+ await this.regionProfileManager.switchRegionProfile(undefined)
+ }
})
this.regionProfileManager.onDidChangeRegionProfile(async () => {
diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
index 846f3c6e445..3c1628c7aa3 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
@@ -141,6 +141,7 @@ export class ChatController {
private readonly userIntentRecognizer: UserIntentRecognizer
private readonly telemetryHelper: CWCTelemetryHelper
private userPromptsWatcher: vscode.FileSystemWatcher | undefined
+ private cancelTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource()
public constructor(
private readonly chatControllerMessageListeners: ChatControllerMessageListeners,
@@ -167,6 +168,10 @@ export class ChatController {
}
})
+ AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => {
+ this.cancelTokenSource.cancel()
+ })
+
this.chatControllerMessageListeners.processPromptChatMessage.onMessage((data) => {
const uiEvents = uiEventRecorder.get(data.tabID)
if (uiEvents) {
@@ -1133,6 +1138,7 @@ export class ChatController {
)
let response: MessengerResponseType | undefined = undefined
session.createNewTokenSource()
+ // TODO: onProfileChanged, abort previous response?
try {
this.messenger.sendInitalStream(tabID, triggerID, triggerPayload.documentReferences)
this.telemetryHelper.setConversationStreamStartTime(tabID)
@@ -1157,7 +1163,15 @@ export class ChatController {
response.$metadata.requestId
} metadata: ${inspect(response.$metadata, { depth: 12 })}`
)
- await this.messenger.sendAIResponse(response, session, tabID, triggerID, triggerPayload)
+ this.cancelTokenSource = new vscode.CancellationTokenSource()
+ await this.messenger.sendAIResponse(
+ response,
+ session,
+ tabID,
+ triggerID,
+ triggerPayload,
+ this.cancelTokenSource.token
+ )
} catch (e: any) {
this.telemetryHelper.recordMessageResponseError(triggerPayload, tabID, getHttpStatusCode(e) ?? 0)
// clears session, record telemetry before this call
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index dd80676cf8b..78e3ea373bc 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import * as vscode from 'vscode'
import { waitUntil } from '../../../../shared/utilities/timeoutUtils'
import {
AppToWebViewMessageDispatcher,
@@ -121,7 +122,8 @@ export class Messenger {
session: ChatSession,
tabID: string,
triggerID: string,
- triggerPayload: TriggerPayload
+ triggerPayload: TriggerPayload,
+ cancelToken: vscode.CancellationToken
) {
let message = ''
const messageID = response.$metadata.requestId ?? ''
@@ -159,6 +161,9 @@ export class Messenger {
waitUntil(
async () => {
for await (const chatEvent of response.message!) {
+ if (cancelToken.isCancellationRequested) {
+ return
+ }
for (const key of keys(chatEvent)) {
if ((chatEvent[key] as any) !== undefined) {
eventCounts.set(key, (eventCounts.get(key) ?? 0) + 1)
diff --git a/packages/core/src/login/webview/vue/regionProfileSelector.vue b/packages/core/src/login/webview/vue/regionProfileSelector.vue
index a74e707856b..40edd9f911f 100644
--- a/packages/core/src/login/webview/vue/regionProfileSelector.vue
+++ b/packages/core/src/login/webview/vue/regionProfileSelector.vue
@@ -118,8 +118,11 @@ export default defineComponent({
async created() {
this.doShow = true
},
- async mounted() {
+ async beforeMount() {
this.availableRegionProfiles = await client.listRegionProfiles()
+ if (this.availableRegionProfiles.length === 1) {
+ await client.selectRegionProfile(this.availableRegionProfiles[0])
+ }
},
methods: {
toggleItemSelection(itemId: number) {
From 0c742e7f794d1c7b18f4048fadd708301770eaa9 Mon Sep 17 00:00:00 2001
From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Date: Thu, 27 Mar 2025 14:02:51 -0700
Subject: [PATCH 08/49] fix(amazonq): fit & finish / bugbash issues
## Problem
1. css style
2. error handling
3. agents quick action command is gone after users select profile
---
packages/amazonq/package.json | 4 +-
.../region/regionProfileManager.test.ts | 32 ++++++
.../webview/generators/webViewContent.ts | 16 ++-
.../src/amazonq/webview/ui/tabs/generator.ts | 2 +-
packages/core/src/amazonq/webview/webView.ts | 17 ++--
.../amazonqTest/chat/storages/chatSession.ts | 7 +-
.../codewhisperer/commands/basicCommands.ts | 12 ++-
.../region/regionProfileManager.ts | 47 +++++++--
.../service/recommendationService.ts | 6 ++
.../codewhisperer/ui/codeWhispererNodes.ts | 2 +-
.../src/codewhisperer/ui/statusBarMenu.ts | 4 +
.../core/src/codewhisperer/util/authUtil.ts | 7 +-
.../webview/vue/amazonq/backend_amazonq.ts | 12 ++-
.../core/src/login/webview/vue/backend.ts | 2 +-
.../webview/vue/regionProfileSelector.vue | 99 +++++++++----------
.../webview/vue/toolkit/backend_toolkit.ts | 2 +-
16 files changed, 187 insertions(+), 84 deletions(-)
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index d2d25985f9e..c99d83300da 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -351,7 +351,7 @@
},
{
"command": "aws.amazonq.selectRegionProfile",
- "when": "view == aws.AmazonQChatView && aws.amazonq.connectedSsoIdc == true",
+ "when": "view == aws.amazonq.AmazonQChatView && aws.amazonq.connectedSsoIdc == true",
"group": "1_amazonQ@1"
},
{
@@ -566,7 +566,7 @@
},
{
"command": "aws.amazonq.selectRegionProfile",
- "title": "Switch Profile",
+ "title": "Change Profile",
"category": "%AWS.amazonq.title%",
"enablement": "aws.codewhisperer.connected"
},
diff --git a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts
index b867cbeb5d1..3f948c5be10 100644
--- a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts
@@ -195,6 +195,38 @@ describe('RegionProfileManager', function () {
})
})
+ describe('invalidate', function () {
+ it('should reset activeProfile and global state', async function () {
+ // setup
+ await setupConnection('idc')
+ await sut.switchRegionProfile(profileFoo)
+ assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
+ const conn = authUtil.conn
+ if (!conn) {
+ fail('connection should not be undefined')
+ }
+ await sut.persistSelectRegionProfile()
+ const state = globals.globalState.tryGet<{ [label: string]: string }>(
+ 'aws.amazonq.regionProfiles',
+ Object,
+ {}
+ )
+ assert.strictEqual(state[conn.id], profileFoo.arn)
+
+ // subject to test
+ await sut.invalidateProfile(profileFoo.arn)
+
+ // assertion
+ assert.strictEqual(sut.activeRegionProfile, undefined)
+ const actualGlobalState = globals.globalState.tryGet<{ [label: string]: string }>(
+ 'aws.amazonq.regionProfiles',
+ Object,
+ {}
+ )
+ assert.deepStrictEqual(actualGlobalState, {})
+ })
+ })
+
describe('createQClient', function () {
it(`should configure the endpoint and region correspondingly`, async function () {
await setupConnection('idc')
diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts
index 28792e867d3..dbfce0c7f18 100644
--- a/packages/core/src/amazonq/webview/generators/webViewContent.ts
+++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts
@@ -98,14 +98,24 @@ export class WebViewContentGenerator {
const regionProfileString: string = JSON.stringify(regionProfile)
+ // AuthUtil.instance.getChatAuthState is throttled version which possibly return an old snapshot of auth state however webview initialization here requires the latest accurate
+ // otherwise features will be disabled as auth still says it's not connected & profile selected
+ const authState = (await AuthUtil.instance._getChatAuthState()).amazonQ
+
return `
${cssLinks}
`
diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts
index f217e48f243..e2b08a5e00e 100644
--- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts
+++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts
@@ -57,7 +57,7 @@ export class TabDataGenerator {
? undefined
: {
type: ChatItemType.ANSWER,
- body: `You are using the ${this.regionProfile?.name} profile for this chat`,
+ body: `You are using the ${this.regionProfile?.name} profile for this chat period`,
status: 'info',
messageId: 'regionProfile',
}
diff --git a/packages/core/src/amazonq/webview/webView.ts b/packages/core/src/amazonq/webview/webView.ts
index f5e0c6e2010..70683e65bed 100644
--- a/packages/core/src/amazonq/webview/webView.ts
+++ b/packages/core/src/amazonq/webview/webView.ts
@@ -36,6 +36,15 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
) {
registerAssetsHttpsFileSystem(extensionContext)
this.webViewContentGenerator = new WebViewContentGenerator()
+
+ AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => {
+ if (this.webView) {
+ this.webView.html = await this.webViewContentGenerator.generate(
+ this.extensionContext.extensionUri,
+ this.webView
+ )
+ }
+ })
}
public async resolveWebviewView(
@@ -47,13 +56,6 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
this.onDidChangeAmazonQVisibility.fire(webviewView.visible)
})
- AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => {
- webviewView.webview.html = await this.webViewContentGenerator.generate(
- this.extensionContext.extensionUri,
- webviewView.webview
- )
- })
-
const dist = Uri.joinPath(this.extensionContext.extensionUri, 'dist')
const resources = Uri.joinPath(this.extensionContext.extensionUri, 'resources')
webviewView.webview.options = {
@@ -71,6 +73,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider {
webviewView.webview
)
+ this.webView = webviewView.webview
performance.mark(amazonqMark.open)
}
}
diff --git a/packages/core/src/amazonqTest/chat/storages/chatSession.ts b/packages/core/src/amazonqTest/chat/storages/chatSession.ts
index a8a3ccf429d..d99b3c551da 100644
--- a/packages/core/src/amazonqTest/chat/storages/chatSession.ts
+++ b/packages/core/src/amazonqTest/chat/storages/chatSession.ts
@@ -6,6 +6,7 @@
import { Session } from '../session/session'
import { getLogger } from '../../../shared/logger/logger'
+import { AuthUtil } from '../../../codewhisperer/util/authUtil'
export class SessionNotFoundError extends Error {}
@@ -14,7 +15,11 @@ export class ChatSessionManager {
private activeSession: Session | undefined
private isInProgress: boolean = false
- constructor() {}
+ constructor() {
+ AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => {
+ this.removeActiveTab()
+ })
+ }
public static get Instance() {
return this._instance || (this._instance = new this())
diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts
index 97a0bafe97e..e27a0e59460 100644
--- a/packages/core/src/codewhisperer/commands/basicCommands.ts
+++ b/packages/core/src/codewhisperer/commands/basicCommands.ts
@@ -69,7 +69,7 @@ import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker'
import { parsePatch } from 'diff'
import { createCodeIssueGroupingStrategyPrompter } from '../ui/prompters'
import { cancel, confirm } from '../../shared/localizedText'
-import { DataQuickPickItem, showQuickPick } from '../../shared/ui/pickerPrompter'
+import { showQuickPick } from '../../shared/ui/pickerPrompter'
import { i18n } from '../../shared/i18n-helper'
const MessageTimeOut = 5_000
@@ -253,12 +253,14 @@ export const selectCustomizationPrompt = Commands.declare(
export const selectRegionProfileCommand = Commands.declare(
{ id: 'aws.amazonq.selectRegionProfile', compositeKey: { 1: 'source' } },
() => async (_: VsCodeCommandArg, source: CodeWhispererSource) => {
- const quickPickItems: DataQuickPickItem[] =
- await AuthUtil.instance.regionProfileManager.generateQuickPickItem()
+ const quickPickItems = AuthUtil.instance.regionProfileManager.generateQuickPickItem()
await showQuickPick(quickPickItems, {
- title: localize('AWS.q.profile.quickPick.title', 'Select a Profile'),
- placeholder: localize('AWS.q.profile.quickPick.placeholder', 'You have access to the following profiles'),
+ title: localize('AWS.amazonq.profile.quickPick.title', 'Select a Profile'),
+ placeholder: localize(
+ 'AWS.amazonq.profile.quickPick.placeholder',
+ 'You can choose from the following profiles:'
+ ),
recentlyUsed: i18n('AWS.codewhisperer.customization.selected'),
})
}
diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts
index dbf8740693b..89456be7611 100644
--- a/packages/core/src/codewhisperer/region/regionProfileManager.ts
+++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts
@@ -25,7 +25,8 @@ import { createConstantMap } from '../../shared/utilities/tsUtils'
import { getLogger } from '../../shared/logger/logger'
import { pageableToCollection } from '../../shared/utilities/collectionUtils'
import { parse } from '@aws-sdk/util-arn-parser'
-import { ToolkitError } from '../../shared/errors'
+import { isAwsError, ToolkitError } from '../../shared/errors'
+import { localize } from '../../shared/utilities/vsCodeUtils'
const defaultProfile: RegionProfile = {
name: 'default',
@@ -58,7 +59,7 @@ export class RegionProfileManager {
get activeRegionProfile() {
const conn = this.connectionProvider()
- if (conn === undefined || !isIdcSsoConnection(conn)) {
+ if (isBuilderIdConnection(conn)) {
return undefined
}
return this._activeRegionProfile
@@ -132,9 +133,9 @@ export class RegionProfileManager {
availableProfiles.push(...mappedPfs)
} catch (e) {
- RegionProfileManager.logger.error(`failed to listRegionProfile: ${e}`)
- await this.switchRegionProfile(defaultProfile)
- return []
+ const logMsg = isAwsError(e) ? `requestId=${e.requestId}; message=${e.message}` : (e as Error).message
+ RegionProfileManager.logger.error(`failed to listRegionProfile: ${logMsg}`)
+ throw e
}
RegionProfileManager.logger.info(`available amazonq profiles: ${availableProfiles.length}`)
@@ -150,16 +151,21 @@ export class RegionProfileManager {
return
}
- if (regionProfile === this.activeRegionProfile) {
+ if (regionProfile && this.activeRegionProfile && regionProfile.arn === this.activeRegionProfile.arn) {
return
}
// only prompt to users when users switch from A profile to B profile
if (this.activeRegionProfile !== undefined && regionProfile !== undefined) {
const response = await showConfirmationMessage({
- prompt: `Do you want to switch Amazon Q profiles to ${regionProfile?.name}`,
+ prompt: localize(
+ 'AWS.amazonq.profile.confirmation',
+ "Do you want to change your Q Developer profile to '{0}'?\n When you change profiles, you will no longer have access to your current customizations, chats, code reviews, or any other code or content being generated by Amazon Q",
+ regionProfile?.name
+ ),
confirm: 'Switch profiles',
cancel: 'Cancel',
+ type: 'info',
})
if (!response) {
@@ -242,7 +248,18 @@ export class RegionProfileManager {
async generateQuickPickItem(): Promise[]> {
const selected = this.activeRegionProfile
- const profiles = await this.listRegionProfile()
+ let profiles: RegionProfile[] = []
+ try {
+ profiles = await this.listRegionProfile()
+ } catch (e) {
+ return [
+ {
+ label: '[Failed to list available profiles]',
+ detail: `${(e as Error).message}`,
+ data: '',
+ },
+ ]
+ }
const icon = getIcon('vscode-account')
const quickPickItems: DataQuickPickItem[] = profiles.map((it) => {
const label = it.name
@@ -266,6 +283,20 @@ export class RegionProfileManager {
return quickPickItems
}
+ async invalidateProfile(arn: string | undefined) {
+ if (arn) {
+ if (this.activeRegionProfile && this.activeRegionProfile.arn === arn) {
+ this._activeRegionProfile = undefined
+ }
+
+ const profiles = this.loadPersistedRegionProfle()
+ const updatedProfiles = Object.fromEntries(
+ Object.entries(profiles).filter(([connId, profileArn]) => profileArn !== arn)
+ )
+ await globals.globalState.update('aws.amazonq.regionProfiles', updatedProfiles)
+ }
+ }
+
async createQClient(region: string, endpoint: string, conn: SsoConnection): Promise {
const token = (await conn.getToken()).accessToken
const serviceOption: ServiceOptions = {
diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts
index 1da76995781..de78b435913 100644
--- a/packages/core/src/codewhisperer/service/recommendationService.ts
+++ b/packages/core/src/codewhisperer/service/recommendationService.ts
@@ -15,6 +15,7 @@ import { ClassifierTrigger } from './classifierTrigger'
import { DefaultCodeWhispererClient } from '../client/codewhisperer'
import { randomUUID } from '../../shared/crypto'
import { TelemetryHelper } from '../util/telemetryHelper'
+import { AuthUtil } from '../util/authUtil'
export interface SuggestionActionEvent {
readonly editor: vscode.TextEditor | undefined
@@ -66,6 +67,11 @@ export class RecommendationService {
autoTriggerType?: CodewhispererAutomatedTriggerType,
event?: vscode.TextDocumentChangeEvent
) {
+ // TODO: should move all downstream auth check(inlineCompletionService, recommendationHandler etc) to here(upstream) instead of spreading everywhere
+ if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) {
+ return
+ }
+
if (this._isRunning) {
return
}
diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
index 0a91d2e8a63..9f90189c2ff 100644
--- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
+++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts
@@ -145,7 +145,7 @@ export function createSelectRegionProfileNode(): DataQuickPickItem<'selectRegion
selectedRegionProfile = undefined
}
- const label = 'Switch Profile'
+ const label = 'Change Profile'
const icon = getIcon('vscode-arrow-swap')
const description = selectedRegionProfile ? `Current profile: ${selectedRegionProfile.name}` : ''
diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts
index 190c3c09d80..ddc1d28e8d0 100644
--- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts
+++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts
@@ -43,6 +43,10 @@ function getAmazonQCodeWhispererNodes() {
return [createSignIn(), createLearnMore()]
}
+ if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) {
+ return []
+ }
+
if (vsCodeState.isFreeTierLimitReached) {
if (hasVendedIamCredentials()) {
return [createFreeTierLimitMet(), createOpenReferenceLog()]
diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts
index a85dc272a71..bfca8b6f06d 100644
--- a/packages/core/src/codewhisperer/util/authUtil.ts
+++ b/packages/core/src/codewhisperer/util/authUtil.ts
@@ -141,7 +141,7 @@ export class AuthUtil {
}
if (!this.isConnected()) {
- await this.regionProfileManager.switchRegionProfile(undefined)
+ await this.regionProfileManager.invalidateProfile(this.regionProfileManager.activeRegionProfile?.arn)
}
})
@@ -307,7 +307,10 @@ export class AuthUtil {
return connectionExpired
}
- private requireProfileSelection(): boolean {
+ requireProfileSelection(): boolean {
+ if (isBuilderIdConnection(this.conn)) {
+ return false
+ }
return isIdcSsoConnection(this.conn) && this.regionProfileManager.activeRegionProfile === undefined
}
diff --git a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
index a9fc2898d6c..72d1f80e734 100644
--- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
+++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts
@@ -206,8 +206,16 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
/** If users are unauthenticated in Q/CW, we should always display the auth screen. */
async quitLoginScreen() {}
- override listRegionProfiles(): Promise {
- return AuthUtil.instance.regionProfileManager.listRegionProfile()
+ /**
+ * The purpose of returning Error.message is to notify vue frontend that API call fails and to render corresponding error message to users
+ * @returns ProfileList when API call succeeds, otherwise Error.message
+ */
+ override async listRegionProfiles(): Promise {
+ try {
+ return await AuthUtil.instance.regionProfileManager.listRegionProfile()
+ } catch (e) {
+ return (e as Error).message
+ }
}
override selectRegionProfile(profile: RegionProfile) {
diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts
index c595fbe0f10..25e4b046205 100644
--- a/packages/core/src/login/webview/vue/backend.ts
+++ b/packages/core/src/login/webview/vue/backend.ts
@@ -207,7 +207,7 @@ export abstract class CommonAuthWebview extends VueWebview {
/** List current connections known by the extension for the purpose of preventing duplicates. */
abstract listSsoConnections(): Promise
- abstract listRegionProfiles(): Promise
+ abstract listRegionProfiles(): Promise
abstract selectRegionProfile(profile: RegionProfile): Promise
diff --git a/packages/core/src/login/webview/vue/regionProfileSelector.vue b/packages/core/src/login/webview/vue/regionProfileSelector.vue
index 40edd9f911f..c51570bd79a 100644
--- a/packages/core/src/login/webview/vue/regionProfileSelector.vue
+++ b/packages/core/src/login/webview/vue/regionProfileSelector.vue
@@ -46,10 +46,10 @@
-
Select profile
+
Choose a Q Developer profile
- Profles have different configs defined by your adminstrators. Select the profile that best meets your
- current working need and switch at any time.
+ Your administrator has given you access to Q from multiple profiles. Choose the profile that meets your
+ current working needs. You can change your profile at any time.
@@ -69,8 +69,18 @@
>
-
-
+
+ We couldn't load your Q Developer profiles. Please try again.
+
Fetching Q Developer profiles...this may take a minute.
From 46d475e4c073e122fc0739b8367cb8f77d991677 Mon Sep 17 00:00:00 2001
From: "Justin M. Keyes"
Date: Wed, 9 Apr 2025 13:36:19 -0700
Subject: [PATCH 32/49] changelog
---
.../Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json b/packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json
new file mode 100644
index 00000000000..520644150b4
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Improve status message while loading Amazon Q Profiles during login"
+}
From 4ad3e92ef7111662ce71fe07a0950b406bbed366 Mon Sep 17 00:00:00 2001
From: Randall-Jiang
Date: Wed, 9 Apr 2025 13:56:59 -0700
Subject: [PATCH 33/49] fix(chat): change warning category and show the currect
warning message (#6977)
## Problem
Before we are unclear about the commands' category and sometimes we show
warning for some wide-used commands. Also we want to show the different
warning message for destructive commands and mutable commands.
## Solution
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../codewhispererChat/tools/executeBash.ts | 96 +++++++------------
1 file changed, 36 insertions(+), 60 deletions(-)
diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts
index 84308390673..616ea49ed47 100644
--- a/packages/core/src/codewhispererChat/tools/executeBash.ts
+++ b/packages/core/src/codewhispererChat/tools/executeBash.ts
@@ -12,11 +12,10 @@ import { split } from 'shlex'
export enum CommandCategory {
ReadOnly,
- HighRisk,
+ Mutate,
Destructive,
}
-export const dangerousPatterns = new Set(['<(', '$(', '`'])
export const splitOperators = new Set(['|', '&&', '||', '>'])
export const splitOperatorsArray = Array.from(splitOperators)
export const commandCategories = new Map([
@@ -47,45 +46,37 @@ export const commandCategories = new Map([
['netstat', CommandCategory.ReadOnly],
['ss', CommandCategory.ReadOnly],
['dig', CommandCategory.ReadOnly],
- ['grep', CommandCategory.ReadOnly],
['wc', CommandCategory.ReadOnly],
['sort', CommandCategory.ReadOnly],
['diff', CommandCategory.ReadOnly],
['head', CommandCategory.ReadOnly],
['tail', CommandCategory.ReadOnly],
- // HighRisk commands
- ['chmod', CommandCategory.HighRisk],
- ['chown', CommandCategory.HighRisk],
- ['mv', CommandCategory.HighRisk],
- ['cp', CommandCategory.HighRisk],
- ['ln', CommandCategory.HighRisk],
- ['mount', CommandCategory.HighRisk],
- ['umount', CommandCategory.HighRisk],
- ['kill', CommandCategory.HighRisk],
- ['killall', CommandCategory.HighRisk],
- ['pkill', CommandCategory.HighRisk],
- ['iptables', CommandCategory.HighRisk],
- ['route', CommandCategory.HighRisk],
- ['systemctl', CommandCategory.HighRisk],
- ['service', CommandCategory.HighRisk],
- ['crontab', CommandCategory.HighRisk],
- ['at', CommandCategory.HighRisk],
- ['tar', CommandCategory.HighRisk],
- ['awk', CommandCategory.HighRisk],
- ['sed', CommandCategory.HighRisk],
- ['wget', CommandCategory.HighRisk],
- ['curl', CommandCategory.HighRisk],
- ['nc', CommandCategory.HighRisk],
- ['ssh', CommandCategory.HighRisk],
- ['scp', CommandCategory.HighRisk],
- ['ftp', CommandCategory.HighRisk],
- ['sftp', CommandCategory.HighRisk],
- ['rsync', CommandCategory.HighRisk],
- ['chroot', CommandCategory.HighRisk],
- ['lsof', CommandCategory.HighRisk],
- ['strace', CommandCategory.HighRisk],
- ['gdb', CommandCategory.HighRisk],
+ // Mutable commands
+ ['chmod', CommandCategory.Mutate],
+ ['curl', CommandCategory.Mutate],
+ ['mount', CommandCategory.Mutate],
+ ['umount', CommandCategory.Mutate],
+ ['systemctl', CommandCategory.Mutate],
+ ['service', CommandCategory.Mutate],
+ ['crontab', CommandCategory.Mutate],
+ ['at', CommandCategory.Mutate],
+ ['nc', CommandCategory.Mutate],
+ ['ssh', CommandCategory.Mutate],
+ ['scp', CommandCategory.Mutate],
+ ['ftp', CommandCategory.Mutate],
+ ['sftp', CommandCategory.Mutate],
+ ['rsync', CommandCategory.Mutate],
+ ['chroot', CommandCategory.Mutate],
+ ['strace', CommandCategory.Mutate],
+ ['gdb', CommandCategory.Mutate],
+ ['apt', CommandCategory.Mutate],
+ ['yum', CommandCategory.Mutate],
+ ['dnf', CommandCategory.Mutate],
+ ['pacman', CommandCategory.Mutate],
+ ['exec', CommandCategory.Mutate],
+ ['eval', CommandCategory.Mutate],
+ ['xargs', CommandCategory.Mutate],
// Destructive commands
['rm', CommandCategory.Destructive],
@@ -104,22 +95,17 @@ export const commandCategories = new Map([
['insmod', CommandCategory.Destructive],
['rmmod', CommandCategory.Destructive],
['modprobe', CommandCategory.Destructive],
- ['apt', CommandCategory.Destructive],
- ['yum', CommandCategory.Destructive],
- ['dnf', CommandCategory.Destructive],
- ['pacman', CommandCategory.Destructive],
- ['perl', CommandCategory.Destructive],
- ['python', CommandCategory.Destructive],
- ['bash', CommandCategory.Destructive],
- ['sh', CommandCategory.Destructive],
- ['exec', CommandCategory.Destructive],
- ['eval', CommandCategory.Destructive],
- ['xargs', CommandCategory.Destructive],
+ ['kill', CommandCategory.Destructive],
+ ['killall', CommandCategory.Destructive],
+ ['pkill', CommandCategory.Destructive],
+ ['iptables', CommandCategory.Destructive],
+ ['route', CommandCategory.Destructive],
+ ['chown', CommandCategory.Destructive],
])
export const maxBashToolResponseSize: number = 1024 * 1024 // 1MB
export const lineCount: number = 1024
export const destructiveCommandWarningMessage = '⚠️ WARNING: Destructive command detected:\n\n'
-export const highRiskCommandWarningMessage = '⚠️ WARNING: High risk command detected:\n\n'
+export const mutateCommandWarningMessage = 'Mutation command:\n\n'
export interface ExecuteBashParams {
command: string
@@ -197,22 +183,12 @@ export class ExecuteBash {
switch (category) {
case CommandCategory.Destructive:
return { requiresAcceptance: true, warning: destructiveCommandWarningMessage }
- case CommandCategory.HighRisk:
- return {
- requiresAcceptance: true,
- warning: highRiskCommandWarningMessage,
- }
+ case CommandCategory.Mutate:
+ return { requiresAcceptance: true, warning: mutateCommandWarningMessage }
case CommandCategory.ReadOnly:
- if (
- cmdArgs.some((arg) =>
- Array.from(dangerousPatterns).some((pattern) => arg.includes(pattern))
- )
- ) {
- return { requiresAcceptance: true, warning: highRiskCommandWarningMessage }
- }
continue
default:
- return { requiresAcceptance: true, warning: highRiskCommandWarningMessage }
+ return { requiresAcceptance: true }
}
}
return { requiresAcceptance: false }
From 35ac939799f923091bb6d2faaaeb605c00eebe32 Mon Sep 17 00:00:00 2001
From: Jason Guo
Date: Wed, 9 Apr 2025 15:19:41 -0700
Subject: [PATCH 34/49] fix(chat): Send back throttling error message
---
.../controllers/chat/messenger/messenger.ts | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index ff079e422a6..3c83b096afb 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -754,6 +754,20 @@ export class Messenger {
}
private showChatExceptionMessage(e: ChatException, tabID: string, requestID: string | undefined) {
+ const title = 'An error occurred while processing your request.'
+ // TODO: once the server sends the correct exception back, fix this
+ if (e.statusCode && e.statusCode === '500') {
+ // Send throttling message
+ this.dispatcher.sendErrorMessage(
+ new ErrorMessage(
+ title,
+ 'We are experiencing heavy traffic, please try again shortly.'.trimEnd().trimStart(),
+ tabID
+ )
+ )
+ return
+ }
+
let message = 'This error is reported to the team automatically. We will attempt to fix it as soon as possible.'
if (e.errorMessage !== undefined) {
message += `\n\nDetails: ${e.errorMessage}`
@@ -769,9 +783,7 @@ export class Messenger {
message += `\n\nRequest ID: ${requestID}`
}
- this.dispatcher.sendErrorMessage(
- new ErrorMessage('An error occurred while processing your request.', message.trimEnd().trimStart(), tabID)
- )
+ this.dispatcher.sendErrorMessage(new ErrorMessage(title, message.trimEnd().trimStart(), tabID))
}
public sendOpenSettingsMessage(triggerId: string, tabID: string) {
From 1b81576f175ea590e202516ad1f2bab4855a47b7 Mon Sep 17 00:00:00 2001
From: Jason Guo <81202082+jguoamz@users.noreply.github.com>
Date: Wed, 9 Apr 2025 15:20:25 -0700
Subject: [PATCH 35/49] fix(chat): Improve diffView UX (#6981)
## Notes
- fix(chat): Improve diffView UX, changes include:
- Remove the icon in diff view
- Clean up temp files when accept or reject
- Do not open the files as they generate
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../controllers/chat/controller.ts | 59 +++++++++++--------
.../controllers/chat/messenger/messenger.ts | 3 -
2 files changed, 35 insertions(+), 27 deletions(-)
diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
index 7a0921724c7..91e1fe69fb5 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
@@ -97,7 +97,6 @@ import { OutputKind } from '../../tools/toolShared'
import { ToolUtils, Tool, ToolType } from '../../tools/toolUtils'
import { ChatStream } from '../../tools/chatStream'
import { ChatHistoryStorage } from '../../storages/chatHistoryStorage'
-import { FsWriteParams } from '../../tools/fsWrite'
import { tempDirPath } from '../../../shared/filesystemUtilities'
import { Database } from '../../../shared/db/chatDb/chatDb'
import { TabBarController } from './tabBarController'
@@ -746,13 +745,6 @@ export class ChatController {
const toolResult: ToolResult = result
toolResults.push(toolResult)
}
-
- if (toolUse.name === ToolType.FsWrite) {
- await vscode.commands.executeCommand(
- 'vscode.open',
- vscode.Uri.file((toolUse.input as unknown as FsWriteParams).path)
- )
- }
}
await this.generateResponse(
@@ -787,11 +779,26 @@ export class ChatController {
})
}
- private async closeDiffView() {
- // Close the diff view if User reject the generated code changes.
+ private async closeDiffView(message: CustomFormActionMessage) {
+ // Close the diff view if User rejected or accepted the generated code changes.
if (vscode.window.tabGroups.activeTabGroup.activeTab?.label.includes(amazonQTabSuffix)) {
await vscode.commands.executeCommand('workbench.action.closeActiveEditor')
}
+ // clean up temp file
+ const tabID = message.tabID
+ const toolUseId = message.action.formItemValues?.toolUseId
+ if (!tabID || !toolUseId) {
+ return
+ }
+
+ const session = this.sessionStorage.getSession(tabID)
+ const { filePath } = session.fsWriteBackups.get(toolUseId) ?? {}
+ if (filePath) {
+ const tempFilePath = await this.getTempFilePath(filePath)
+ if (await fs.existsFile(tempFilePath)) {
+ await fs.delete(tempFilePath)
+ }
+ }
}
private async rejectShellCommand(message: CustomFormActionMessage) {
@@ -825,11 +832,11 @@ export class ChatController {
await this.processToolUseMessage(message)
break
case 'accept-code-diff':
- await this.closeDiffView()
+ await this.closeDiffView(message)
break
case 'reject-code-diff':
await this.restoreBackup(message)
- await this.closeDiffView()
+ await this.closeDiffView(message)
break
case 'reject-shell-command':
await this.rejectShellCommand(message)
@@ -873,6 +880,21 @@ export class ChatController {
}
}
+ private async getTempFilePath(filePath: string) {
+ // Create a temporary file path to show the diff view
+ const pathToArchiveDir = path.join(tempDirPath, 'q-chat')
+ const archivePathExists = await fs.existsDir(pathToArchiveDir)
+ if (!archivePathExists) {
+ await fs.mkdir(pathToArchiveDir)
+ }
+ const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts')
+ const resultArtifactsDirExists = await fs.existsDir(resultArtifactsDir)
+ if (!resultArtifactsDirExists) {
+ await fs.mkdir(resultArtifactsDir)
+ }
+ return path.join(resultArtifactsDir, `temp-${path.basename(filePath)}`)
+ }
+
private async processFileClickMessage(message: FileClick) {
const session = this.sessionStorage.getSession(message.tabID)
// Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly.
@@ -884,18 +906,7 @@ export class ChatController {
}
try {
- // Create a temporary file path to show the diff view
- // TODO: Use amazonQDiffScheme for temp file
- const pathToArchiveDir = path.join(tempDirPath, 'q-chat')
- const archivePathExists = await fs.existsDir(pathToArchiveDir)
- if (archivePathExists) {
- await fs.delete(pathToArchiveDir, { recursive: true })
- }
- await fs.mkdir(pathToArchiveDir)
- const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts')
- await fs.mkdir(resultArtifactsDir)
-
- const tempFilePath = path.join(resultArtifactsDir, `temp-${path.basename(filePath)}`)
+ const tempFilePath = await this.getTempFilePath(filePath)
await fs.writeFile(tempFilePath, content)
const leftUri = vscode.Uri.file(tempFilePath)
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index c9894b04596..d123f462111 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -591,14 +591,11 @@ export class Messenger {
},
]
header = {
- icon: 'code-block' as MynahIconsType,
buttons,
fileList,
}
fullWidth = true
padding = false
- // eslint-disable-next-line unicorn/no-null
- codeBlockActions = { 'insert-to-cursor': null, copy: null }
}
this.dispatcher.sendChatMessage(
From 529da802e9ff931e7b185c3f9ee8ed5b8b915b60 Mon Sep 17 00:00:00 2001
From: Randall-Jiang
Date: Wed, 9 Apr 2025 15:21:20 -0700
Subject: [PATCH 36/49] fix(feat): add tooltip and set default mode to on
(#6983)
## Problem
The pair-program was off by default and there was no tooltip.
## Solution

---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
package-lock.json | 15 ++++++++++-----
package.json | 2 +-
.../core/src/amazonq/webview/ui/tabs/generator.ts | 3 ++-
3 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 2c9578a1ba8..edfd42ab17f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,7 +33,7 @@
"@vscode/test-electron": "^2.3.8",
"@vscode/test-web": "^0.0.65",
"@vscode/vsce": "^2.19.0",
- "eslint": "^8.56.0",
+ "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-aws-toolkits": "file:plugins/eslint-plugin-aws-toolkits",
"eslint-plugin-header": "^3.1.1",
@@ -12254,7 +12254,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.56.0",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
+ "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -18195,15 +18197,18 @@
}
},
"node_modules/eslint": {
- "version": "8.56.0",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
+ "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.56.0",
- "@humanwhocodes/config-array": "^0.11.13",
+ "@eslint/js": "8.57.0",
+ "@humanwhocodes/config-array": "^0.11.14",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
diff --git a/package.json b/package.json
index fcc58e487cd..3ac7ea42a96 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"@vscode/test-electron": "^2.3.8",
"@vscode/test-web": "^0.0.65",
"@vscode/vsce": "^2.19.0",
- "eslint": "^8.56.0",
+ "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-aws-toolkits": "file:plugins/eslint-plugin-aws-toolkits",
"eslint-plugin-header": "^3.1.1",
diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts
index 15f00e9baeb..26644975d99 100644
--- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts
+++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts
@@ -78,7 +78,8 @@ export class TabDataGenerator {
{
type: 'toggle',
id: 'prompt-type',
- value: 'ask',
+ value: 'pair-programming-on',
+ tooltip: 'Pair programmar on',
options: [
{
value: 'pair-programming-on',
From e78b9a21486880cf09115992a4220a0d4727ea51 Mon Sep 17 00:00:00 2001
From: "Justin M. Keyes"
Date: Wed, 9 Apr 2025 15:33:41 -0700
Subject: [PATCH 37/49] fix(amazonq): "failed to run command" #6982
This command is called frequently and the command or related vscode
"view" may not exist yet early in the extension startup/lifecycle.
---
.../Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json | 4 ++++
packages/amazonq/src/lsp/chat/commands.ts | 5 ++---
.../src/amazonqFeatureDev/controllers/chat/controller.ts | 4 ++--
.../core/src/codewhispererChat/commands/registerCommands.ts | 5 ++---
4 files changed, 10 insertions(+), 8 deletions(-)
create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json b/packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json
new file mode 100644
index 00000000000..6d3a97660f2
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "\"failed to run command\" error"
+}
diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts
index 3febc748442..dd495d1bfbf 100644
--- a/packages/amazonq/src/lsp/chat/commands.ts
+++ b/packages/amazonq/src/lsp/chat/commands.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import * as vscode from 'vscode'
import { Commands, globals } from 'aws-core-vscode/shared'
import { window } from 'vscode'
import { AmazonQChatViewProvider } from './webviewProvider'
@@ -74,6 +73,6 @@ function registerGenericCommand(commandName: string, genericCommand: string, pro
* Instead, we just create our own as a temporary solution
*/
async function focusAmazonQPanel() {
- await vscode.commands.executeCommand('aws.amazonq.AmazonQChatView.focus')
- await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus')
+ await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus')
+ await Commands.tryExecute('aws.amazonq.AmazonCommonAuth.focus')
}
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index 79c52c4872a..bdf73eada07 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -42,7 +42,7 @@ import { AuthUtil } from '../../../codewhisperer/util/authUtil'
import { AuthController } from '../../../amazonq/auth/controller'
import { getLogger } from '../../../shared/logger/logger'
import { submitFeedback } from '../../../feedback/vue/submitFeedback'
-import { placeholder } from '../../../shared/vscode/commands2'
+import { Commands, placeholder } from '../../../shared/vscode/commands2'
import { EditorContentController } from '../../../amazonq/commons/controllers/contentController'
import { openUrl } from '../../../shared/utilities/vsCodeUtils'
import { checkForDevFile, getPathsFromZipFilePath } from '../../../amazonq/util/files'
@@ -579,7 +579,7 @@ export class FeatureDevController {
open
)
if (resp === open) {
- await vscode.commands.executeCommand('aws.amazonq.AmazonQChatView.focus')
+ await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus')
// TODO add focusing on the specific tab once that's implemented
}
}
diff --git a/packages/core/src/codewhispererChat/commands/registerCommands.ts b/packages/core/src/codewhispererChat/commands/registerCommands.ts
index dd9f50d7a48..39d8383c867 100644
--- a/packages/core/src/codewhispererChat/commands/registerCommands.ts
+++ b/packages/core/src/codewhispererChat/commands/registerCommands.ts
@@ -7,7 +7,6 @@ import { commandPalette } from '../../codewhisperer/commands/types'
import { CodeScanIssue } from '../../codewhisperer/models/model'
import { Commands, VsCodeCommandArg, placeholder } from '../../shared/vscode/commands2'
import { ChatControllerMessagePublishers } from '../controllers/chat/controller'
-import vscode from 'vscode'
/**
* Opens the Amazon Q panel, showing the correct View that should
@@ -25,8 +24,8 @@ export const focusAmazonQPanel = Commands.declare(
* So when we try to focus the following Views, only one will show depending
* on the context.
*/
- await vscode.commands.executeCommand('aws.amazonq.AmazonQChatView.focus')
- await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus')
+ await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus')
+ await Commands.tryExecute('aws.amazonq.AmazonCommonAuth.focus')
}
)
From dad80e51bd5570753cb6bee4805548f3432ad087 Mon Sep 17 00:00:00 2001
From: "Justin M. Keyes"
Date: Wed, 9 Apr 2025 15:39:08 -0700
Subject: [PATCH 38/49] fix(amazonq): confusing message during loading profiles
---
packages/core/src/login/webview/vue/regionProfileSelector.vue | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/core/src/login/webview/vue/regionProfileSelector.vue b/packages/core/src/login/webview/vue/regionProfileSelector.vue
index d762f254192..a793e314a64 100644
--- a/packages/core/src/login/webview/vue/regionProfileSelector.vue
+++ b/packages/core/src/login/webview/vue/regionProfileSelector.vue
@@ -54,6 +54,9 @@
Your administrator has given you access to Q from multiple profiles. Choose the profile that meets your
current working needs. You can change your profile at any time.
+ More info.
From 9a0a5717f9aa8c4a89d18202e4c3a4920626d4fa Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Wed, 9 Apr 2025 22:47:14 +0000
Subject: [PATCH 39/49] Release 1.56.0
---
package-lock.json | 4 ++--
packages/amazonq/.changes/1.56.0.json | 14 ++++++++++++++
...g Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json | 4 ----
...g Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json | 4 ----
packages/amazonq/CHANGELOG.md | 5 +++++
packages/amazonq/package.json | 2 +-
6 files changed, 22 insertions(+), 11 deletions(-)
create mode 100644 packages/amazonq/.changes/1.56.0.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json
diff --git a/package-lock.json b/package-lock.json
index b4ea09d1520..6875f42da90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -26696,7 +26696,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.56.0-SNAPSHOT",
+ "version": "1.56.0",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/.changes/1.56.0.json b/packages/amazonq/.changes/1.56.0.json
new file mode 100644
index 00000000000..8de6eea2db5
--- /dev/null
+++ b/packages/amazonq/.changes/1.56.0.json
@@ -0,0 +1,14 @@
+{
+ "date": "2025-04-09",
+ "version": "1.56.0",
+ "entries": [
+ {
+ "type": "Bug Fix",
+ "description": "Improve status message while loading Amazon Q Profiles during login"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "\"failed to run command\" error"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json b/packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json
deleted file mode 100644
index 520644150b4..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-54ef457b-7e27-4168-b04a-ad54c571e9d3.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Improve status message while loading Amazon Q Profiles during login"
-}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json b/packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json
deleted file mode 100644
index 6d3a97660f2..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-c6521d8e-639d-4672-b5fb-354c91c526be.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "\"failed to run command\" error"
-}
diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md
index 07127b72fd4..932f944f09c 100644
--- a/packages/amazonq/CHANGELOG.md
+++ b/packages/amazonq/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.56.0 2025-04-09
+
+- **Bug Fix** Improve status message while loading Amazon Q Profiles during login
+- **Bug Fix** "failed to run command" error
+
## 1.55.0 2025-04-09
- **Bug Fix** Amazon Q Chat: Update chat history icon
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 2587b750c41..c4bfb046adb 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -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.56.0-SNAPSHOT",
+ "version": "1.56.0",
"extensionKind": [
"workspace"
],
From c21be7066e7d203871389bdc527225f8f202e67e Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Wed, 9 Apr 2025 23:13:12 +0000
Subject: [PATCH 40/49] Update version to snapshot version: 1.57.0-SNAPSHOT
---
package-lock.json | 4 ++--
packages/amazonq/package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 6875f42da90..1a3bd72c4ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.2",
+ "ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -26696,7 +26696,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.56.0",
+ "version": "1.57.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index c4bfb046adb..1c70a0cce02 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -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.56.0",
+ "version": "1.57.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
From d0a6bc1595f179be454e2ebb57b9b188ce31b270 Mon Sep 17 00:00:00 2001
From: Tai Lai
Date: Wed, 9 Apr 2025 17:02:31 -0700
Subject: [PATCH 41/49] fix(chat): disable prompt during loop (#6986)
## Problem
Chat input prompt should be disabled during the agentic loop
## Solution
- Disable chat prompt
- Removed stop button for now
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../amazonq/webview/ui/apps/cwChatConnector.ts | 6 ++++--
.../core/src/amazonq/webview/ui/connector.ts | 1 +
packages/core/src/amazonq/webview/ui/main.ts | 2 +-
.../controllers/chat/controller.ts | 2 +-
.../controllers/chat/messenger/messenger.ts | 17 +++++++++--------
.../view/connector/connector.ts | 3 +++
6 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
index 71946f06500..6fb36db08b9 100644
--- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
+++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
@@ -125,6 +125,7 @@ export class Connector extends BaseConnector {
padding: messageData.padding ?? undefined,
fullWidth: messageData.fullWidth ?? undefined,
codeBlockActions: messageData.codeBlockActions ?? undefined,
+ rootFolderTitle: messageData.rootFolderTitle ?? undefined,
}
if (messageData.relatedSuggestions !== undefined) {
@@ -257,14 +258,15 @@ export class Connector extends BaseConnector {
}
if (messageData.type === 'asyncEventProgressMessage') {
- const enableStopAction = true
+ const enableStopAction = false
+ const isPromptInputDisabled = true
this.onAsyncEventProgress(
messageData.tabID,
messageData.inProgress,
messageData.message ?? undefined,
messageData.messageId ?? undefined,
enableStopAction,
- false
+ isPromptInputDisabled
)
return
}
diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts
index 6bfd8f7c3f5..9ed46f8f58d 100644
--- a/packages/core/src/amazonq/webview/ui/connector.ts
+++ b/packages/core/src/amazonq/webview/ui/connector.ts
@@ -66,6 +66,7 @@ export interface CWCChatItem extends ChatItem {
codeBlockLanguage?: string
contextList?: Context[]
title?: string
+ rootFolderTitle?: string
}
export interface Context {
diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts
index 6bdb364e96a..d2d08403312 100644
--- a/packages/core/src/amazonq/webview/ui/main.ts
+++ b/packages/core/src/amazonq/webview/ui/main.ts
@@ -409,7 +409,7 @@ export const createMynahUI = (
fileList: {
fileTreeTitle: '',
filePaths: item.contextList.map((file) => file.relativeFilePath),
- rootFolderTitle: item.title,
+ rootFolderTitle: item.rootFolderTitle,
flatList: true,
collapsed: true,
hideFileCount: true,
diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
index 91e1fe69fb5..53413c5a30f 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
@@ -1495,7 +1495,7 @@ export class ChatController {
let response: MessengerResponseType | undefined = undefined
session.createNewTokenSource()
try {
- if (!session.context) {
+ if (!session.context && triggerPayload.context.length) {
// Only show context for the first message in the loop
this.messenger.sendContextMessage(tabID, triggerID, triggerPayload.documentReferences)
session.setContext(triggerPayload.context)
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index 1b4dbb51f98..cdfe873c175 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -130,7 +130,7 @@ export class Messenger {
new ChatMessage(
{
message: '',
- messageType: 'answer',
+ messageType: 'answer-stream',
followUps: undefined,
followUpsHeader: undefined,
relatedSuggestions: undefined,
@@ -139,7 +139,8 @@ export class Messenger {
userIntent: undefined,
codeBlockLanguage: undefined,
contextList: mergedRelevantDocuments,
- title: 'Context',
+ title: '',
+ rootFolderTitle: 'Context',
buttons: undefined,
fileList: undefined,
canBeVoted: false,
@@ -452,11 +453,16 @@ export class Messenger {
)
}
+ const agenticLoopEnded = !eventCounts.has('toolUseEvent')
+ if (agenticLoopEnded) {
+ // Reset context for the next request
+ session.setContext(undefined)
+ }
this.dispatcher.sendChatMessage(
new ChatMessage(
{
message: undefined,
- messageType: 'answer',
+ messageType: agenticLoopEnded ? 'answer' : 'answer-stream',
followUps: followUps,
followUpsHeader: undefined,
relatedSuggestions: undefined,
@@ -480,11 +486,6 @@ export class Messenger {
toolUse.input !== '' && { toolUses: [{ ...toolUse }] }),
},
})
- const agenticLoopEnded = !eventCounts.has('toolUseEvent')
- if (agenticLoopEnded) {
- // Reset context for the next request
- session.setContext(undefined)
- }
getLogger().info(
`All events received. requestId=%s counts=%s`,
diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts
index 51b96486fde..6f04d09cd46 100644
--- a/packages/core/src/codewhispererChat/view/connector/connector.ts
+++ b/packages/core/src/codewhispererChat/view/connector/connector.ts
@@ -352,6 +352,7 @@ export interface ChatMessageProps {
readonly fullWidth?: boolean
readonly padding?: boolean
readonly codeBlockActions?: CodeBlockActions | null
+ readonly rootFolderTitle?: string
}
export class ChatMessage extends UiMessage {
@@ -375,6 +376,7 @@ export class ChatMessage extends UiMessage {
readonly padding?: boolean
readonly codeBlockActions?: CodeBlockActions | null
readonly canBeVoted?: boolean = false
+ readonly rootFolderTitle?: string
override type = 'chatMessage'
constructor(props: ChatMessageProps, tabID: string) {
@@ -398,6 +400,7 @@ export class ChatMessage extends UiMessage {
this.fullWidth = props.fullWidth
this.padding = props.padding
this.codeBlockActions = props.codeBlockActions
+ this.rootFolderTitle = props.rootFolderTitle
}
}
From 6af0425f58ee51396bbd24b7a900cf7c35745e31 Mon Sep 17 00:00:00 2001
From: opieter-aws
Date: Wed, 9 Apr 2025 20:38:23 -0400
Subject: [PATCH 42/49] feat(amazon lsp): add copyToClipboard implementation
(#6980)
## Problem
There was no implementation for the `copyToClipboard` message received
from LSP for Amazon Q. The functionality did work because of the
[MynahUI
implementation](https://github.com/aws/mynah-ui/blob/b56a4b1df828eaa22515ef0f5af211eec24a5f89/src/helper/chat-item.ts#L36C1-L37C1),
but for UI consistency to consider edge cases with webview clipboard, we
decided to handle it explicitly.
## Solution
Call the `copyToClipboard` function and add unit tests
---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
packages/amazonq/src/lsp/chat/messages.ts | 16 ++++++----
.../unit/amazonq/lsp/chat/messages.test.ts | 32 +++++++++++++++++++
2 files changed, 42 insertions(+), 6 deletions(-)
diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts
index e7943dd5418..444555e9950 100644
--- a/packages/amazonq/src/lsp/chat/messages.ts
+++ b/packages/amazonq/src/lsp/chat/messages.ts
@@ -23,12 +23,12 @@ import {
insertToCursorPositionNotificationType,
} from '@aws/language-server-runtimes/protocol'
import { v4 as uuidv4 } from 'uuid'
-import { window } from 'vscode'
+import * as vscode from 'vscode'
import { Disposable, LanguageClient, Position, State, TextDocumentIdentifier } from 'vscode-languageclient'
import * as jose from 'jose'
import { AmazonQChatViewProvider } from './webviewProvider'
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
-import { AmazonQPromptSettings } from 'aws-core-vscode/shared'
+import { AmazonQPromptSettings, messages } from 'aws-core-vscode/shared'
export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) {
languageClient.onDidChangeState(({ oldState, newState }) => {
@@ -62,11 +62,15 @@ export function registerMessageListeners(
switch (message.command) {
case COPY_TO_CLIPBOARD:
- // TODO see what we need to hook this up
languageClient.info('[VSCode Client] Copy to clipboard event received')
+ try {
+ await messages.copyToClipboard(message.params.code)
+ } catch (e) {
+ languageClient.error(`[VSCode Client] Failed to copy to clipboard: ${(e as Error).message}`)
+ }
break
case INSERT_TO_CURSOR_POSITION: {
- const editor = window.activeTextEditor
+ const editor = vscode.window.activeTextEditor
let textDocument: TextDocumentIdentifier | undefined = undefined
let cursorPosition: Position | undefined = undefined
if (editor) {
@@ -119,8 +123,8 @@ export function registerMessageListeners(
)
const editor =
- window.activeTextEditor ||
- window.visibleTextEditors.find((editor) => editor.document.languageId !== 'Log')
+ vscode.window.activeTextEditor ||
+ vscode.window.visibleTextEditors.find((editor) => editor.document.languageId !== 'Log')
if (editor) {
message.params.cursorPosition = [editor.selection.active]
message.params.textDocument = { uri: editor.document.uri.toString() }
diff --git a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts
index 06459cd8932..e6f51a1db34 100644
--- a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts
+++ b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts
@@ -9,6 +9,7 @@ import { AuthUtil } from 'aws-core-vscode/codewhisperer'
import { registerMessageListeners } from '../../../../../src/lsp/chat/messages'
import { AmazonQChatViewProvider } from '../../../../../src/lsp/chat/webviewProvider'
import { secondaryAuth, authConnection, AuthFollowUpType } from 'aws-core-vscode/amazonq'
+import { messages } from 'aws-core-vscode/shared'
describe('registerMessageListeners', () => {
let languageClient: LanguageClient
@@ -126,4 +127,35 @@ describe('registerMessageListeners', () => {
})
})
})
+
+ describe('COPY_TO_CLIPBOARD', () => {
+ let copyToClipboardStub: sinon.SinonStub
+ const testCode = 'test'
+ const copyMessage = {
+ command: 'copyToClipboard',
+ params: {
+ code: testCode,
+ },
+ }
+
+ beforeEach(() => {
+ copyToClipboardStub = sandbox.stub().resolves()
+ sandbox.stub(messages, 'copyToClipboard').get(() => copyToClipboardStub)
+ })
+
+ it('successfully copies code to clipboard', async () => {
+ await messageHandler(copyMessage)
+
+ sinon.assert.calledWith(copyToClipboardStub, testCode)
+ })
+
+ it('handles clipboard copy failure', async () => {
+ const errorMessage = 'Failed to copy'
+ copyToClipboardStub.rejects(new Error(errorMessage))
+
+ await messageHandler(copyMessage)
+
+ sinon.assert.calledWith(errorStub, `[VSCode Client] Failed to copy to clipboard: ${errorMessage}`)
+ })
+ })
})
From 9d382e572edb1c0e7322958125f4950a9c5615a6 Mon Sep 17 00:00:00 2001
From: Jason Guo
Date: Wed, 9 Apr 2025 17:31:25 -0700
Subject: [PATCH 43/49] fix(chat): handle JSON.parse error gracefully
---
.../controllers/chat/messenger/messenger.ts | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index cdfe873c175..8a73250a42e 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -261,12 +261,11 @@ export class Messenger {
}
if (cwChatEvent.toolUseEvent?.stop) {
- toolUse.input = JSON.parse(toolUseInput)
- toolUse.toolUseId = cwChatEvent.toolUseEvent.toolUseId ?? ''
- toolUse.name = cwChatEvent.toolUseEvent.name ?? ''
-
let toolError = undefined
try {
+ toolUse.toolUseId = cwChatEvent.toolUseEvent.toolUseId ?? ''
+ toolUse.name = cwChatEvent.toolUseEvent.name ?? ''
+ toolUse.input = JSON.parse(toolUseInput)
const availableToolsNames = (session.pairProgrammingModeOn ? tools : noWriteTools).map(
(item) => item.toolSpecification?.name
)
From db646376755f05765657bfc63e8ea906cbdf7e5e Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Wed, 9 Apr 2025 18:07:03 -0700
Subject: [PATCH 44/49] fix(amazonq): Grouping read tool messages under
contextList (#6975)
## Problem
- We show read tool message for each file read which is occupying the
entire chat with these messages which is not super important.
## Solution
- Group all the read tool messaged under ContextList.

---
- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../webview/ui/apps/cwChatConnector.ts | 34 ++++-
.../core/src/amazonq/webview/ui/connector.ts | 2 +-
packages/core/src/amazonq/webview/ui/main.ts | 67 +++++----
.../codewhispererChat/clients/chat/v0/chat.ts | 36 ++++-
.../controllers/chat/controller.ts | 16 +-
.../controllers/chat/messenger/messenger.ts | 140 +++++++++++++++---
.../src/codewhispererChat/tools/chatStream.ts | 21 ++-
.../src/codewhispererChat/tools/fsRead.ts | 19 +--
.../codewhispererChat/tools/listDirectory.ts | 6 +-
.../view/connector/connector.ts | 8 +
10 files changed, 269 insertions(+), 80 deletions(-)
diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
index 6fb36db08b9..ab175c1da6f 100644
--- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
+++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
@@ -179,6 +179,33 @@ export class Connector extends BaseConnector {
}
}
+ private processToolMessage = async (messageData: any): Promise => {
+ if (this.onChatAnswerUpdated === undefined) {
+ return
+ }
+ const answer: CWCChatItem = {
+ type: messageData.messageType,
+ messageId: messageData.messageID ?? messageData.triggerID,
+ body: messageData.message,
+ followUp: messageData.followUps,
+ canBeVoted: messageData.canBeVoted ?? false,
+ codeReference: messageData.codeReference,
+ userIntent: messageData.contextList,
+ codeBlockLanguage: messageData.codeBlockLanguage,
+ contextList: messageData.contextList,
+ title: messageData.title,
+ buttons: messageData.buttons,
+ fileList: messageData.fileList,
+ header: messageData.header ?? undefined,
+ padding: messageData.padding ?? undefined,
+ fullWidth: messageData.fullWidth ?? undefined,
+ codeBlockActions: messageData.codeBlockActions ?? undefined,
+ rootFolderTitle: messageData.rootFolderTitle,
+ }
+ this.onChatAnswerUpdated(messageData.tabID, answer)
+ return
+ }
+
private storeChatItem(tabId: string, messageId: string, item: ChatItem): void {
if (!this.chatItems.has(tabId)) {
this.chatItems.set(tabId, new Map())
@@ -238,6 +265,11 @@ export class Connector extends BaseConnector {
return
}
+ if (messageData.type === 'toolMessage') {
+ await this.processToolMessage(messageData)
+ return
+ }
+
if (messageData.type === 'editorContextCommandMessage') {
await this.processEditorContextCommandMessage(messageData)
return
@@ -363,7 +395,6 @@ export class Connector extends BaseConnector {
break
case 'run-shell-command':
answer.header = {
- icon: 'shell' as MynahIconsType,
body: 'shell',
status: {
icon: 'ok' as MynahIconsType,
@@ -374,7 +405,6 @@ export class Connector extends BaseConnector {
break
case 'reject-shell-command':
answer.header = {
- icon: 'shell' as MynahIconsType,
body: 'shell',
status: {
icon: 'cancel' as MynahIconsType,
diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts
index 9ed46f8f58d..04a740624f1 100644
--- a/packages/core/src/amazonq/webview/ui/connector.ts
+++ b/packages/core/src/amazonq/webview/ui/connector.ts
@@ -78,7 +78,7 @@ export interface ConnectorProps {
sendMessageToExtension: (message: ExtensionMessage) => void
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
onRunTestMessageReceived?: (tabID: string, showRunTestMessage: boolean) => void
- onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
+ onChatAnswerUpdated?: (tabID: string, message: CWCChatItem) => void
onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void
onWelcomeFollowUpClicked: (tabID: string, welcomeFollowUpType: WelcomeFollowupType) => void
onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string | undefined) => void
diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts
index d2d08403312..8e666467b8f 100644
--- a/packages/core/src/amazonq/webview/ui/main.ts
+++ b/packages/core/src/amazonq/webview/ui/main.ts
@@ -98,6 +98,41 @@ export const createMynahUI = (
welcomeCount += 1
}
+ /**
+ * Creates a file list header from context list
+ * @param contextList List of file contexts
+ * @param rootFolderTitle Title for the root folder
+ * @returns Header object with file list
+ */
+ const createFileListHeader = (contextList: any[], rootFolderTitle?: string) => {
+ return {
+ fileList: {
+ fileTreeTitle: '',
+ filePaths: contextList.map((file) => file.relativeFilePath),
+ rootFolderTitle: rootFolderTitle,
+ flatList: true,
+ collapsed: true,
+ hideFileCount: true,
+ details: Object.fromEntries(
+ contextList.map((file) => [
+ file.relativeFilePath,
+ {
+ label: file.lineRanges
+ .map((range: { first: number; second: number }) =>
+ range.first === -1 || range.second === -1
+ ? ''
+ : `line ${range.first} - ${range.second}`
+ )
+ .join(', '),
+ description: file.relativeFilePath,
+ clickable: true,
+ },
+ ])
+ ),
+ },
+ }
+ }
+
// Adding the first tab as CWC tab
tabsStorage.addTab({
id: 'tab-1',
@@ -342,8 +377,11 @@ export const createMynahUI = (
sendMessageToExtension: (message) => {
ideApi.postMessage(message)
},
- onChatAnswerUpdated: (tabID: string, item: ChatItem) => {
+ onChatAnswerUpdated: (tabID: string, item: CWCChatItem) => {
if (item.messageId !== undefined) {
+ if (item.contextList !== undefined && item.contextList.length > 0) {
+ item.header = createFileListHeader(item.contextList, item.rootFolderTitle)
+ }
mynahUI.updateChatAnswerWithMessageId(tabID, item.messageId, {
...(item.body !== undefined ? { body: item.body } : {}),
...(item.buttons !== undefined ? { buttons: item.buttons } : {}),
@@ -405,32 +443,7 @@ export const createMynahUI = (
}
if (item.contextList !== undefined && item.contextList.length > 0) {
- item.header = {
- fileList: {
- fileTreeTitle: '',
- filePaths: item.contextList.map((file) => file.relativeFilePath),
- rootFolderTitle: item.rootFolderTitle,
- flatList: true,
- collapsed: true,
- hideFileCount: true,
- details: Object.fromEntries(
- item.contextList.map((file) => [
- file.relativeFilePath,
- {
- label: file.lineRanges
- .map((range) =>
- range.first === -1 || range.second === -1
- ? ''
- : `line ${range.first} - ${range.second}`
- )
- .join(', '),
- description: file.relativeFilePath,
- clickable: true,
- },
- ])
- ),
- },
- }
+ item.header = createFileListHeader(item.contextList, item.rootFolderTitle)
}
if (
diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts
index 53551b254f5..10409be4ada 100644
--- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts
+++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts
@@ -14,7 +14,7 @@ import { ToolkitError } from '../../../../shared/errors'
import { createCodeWhispererChatStreamingClient } from '../../../../shared/clients/codewhispererChatClient'
import { createQDeveloperStreamingClient } from '../../../../shared/clients/qDeveloperChatClient'
import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWrittenCodeTracker'
-import { PromptMessage } from '../../../controllers/chat/model'
+import { DocumentReference, PromptMessage } from '../../../controllers/chat/model'
import { FsWriteBackup } from '../../../../codewhispererChat/tools/fsWrite'
export type ToolUseWithError = {
@@ -30,8 +30,10 @@ export class ChatSession {
* _readFiles = list of files read from the project to gather context before generating response.
* _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user
* _context = Additional context to be passed to the LLM for generating the response
+ * _messageIdToUpdate = messageId of a chat message to be updated, used for reducing consecutive tool messages
*/
- private _readFiles: string[] = []
+ private _readFiles: DocumentReference[] = []
+ private _readFolders: DocumentReference[] = []
private _toolUseWithError: ToolUseWithError | undefined
private _showDiffOnFileWrite: boolean = false
private _context: PromptMessage['context']
@@ -41,6 +43,8 @@ export class ChatSession {
* True if messages from local history have been sent to session.
*/
localHistoryHydrated: boolean = false
+ private _messageIdToUpdate: string | undefined
+ private _messageIdToUpdateListDirectory: string | undefined
contexts: Map = new Map()
// TODO: doesn't handle the edge case when two files share the same relativePath string but from different root
@@ -49,6 +53,21 @@ export class ChatSession {
public get sessionIdentifier(): string | undefined {
return this.sessionId
}
+ public get messageIdToUpdate(): string | undefined {
+ return this._messageIdToUpdate
+ }
+
+ public setMessageIdToUpdate(messageId: string | undefined) {
+ this._messageIdToUpdate = messageId
+ }
+
+ public get messageIdToUpdateListDirectory(): string | undefined {
+ return this._messageIdToUpdateListDirectory
+ }
+
+ public setMessageIdToUpdateListDirectory(messageId: string | undefined) {
+ this._messageIdToUpdateListDirectory = messageId
+ }
public get pairProgrammingModeOn(): boolean {
return this._pairProgrammingModeOn
@@ -95,21 +114,30 @@ export class ChatSession {
public setSessionID(id?: string) {
this.sessionId = id
}
- public get readFiles(): string[] {
+ public get readFiles(): DocumentReference[] {
return this._readFiles
}
+ public get readFolders(): DocumentReference[] {
+ return this._readFolders
+ }
public get showDiffOnFileWrite(): boolean {
return this._showDiffOnFileWrite
}
public setShowDiffOnFileWrite(value: boolean) {
this._showDiffOnFileWrite = value
}
- public addToReadFiles(filePath: string) {
+ public addToReadFiles(filePath: DocumentReference) {
this._readFiles.push(filePath)
}
public clearListOfReadFiles() {
this._readFiles = []
}
+ public setReadFolders(folder: DocumentReference) {
+ this._readFolders.push(folder)
+ }
+ public clearListOfReadFolders() {
+ this._readFolders = []
+ }
async chatIam(chatRequest: SendMessageRequest): Promise {
const client = await createQDeveloperStreamingClient()
diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
index 53413c5a30f..859d0254b72 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
@@ -715,9 +715,18 @@ export class ChatController {
try {
await ToolUtils.validate(tool)
- const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, {
- requiresAcceptance: false,
- })
+ const chatStream = new ChatStream(
+ this.messenger,
+ tabID,
+ triggerID,
+ toolUse,
+ session,
+ undefined,
+ false,
+ {
+ requiresAcceptance: false,
+ }
+ )
if (tool.type === ToolType.FsWrite && toolUse.toolUseId) {
const backup = await tool.tool.getBackup()
session.setFsWriteBackup(toolUse.toolUseId, backup)
@@ -1180,6 +1189,7 @@ export class ChatController {
private async processPromptMessageAsNewThread(message: PromptMessage) {
const session = this.sessionStorage.getSession(message.tabID)
session.clearListOfReadFiles()
+ session.clearListOfReadFolders()
session.setShowDiffOnFileWrite(false)
this.editorContextExtractor
.extractContextForTrigger('ChatMessage')
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index 8a73250a42e..bb4d28bc38d 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -21,6 +21,7 @@ import {
CloseDetailedListMessage,
SelectTabMessage,
ChatItemHeader,
+ ToolMessage,
} from '../../../view/connector/connector'
import { EditorContextCommandType } from '../../../commands/registerCommands'
import { ChatResponseStream as qdevChatResponseStream } from '@amzn/amazon-q-developer-streaming-client'
@@ -68,6 +69,8 @@ import { FsWriteParams } from '../../../tools/fsWrite'
import { AsyncEventProgressMessage } from '../../../../amazonq/commons/connector/connectorMessages'
import { localize } from '../../../../shared/utilities/vsCodeUtils'
import { getDiffLinesFromChanges } from '../../../../shared/utilities/diffUtils'
+import { FsReadParams } from '../../../tools/fsRead'
+import { ListDirectoryParams } from '../../../tools/listDirectory'
export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help'
@@ -279,16 +282,51 @@ export class Messenger {
session.setShowDiffOnFileWrite(true)
changeList = await tool.tool.getDiffChanges()
}
+ if (tool.type === ToolType.FsRead) {
+ const input = toolUse.input as unknown as FsReadParams
+ // Check if this file path is already in the readFiles list
+ const isFileAlreadyRead = session.readFiles.some(
+ (file) => file.relativeFilePath === input.path
+ )
+ if (!isFileAlreadyRead) {
+ session.addToReadFiles({
+ relativeFilePath: input?.path,
+ lineRanges: [{ first: -1, second: -1 }],
+ })
+ }
+ } else if (tool.type === ToolType.ListDirectory) {
+ const input = toolUse.input as unknown as ListDirectoryParams
+ session.setReadFolders({
+ relativeFilePath: input?.path,
+ lineRanges: [{ first: -1, second: -1 }],
+ })
+ }
const validation = ToolUtils.requiresAcceptance(tool)
const chatStream = new ChatStream(
this,
tabID,
triggerID,
toolUse,
+ session,
+ tool.type === ToolType.FsRead
+ ? session.messageIdToUpdate
+ : session.messageIdToUpdateListDirectory,
+ true,
validation,
changeList
)
await ToolUtils.queueDescription(tool, chatStream)
+ if (session.messageIdToUpdate === undefined && tool.type === ToolType.FsRead) {
+ // Store the first messageId in a chain of tool uses
+ session.setMessageIdToUpdate(toolUse.toolUseId)
+ }
+
+ if (
+ session.messageIdToUpdateListDirectory === undefined &&
+ tool.type === ToolType.ListDirectory
+ ) {
+ session.setMessageIdToUpdateListDirectory(toolUse.toolUseId)
+ }
if (!validation.requiresAcceptance) {
// Need separate id for read tool and safe bash command execution as 'run-shell-command' id is required to state in cwChatConnector.ts which will impact generic tool execution.
@@ -508,6 +546,26 @@ export class Messenger {
})
}
+ public sendInitialToolMessage(tabID: string, triggerID: string, toolUseId: string | undefined) {
+ this.dispatcher.sendChatMessage(
+ new ChatMessage(
+ {
+ message: '',
+ messageType: 'answer',
+ followUps: undefined,
+ followUpsHeader: undefined,
+ relatedSuggestions: undefined,
+ triggerID,
+ messageID: toolUseId ?? 'toolUse',
+ userIntent: undefined,
+ codeBlockLanguage: undefined,
+ contextList: undefined,
+ },
+ tabID
+ )
+ )
+ }
+
public sendErrorMessage(errorMessage: string | undefined, tabID: string, requestID: string | undefined) {
this.showChatExceptionMessage(
{
@@ -520,37 +578,85 @@ export class Messenger {
)
}
+ private sendReadAndListDirToolMessage(
+ toolUse: ToolUse,
+ session: ChatSession,
+ tabID: string,
+ triggerID: string,
+ messageIdToUpdate?: string
+ ) {
+ const contextList = toolUse.name === ToolType.ListDirectory ? session.readFolders : session.readFiles
+ const isFileRead = toolUse.name === ToolType.FsRead
+ const items = isFileRead ? session.readFiles : session.readFolders
+ const itemCount = items.length
+
+ const title =
+ itemCount < 1
+ ? 'Gathering context'
+ : isFileRead
+ ? `${itemCount} file${itemCount > 1 ? 's' : ''} read`
+ : `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed`
+
+ this.dispatcher.sendToolMessage(
+ new ToolMessage(
+ {
+ message: '',
+ messageType: 'answer-part',
+ followUps: undefined,
+ followUpsHeader: undefined,
+ relatedSuggestions: undefined,
+ triggerID,
+ messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '',
+ userIntent: undefined,
+ codeBlockLanguage: undefined,
+ contextList,
+ canBeVoted: false,
+ buttons: undefined,
+ fullWidth: false,
+ padding: false,
+ codeBlockActions: undefined,
+ rootFolderTitle: title,
+ },
+ tabID
+ )
+ )
+ }
+
public sendPartialToolLog(
message: string,
tabID: string,
triggerID: string,
toolUse: ToolUse | undefined,
+ session: ChatSession,
+ messageIdToUpdate: string | undefined,
validation: CommandValidation,
changeList?: Change[]
) {
+ // Handle read tool and list directory messages
+ if (toolUse?.name === ToolType.FsRead || toolUse?.name === ToolType.ListDirectory) {
+ return this.sendReadAndListDirToolMessage(toolUse, session, tabID, triggerID, messageIdToUpdate)
+ }
+
+ // Handle file write tool, execute bash tool and bash command output log messages
const buttons: ChatItemButton[] = []
let header: ChatItemHeader | undefined = undefined
- let fullWidth: boolean | undefined = undefined
- let padding: boolean | undefined = undefined
- let codeBlockActions: ChatItemContent['codeBlockActions'] = undefined
if (toolUse?.name === ToolType.ExecuteBash && message.startsWith('```shell')) {
if (validation.requiresAcceptance) {
const buttons: ChatItemButton[] = [
- {
- id: 'run-shell-command',
- text: localize('AWS.amazonq.executeBash.run', 'Run'),
- status: 'main',
- icon: 'play' as MynahIconsType,
- },
{
id: 'reject-shell-command',
text: localize('AWS.amazonq.executeBash.reject', 'Reject'),
status: 'clear',
icon: 'cancel' as MynahIconsType,
},
+ {
+ id: 'run-shell-command',
+ text: localize('AWS.amazonq.executeBash.run', 'Run'),
+ status: 'clear',
+ icon: 'play' as MynahIconsType,
+ },
]
header = {
- icon: 'shell' as MynahIconsType,
body: 'shell',
buttons,
}
@@ -558,10 +664,6 @@ export class Messenger {
if (validation.warning) {
message = validation.warning + message
}
- fullWidth = true
- padding = false
- // eslint-disable-next-line unicorn/no-null
- codeBlockActions = { 'insert-to-cursor': null, copy: null }
} else if (toolUse?.name === ToolType.FsWrite) {
const input = toolUse.input as unknown as FsWriteParams
const fileName = path.basename(input.path)
@@ -594,8 +696,6 @@ export class Messenger {
buttons,
fileList,
}
- fullWidth = true
- padding = false
}
this.dispatcher.sendChatMessage(
@@ -611,12 +711,14 @@ export class Messenger {
userIntent: undefined,
codeBlockLanguage: undefined,
contextList: undefined,
+ title: undefined,
canBeVoted: false,
buttons,
- fullWidth,
- padding,
+ fullWidth: true,
+ padding: false,
header,
- codeBlockActions,
+ // eslint-disable-next-line unicorn/no-null
+ codeBlockActions: { 'insert-to-cursor': null, copy: null },
},
tabID
)
diff --git a/packages/core/src/codewhispererChat/tools/chatStream.ts b/packages/core/src/codewhispererChat/tools/chatStream.ts
index 7e6e4d3ae3a..8f38e789f15 100644
--- a/packages/core/src/codewhispererChat/tools/chatStream.ts
+++ b/packages/core/src/codewhispererChat/tools/chatStream.ts
@@ -9,6 +9,7 @@ import { Messenger } from '../controllers/chat/messenger/messenger'
import { ToolUse } from '@amzn/codewhisperer-streaming'
import { CommandValidation } from './executeBash'
import { Change } from 'diff'
+import { ChatSession } from '../clients/chat/v0/chat'
/**
* A writable stream that feeds each chunk/line to the chat UI.
@@ -22,24 +23,38 @@ export class ChatStream extends Writable {
private readonly tabID: string,
private readonly triggerID: string,
private readonly toolUse: ToolUse | undefined,
+ private readonly session: ChatSession,
+ private readonly messageIdToUpdate: string | undefined,
+ // emitEvent decides to show the streaming message or read/list directory tool message to the user.
+ private readonly emitEvent: boolean,
private readonly validation: CommandValidation,
private readonly changeList?: Change[],
private readonly logger = getLogger('chatStream')
) {
super()
- this.logger.debug(`ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}`)
- this.messenger.sendInitalStream(tabID, triggerID)
+ this.logger.debug(
+ `ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}, session: ${session.readFiles}, emitEvent to mynahUI: ${emitEvent}`
+ )
+ if (!emitEvent) {
+ return
+ }
+ // If messageIdToUpdate is undefined, we need to first create an empty message with messageId so it can be updated later
+ messageIdToUpdate
+ ? this.messenger.sendInitalStream(tabID, triggerID)
+ : this.messenger.sendInitialToolMessage(tabID, triggerID, toolUse?.toolUseId)
}
override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
const text = chunk.toString()
this.accumulatedLogs += text
- this.logger.debug(`ChatStream received chunk: ${text}`)
+ this.logger.debug(`ChatStream received chunk: ${text}, emitEvent to mynahUI: ${this.emitEvent}`)
this.messenger.sendPartialToolLog(
this.accumulatedLogs,
this.tabID,
this.triggerID,
this.toolUse,
+ this.session,
+ this.messageIdToUpdate,
this.validation,
this.changeList
)
diff --git a/packages/core/src/codewhispererChat/tools/fsRead.ts b/packages/core/src/codewhispererChat/tools/fsRead.ts
index 05519641bd0..ecf90ea6dd3 100644
--- a/packages/core/src/codewhispererChat/tools/fsRead.ts
+++ b/packages/core/src/codewhispererChat/tools/fsRead.ts
@@ -7,7 +7,6 @@ import { getLogger } from '../../shared/logger/logger'
import fs from '../../shared/fs/fs'
import { InvokeOutput, OutputKind, sanitizePath } from './toolShared'
import { Writable } from 'stream'
-import path from 'path'
export interface FsReadParams {
path: string
@@ -48,23 +47,7 @@ export class FsRead {
}
public queueDescription(updates: Writable): void {
- const fileName = path.basename(this.fsPath)
- const fileUri = vscode.Uri.file(this.fsPath)
- updates.write(`Reading file: [${fileName}](${fileUri}), `)
-
- const [start, end] = this.readRange ?? []
-
- if (start && end) {
- updates.write(`from line ${start} to ${end}`)
- } else if (start) {
- if (start > 0) {
- updates.write(`from line ${start} to end of file`)
- } else {
- updates.write(`${start} line from the end of file to end of file`)
- }
- } else {
- updates.write('all lines')
- }
+ updates.write('')
updates.end()
}
diff --git a/packages/core/src/codewhispererChat/tools/listDirectory.ts b/packages/core/src/codewhispererChat/tools/listDirectory.ts
index 96ac6972bdc..a7379169a4d 100644
--- a/packages/core/src/codewhispererChat/tools/listDirectory.ts
+++ b/packages/core/src/codewhispererChat/tools/listDirectory.ts
@@ -51,12 +51,12 @@ export class ListDirectory {
public queueDescription(updates: Writable): void {
const fileName = path.basename(this.fsPath)
if (this.maxDepth === undefined) {
- updates.write(`Listing directory recursively: ${fileName}`)
+ updates.write(`Analyzing directories recursively: ${fileName}`)
} else if (this.maxDepth === 0) {
- updates.write(`Listing directory: ${fileName}`)
+ updates.write(`Analyzing directory: ${fileName}`)
} else {
const level = this.maxDepth > 1 ? 'levels' : 'level'
- updates.write(`Listing directory: ${fileName} limited to ${this.maxDepth} subfolder ${level}`)
+ updates.write(`Analyzing directory: ${fileName} limited to ${this.maxDepth} subfolder ${level}`)
}
updates.end()
}
diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts
index 6f04d09cd46..6e2aa92fe63 100644
--- a/packages/core/src/codewhispererChat/view/connector/connector.ts
+++ b/packages/core/src/codewhispererChat/view/connector/connector.ts
@@ -404,6 +404,10 @@ export class ChatMessage extends UiMessage {
}
}
+export class ToolMessage extends ChatMessage {
+ override type = 'toolMessage'
+}
+
export interface FollowUp {
readonly type: string
readonly pillText: string
@@ -458,6 +462,10 @@ export class AppToWebViewMessageDispatcher {
this.appsToWebViewMessagePublisher.publish(message)
}
+ public sendToolMessage(message: ToolMessage) {
+ this.appsToWebViewMessagePublisher.publish(message)
+ }
+
public sendEditorContextCommandMessage(message: EditorContextCommandMessage) {
this.appsToWebViewMessagePublisher.publish(message)
}
From b4f2428e9ef961a53688ed297f2640cefc54fea0 Mon Sep 17 00:00:00 2001
From: zuoyaofu
Date: Wed, 9 Apr 2025 20:02:13 -0700
Subject: [PATCH 45/49] fix(amazonq): /review returns 0 findings #6992
---
.../Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json | 4 ++++
packages/core/src/codewhisperer/client/codewhisperer.ts | 4 +++-
.../core/src/codewhisperer/service/securityScanHandler.ts | 3 ++-
3 files changed, 9 insertions(+), 2 deletions(-)
create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json b/packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json
new file mode 100644
index 00000000000..770525b5b94
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Fix bug where review shows 0 findings"
+}
diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts
index 6f65f542c3a..b338eaf65e1 100644
--- a/packages/core/src/codewhisperer/client/codewhisperer.ts
+++ b/packages/core/src/codewhisperer/client/codewhisperer.ts
@@ -202,13 +202,15 @@ export class DefaultCodeWhispererClient {
}
public async listCodeScanFindings(
- request: ListCodeScanFindingsRequest
+ request: ListCodeScanFindingsRequest,
+ profileArn: string | undefined
): Promise> {
if (this.isBearerTokenAuth()) {
const req = {
jobId: request.jobId,
nextToken: request.nextToken,
codeAnalysisFindingsSchema: 'codeanalysis/findings/1.0',
+ profileArn: profileArn,
} as CodeWhispererUserClient.ListCodeAnalysisFindingsRequest
return (await this.createUserSdkClient()).listCodeAnalysisFindings(req).promise()
}
diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts
index 00d46e3184d..b83fdbebb1a 100644
--- a/packages/core/src/codewhisperer/service/securityScanHandler.ts
+++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts
@@ -57,7 +57,8 @@ export async function listScanResults(
const logger = getLoggerForScope(scope)
const codeScanIssueMap: Map = new Map()
const aggregatedCodeScanIssueList: AggregatedCodeScanIssue[] = []
- const requester = (request: codewhispererClient.ListCodeScanFindingsRequest) => client.listCodeScanFindings(request)
+ const requester = (request: codewhispererClient.ListCodeScanFindingsRequest) =>
+ client.listCodeScanFindings(request, profile?.arn)
const request: codewhispererClient.ListCodeScanFindingsRequest = {
jobId,
codeAnalysisFindingsSchema: codeScanFindingsSchema,
From 1da77e7997e09dd81f26b4cc4083a50a2164480d Mon Sep 17 00:00:00 2001
From: zuoyaofu
Date: Wed, 9 Apr 2025 20:28:43 -0700
Subject: [PATCH 46/49] fix(amazonq): /review "Generate fix" does not work
#6993
---
.../Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json | 4 ++++
.../core/src/codewhisperer/commands/startCodeFixGeneration.ts | 3 ++-
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json b/packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json
new file mode 100644
index 00000000000..bd59a725d3e
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Fix bug where generate fix does not work"
+}
diff --git a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts
index ea26de5c5fc..9dc2fd5f68f 100644
--- a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts
+++ b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts
@@ -78,7 +78,8 @@ export async function startCodeFixGeneration(
: 'BLOCK',
},
codeFixName,
- issue.ruleId
+ issue.ruleId,
+ profile
)
if (codeFixJob.status === 'Failed') {
throw new CreateCodeFixError()
From 28574522758d513f054f0701281ea4d4ce43c536 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 10 Apr 2025 03:32:22 +0000
Subject: [PATCH 47/49] Release 1.57.0
---
package-lock.json | 4 ++--
packages/amazonq/.changes/1.57.0.json | 14 ++++++++++++++
...g Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json | 4 ----
...g Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json | 4 ----
packages/amazonq/CHANGELOG.md | 5 +++++
packages/amazonq/package.json | 2 +-
6 files changed, 22 insertions(+), 11 deletions(-)
create mode 100644 packages/amazonq/.changes/1.57.0.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json
diff --git a/package-lock.json b/package-lock.json
index 1a3bd72c4ec..f83e8462b96 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -26696,7 +26696,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.57.0-SNAPSHOT",
+ "version": "1.57.0",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/.changes/1.57.0.json b/packages/amazonq/.changes/1.57.0.json
new file mode 100644
index 00000000000..3a7a8d2ab95
--- /dev/null
+++ b/packages/amazonq/.changes/1.57.0.json
@@ -0,0 +1,14 @@
+{
+ "date": "2025-04-10",
+ "version": "1.57.0",
+ "entries": [
+ {
+ "type": "Bug Fix",
+ "description": "Fix bug where generate fix does not work"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "Fix bug where review shows 0 findings"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json b/packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json
deleted file mode 100644
index bd59a725d3e..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-09516662-6f63-4ec7-8d7e-763b3937b40b.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Fix bug where generate fix does not work"
-}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json b/packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json
deleted file mode 100644
index 770525b5b94..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-f098149d-ad2c-485c-b652-c5bef1ecb379.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Fix bug where review shows 0 findings"
-}
diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md
index 932f944f09c..ac52aa0e297 100644
--- a/packages/amazonq/CHANGELOG.md
+++ b/packages/amazonq/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.57.0 2025-04-10
+
+- **Bug Fix** Fix bug where generate fix does not work
+- **Bug Fix** Fix bug where review shows 0 findings
+
## 1.56.0 2025-04-09
- **Bug Fix** Improve status message while loading Amazon Q Profiles during login
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 1c70a0cce02..c6eab83f765 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -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.57.0-SNAPSHOT",
+ "version": "1.57.0",
"extensionKind": [
"workspace"
],
From f540496854b5c902c772374f8354ebfd23b85397 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 10 Apr 2025 03:51:28 +0000
Subject: [PATCH 48/49] Update version to snapshot version: 1.58.0-SNAPSHOT
---
package-lock.json | 4 ++--
packages/amazonq/package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index f83e8462b96..7fefc92ec14 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.2",
+ "ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -26696,7 +26696,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.57.0",
+ "version": "1.58.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index c6eab83f765..ec26ba1b56c 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -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.57.0",
+ "version": "1.58.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
From ce3dd6185c5c8c7bbc77c7e6d066055bdea0d3de Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Thu, 10 Apr 2025 09:43:20 -0400
Subject: [PATCH 49/49] fix(amazonq): inline chat now activates independently
of LSP chat #6973
## Problem
When toggling the experiment via, the `aws.experiments.amazonqChatLSP`
flag in settings, inline chat fails to activate. Specifically, it
appears the command fails to register.
```
command 'aws.amazonq.inline.invokeChat' not found
```
This happened because inline chat was activated as part of regular chat.
When we use LSP chat, this then never gets activated.
## Solution
- Activate inline chat independently of regular chat.
## Testing
- Verified the fix by running extension with and without
`aws.experiments.amazonqChatLSP` flag and using inlineChat in both
cases. One of these cases is shown in the demo below.
## Notes
This will have to change once we consume LSP inline chat, as we don't
want to activate both our inline chat and Flare's.
---
.../Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json | 4 ++++
packages/amazonq/src/app/chat/activation.ts | 2 --
packages/amazonq/src/extensionNode.ts | 2 ++
packages/amazonq/src/inlineChat/{app.ts => activation.ts} | 6 +++---
4 files changed, 9 insertions(+), 5 deletions(-)
create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json
rename packages/amazonq/src/inlineChat/{app.ts => activation.ts} (54%)
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json b/packages/amazonq/.changes/next-release/Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json
new file mode 100644
index 00000000000..e02212b84ca
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-45379b8c-1faa-4b04-951a-26e234c6dc03.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "inline chat activates properly when using 'aws.experiments.amazonqChatLSP' feature flag"
+}
diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts
index 49205c75c7d..10f827814aa 100644
--- a/packages/amazonq/src/app/chat/activation.ts
+++ b/packages/amazonq/src/app/chat/activation.ts
@@ -10,7 +10,6 @@ import { AuthUtil, CodeWhispererSettings } from 'aws-core-vscode/codewhisperer'
import { Commands, placeholder, funcUtil } from 'aws-core-vscode/shared'
import * as amazonq from 'aws-core-vscode/amazonq'
import { scanChatAppInit } from '../amazonqScan'
-import { init as inlineChatInit } from '../../inlineChat/app'
export async function activate(context: ExtensionContext) {
const appInitContext = amazonq.DefaultAmazonQAppInitContext.instance
@@ -72,7 +71,6 @@ function registerApps(appInitContext: amazonq.AmazonQAppInitContext, context: Ex
amazonq.testChatAppInit(appInitContext)
scanChatAppInit(appInitContext)
amazonq.docChatAppInit(appInitContext)
- inlineChatInit(context)
}
/**
diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts
index d9d36f828eb..945537b38ee 100644
--- a/packages/amazonq/src/extensionNode.ts
+++ b/packages/amazonq/src/extensionNode.ts
@@ -25,6 +25,7 @@ import { DevOptions } from 'aws-core-vscode/dev'
import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth'
import api from './api'
import { activate as activateCWChat } from './app/chat/activation'
+import { activate as activateInlineChat } from './inlineChat/activation'
import { beta } from 'aws-core-vscode/dev'
import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications'
import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer'
@@ -55,6 +56,7 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
await activateCWChat(context)
await activateQGumby(extContext as ExtContext)
}
+ activateInlineChat(context)
const authProvider = new CommonAuthViewProvider(
context,
diff --git a/packages/amazonq/src/inlineChat/app.ts b/packages/amazonq/src/inlineChat/activation.ts
similarity index 54%
rename from packages/amazonq/src/inlineChat/app.ts
rename to packages/amazonq/src/inlineChat/activation.ts
index f783ef8d84f..a42dfdb3e02 100644
--- a/packages/amazonq/src/inlineChat/app.ts
+++ b/packages/amazonq/src/inlineChat/activation.ts
@@ -3,10 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'
-import { InlineChatController } from '../inlineChat/controller/inlineChatController'
-import { registerInlineCommands } from '../inlineChat/command/registerInlineCommands'
+import { InlineChatController } from './controller/inlineChatController'
+import { registerInlineCommands } from './command/registerInlineCommands'
-export function init(context: vscode.ExtensionContext) {
+export function activate(context: vscode.ExtensionContext) {
const inlineChatController = new InlineChatController(context)
registerInlineCommands(context, inlineChatController)
}