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.

", "idempotent": true }, + "CreateWorkspace": { + "name": "CreateWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "CreateWorkspaceRequest" }, + "output": { "shape": "CreateWorkspaceResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

Create a workspace based on a workspace root

" + }, "DeleteTaskAssistConversation": { "name": "DeleteTaskAssistConversation", "http": { @@ -83,6 +100,22 @@ ], "documentation": "

API to delete task assist conversation.

" }, + "DeleteWorkspace": { + "name": "DeleteWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "DeleteWorkspaceRequest" }, + "output": { "shape": "DeleteWorkspaceResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

Delete a workspace based on a workspaceId

" + }, "GenerateCompletions": { "name": "GenerateCompletions", "http": { @@ -215,6 +248,21 @@ { "shape": "AccessDeniedException" } ] }, + "ListAvailableProfiles": { + "name": "ListAvailableProfiles", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "ListAvailableProfilesRequest" }, + "output": { "shape": "ListAvailableProfilesResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ] + }, "ListCodeAnalysisFindings": { "name": "ListCodeAnalysisFindings", "http": { @@ -248,6 +296,22 @@ ], "documentation": "

Return configruations for each feature that has been setup for A/B testing.

" }, + "ListWorkspaceMetadata": { + "name": "ListWorkspaceMetadata", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "ListWorkspaceMetadataRequest" }, + "output": { "shape": "ListWorkspaceMetadataResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

List workspace metadata based on a workspace root

" + }, "ResumeTransformation": { "name": "ResumeTransformation", "http": { @@ -404,6 +468,57 @@ "documentation": "

Reason for AccessDeniedException

", "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] }, + "ActiveFunctionalityList": { + "type": "list", + "member": { "shape": "FunctionalityName" }, + "max": 10, + "min": 0 + }, + "AdditionalContentEntry": { + "type": "structure", + "required": ["name", "description"], + "members": { + "name": { + "shape": "AdditionalContentEntryNameString", + "documentation": "

The name/identifier for this context entry

" + }, + "description": { + "shape": "AdditionalContentEntryDescriptionString", + "documentation": "

A description of what this context entry represents

" + }, + "innerContext": { + "shape": "AdditionalContentEntryInnerContextString", + "documentation": "

The actual contextual content

" + } + }, + "documentation": "

Structure representing a single entry of additional contextual content

" + }, + "AdditionalContentEntryDescriptionString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryInnerContextString": { + "type": "string", + "max": 8192, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryNameString": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "[a-z]+(?:-[a-z0-9]+)*", + "sensitive": true + }, + "AdditionalContentList": { + "type": "list", + "member": { "shape": "AdditionalContentEntry" }, + "documentation": "

A list of additional content entries, limited to 20 items

", + "max": 20, + "min": 0 + }, "AppStudioState": { "type": "structure", "required": ["namespace", "propertyName", "propertyContext"], @@ -451,6 +566,20 @@ "min": 0, "sensitive": true }, + "ApplicationProperties": { + "type": "structure", + "required": ["tenantId", "applicationArn", "tenantUrl", "applicationType"], + "members": { + "tenantId": { "shape": "TenantId" }, + "applicationArn": { "shape": "ResourceArn" }, + "tenantUrl": { "shape": "Url" }, + "applicationType": { "shape": "FunctionalityName" } + } + }, + "ApplicationPropertiesList": { + "type": "list", + "member": { "shape": "ApplicationProperties" } + }, "ArtifactId": { "type": "string", "max": 126, @@ -488,13 +617,17 @@ "followupPrompt": { "shape": "FollowupPrompt", "documentation": "

Followup Prompt

" + }, + "toolUses": { + "shape": "ToolUses", + "documentation": "

ToolUse Request

" } }, "documentation": "

Markdown text message.

" }, "AssistantResponseMessageContentString": { "type": "string", - "max": 4096, + "max": 100000, "min": 0, "sensitive": true }, @@ -508,6 +641,14 @@ "type": "boolean", "box": true }, + "ByUserAnalytics": { + "type": "structure", + "required": ["toggle"], + "members": { + "s3Uri": { "shape": "S3Uri" }, + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "ChatAddMessageEvent": { "type": "structure", "required": ["conversationId", "messageId"], @@ -532,7 +673,7 @@ "type": "list", "member": { "shape": "ChatMessage" }, "documentation": "

Indicates Participant in Chat conversation

", - "max": 10, + "max": 100, "min": 0 }, "ChatInteractWithMessageEvent": { @@ -868,7 +1009,9 @@ }, "CreateTaskAssistConversationRequest": { "type": "structure", - "members": {}, + "members": { + "profileArn": { "shape": "ProfileArn" } + }, "documentation": "

Structure to represent bootstrap conversation request.

" }, "CreateTaskAssistConversationResponse": { @@ -889,7 +1032,8 @@ "artifactType": { "shape": "ArtifactType" }, "uploadIntent": { "shape": "UploadIntent" }, "uploadContext": { "shape": "UploadContext" }, - "uploadId": { "shape": "UploadId" } + "uploadId": { "shape": "UploadId" }, + "profileArn": { "shape": "ProfileArn" } } }, "CreateUploadUrlRequestContentChecksumString": { @@ -919,6 +1063,26 @@ "requestHeaders": { "shape": "RequestHeaders" } } }, + "CreateWorkspaceRequest": { + "type": "structure", + "required": ["workspaceRoot"], + "members": { + "workspaceRoot": { "shape": "CreateWorkspaceRequestWorkspaceRootString" }, + "profileArn": { "shape": "ProfileArn" } + } + }, + "CreateWorkspaceRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1 + }, + "CreateWorkspaceResponse": { + "type": "structure", + "required": ["workspace"], + "members": { + "workspace": { "shape": "WorkspaceMetadata" } + } + }, "CursorState": { "type": "structure", "members": { @@ -959,11 +1123,19 @@ "type": "list", "member": { "shape": "Customization" } }, + "DashboardAnalytics": { + "type": "structure", + "required": ["toggle"], + "members": { + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "DeleteTaskAssistConversationRequest": { "type": "structure", "required": ["conversationId"], "members": { - "conversationId": { "shape": "ConversationId" } + "conversationId": { "shape": "ConversationId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent bootstrap conversation request.

" }, @@ -975,6 +1147,18 @@ }, "documentation": "

Structure to represent bootstrap conversation response.

" }, + "DeleteWorkspaceRequest": { + "type": "structure", + "required": ["workspaceId"], + "members": { + "workspaceId": { "shape": "UUID" }, + "profileArn": { "shape": "ProfileArn" } + } + }, + "DeleteWorkspaceResponse": { + "type": "structure", + "members": {} + }, "Description": { "type": "string", "max": 256, @@ -1282,6 +1466,11 @@ "max": 100, "min": 0 }, + "ErrorDetails": { + "type": "string", + "max": 2048, + "min": 0 + }, "FeatureDevCodeAcceptanceEvent": { "type": "structure", "required": ["conversationId", "linesOfCodeAccepted", "charactersOfCodeAccepted"], @@ -1418,6 +1607,20 @@ "min": 0, "sensitive": true }, + "FunctionalityName": { + "type": "string", + "enum": [ + "COMPLETIONS", + "ANALYSIS", + "CONVERSATIONS", + "TASK_ASSIST", + "TRANSFORMATIONS", + "CHAT_CUSTOMIZATION", + "TRANSFORMATIONS_WEBAPP" + ], + "max": 64, + "min": 1 + }, "GenerateCompletionsRequest": { "type": "structure", "required": ["fileContext"], @@ -1430,7 +1633,8 @@ "customizationArn": { "shape": "CustomizationArn" }, "optOutPreference": { "shape": "OptOutPreference" }, "userContext": { "shape": "UserContext" }, - "profileArn": { "shape": "ProfileArn" } + "profileArn": { "shape": "ProfileArn" }, + "workspaceId": { "shape": "UUID" } } }, "GenerateCompletionsRequestMaxResultsInteger": { @@ -1457,7 +1661,8 @@ "type": "structure", "required": ["jobId"], "members": { - "jobId": { "shape": "GetCodeAnalysisRequestJobIdString" } + "jobId": { "shape": "GetCodeAnalysisRequestJobIdString" }, + "profileArn": { "shape": "ProfileArn" } } }, "GetCodeAnalysisRequestJobIdString": { @@ -1477,7 +1682,8 @@ "type": "structure", "required": ["jobId"], "members": { - "jobId": { "shape": "GetCodeFixJobRequestJobIdString" } + "jobId": { "shape": "GetCodeFixJobRequestJobIdString" }, + "profileArn": { "shape": "ProfileArn" } } }, "GetCodeFixJobRequestJobIdString": { @@ -1498,7 +1704,8 @@ "required": ["conversationId", "codeGenerationId"], "members": { "conversationId": { "shape": "ConversationId" }, - "codeGenerationId": { "shape": "CodeGenerationId" } + "codeGenerationId": { "shape": "CodeGenerationId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Request for getting task assist code generation.

" }, @@ -1519,7 +1726,8 @@ "required": ["testGenerationJobGroupName", "testGenerationJobId"], "members": { "testGenerationJobGroupName": { "shape": "TestGenerationJobGroupName" }, - "testGenerationJobId": { "shape": "UUID" } + "testGenerationJobId": { "shape": "UUID" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent get test generation request.

" }, @@ -1534,7 +1742,8 @@ "type": "structure", "required": ["transformationJobId"], "members": { - "transformationJobId": { "shape": "TransformationJobId" } + "transformationJobId": { "shape": "TransformationJobId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent get code transformation plan request.

" }, @@ -1550,7 +1759,8 @@ "type": "structure", "required": ["transformationJobId"], "members": { - "transformationJobId": { "shape": "TransformationJobId" } + "transformationJobId": { "shape": "TransformationJobId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent get code transformation request.

" }, @@ -1589,6 +1799,13 @@ "max": 256, "min": 1 }, + "IdentityDetails": { + "type": "structure", + "members": { + "ssoIdentityDetails": { "shape": "SSOIdentityDetails" } + }, + "union": true + }, "Import": { "type": "structure", "members": { @@ -1664,7 +1881,8 @@ "type": "structure", "members": { "maxResults": { "shape": "ListAvailableCustomizationsRequestMaxResultsInteger" }, - "nextToken": { "shape": "Base64EncodedPaginationToken" } + "nextToken": { "shape": "Base64EncodedPaginationToken" }, + "profileArn": { "shape": "ProfileArn" } } }, "ListAvailableCustomizationsRequestMaxResultsInteger": { @@ -1681,13 +1899,35 @@ "nextToken": { "shape": "Base64EncodedPaginationToken" } } }, + "ListAvailableProfilesRequest": { + "type": "structure", + "members": { + "maxResults": { "shape": "ListAvailableProfilesRequestMaxResultsInteger" }, + "nextToken": { "shape": "Base64EncodedPaginationToken" } + } + }, + "ListAvailableProfilesRequestMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 10, + "min": 1 + }, + "ListAvailableProfilesResponse": { + "type": "structure", + "required": ["profiles"], + "members": { + "profiles": { "shape": "ProfileList" }, + "nextToken": { "shape": "Base64EncodedPaginationToken" } + } + }, "ListCodeAnalysisFindingsRequest": { "type": "structure", "required": ["jobId", "codeAnalysisFindingsSchema"], "members": { "jobId": { "shape": "ListCodeAnalysisFindingsRequestJobIdString" }, "nextToken": { "shape": "PaginationToken" }, - "codeAnalysisFindingsSchema": { "shape": "CodeAnalysisFindingsSchema" } + "codeAnalysisFindingsSchema": { "shape": "CodeAnalysisFindingsSchema" }, + "profileArn": { "shape": "ProfileArn" } } }, "ListCodeAnalysisFindingsRequestJobIdString": { @@ -1707,7 +1947,8 @@ "type": "structure", "required": ["userContext"], "members": { - "userContext": { "shape": "UserContext" } + "userContext": { "shape": "UserContext" }, + "profileArn": { "shape": "ProfileArn" } } }, "ListFeatureEvaluationsResponse": { @@ -1717,6 +1958,29 @@ "featureEvaluations": { "shape": "FeatureEvaluationsList" } } }, + "ListWorkspaceMetadataRequest": { + "type": "structure", + "required": ["workspaceRoot"], + "members": { + "workspaceRoot": { "shape": "ListWorkspaceMetadataRequestWorkspaceRootString" }, + "nextToken": { "shape": "String" }, + "maxResults": { "shape": "Integer" }, + "profileArn": { "shape": "ProfileArn" } + } + }, + "ListWorkspaceMetadataRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1 + }, + "ListWorkspaceMetadataResponse": { + "type": "structure", + "required": ["workspaces"], + "members": { + "workspaces": { "shape": "WorkspaceList" }, + "nextToken": { "shape": "String" } + } + }, "Long": { "type": "long", "box": true @@ -1750,16 +2014,98 @@ "min": 1, "pattern": "[-a-zA-Z0-9._]*" }, + "Notifications": { + "type": "list", + "member": { "shape": "NotificationsFeature" }, + "max": 10, + "min": 0 + }, + "NotificationsFeature": { + "type": "structure", + "required": ["feature", "toggle"], + "members": { + "feature": { "shape": "FeatureName" }, + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "OperatingSystem": { "type": "string", "enum": ["MAC", "WINDOWS", "LINUX"], "max": 64, "min": 1 }, + "OptInFeatureToggle": { + "type": "string", + "enum": ["ON", "OFF"] + }, + "OptInFeatures": { + "type": "structure", + "members": { + "promptLogging": { "shape": "PromptLogging" }, + "byUserAnalytics": { "shape": "ByUserAnalytics" }, + "dashboardAnalytics": { "shape": "DashboardAnalytics" }, + "notifications": { "shape": "Notifications" }, + "workspaceContext": { "shape": "WorkspaceContext" } + } + }, "OptOutPreference": { "type": "string", "enum": ["OPTIN", "OPTOUT"] }, + "Origin": { + "type": "string", + "documentation": "

Enum to represent the origin application conversing with Sidekick.

", + "enum": [ + "CHATBOT", + "CONSOLE", + "DOCUMENTATION", + "MARKETING", + "MOBILE", + "SERVICE_INTERNAL", + "UNIFIED_SEARCH", + "UNKNOWN", + "MD", + "IDE", + "SAGE_MAKER", + "CLI", + "AI_EDITOR", + "OPENSEARCH_DASHBOARD", + "GITLAB" + ] + }, + "PackageInfo": { + "type": "structure", + "members": { + "executionCommand": { "shape": "SensitiveString" }, + "buildCommand": { "shape": "SensitiveString" }, + "buildOrder": { "shape": "PackageInfoBuildOrderInteger" }, + "testFramework": { "shape": "String" }, + "packageSummary": { "shape": "PackageInfoPackageSummaryString" }, + "packagePlan": { "shape": "PackageInfoPackagePlanString" }, + "targetFileInfoList": { "shape": "TargetFileInfoList" } + } + }, + "PackageInfoBuildOrderInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "PackageInfoList": { + "type": "list", + "member": { "shape": "PackageInfo" } + }, + "PackageInfoPackagePlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "PackageInfoPackageSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, "PaginationToken": { "type": "string", "max": 2048, @@ -1788,12 +2134,49 @@ "sensitive": true }, "PrimitiveInteger": { "type": "integer" }, + "Profile": { + "type": "structure", + "required": ["arn", "profileName"], + "members": { + "arn": { "shape": "ProfileArn" }, + "identityDetails": { "shape": "IdentityDetails" }, + "profileName": { "shape": "ProfileName" }, + "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" }, + "kmsKeyArn": { "shape": "ResourceArn" }, + "activeFunctionalities": { "shape": "ActiveFunctionalityList" }, + "status": { "shape": "ProfileStatus" }, + "errorDetails": { "shape": "ErrorDetails" }, + "resourcePolicy": { "shape": "ResourcePolicy" }, + "profileType": { "shape": "ProfileType" }, + "optInFeatures": { "shape": "OptInFeatures" }, + "permissionUpdateRequired": { "shape": "Boolean" }, + "applicationProperties": { "shape": "ApplicationPropertiesList" } + } + }, "ProfileArn": { "type": "string", "max": 950, "min": 0, "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, + "ProfileList": { + "type": "list", + "member": { "shape": "Profile" } + }, + "ProfileName": { + "type": "string", + "max": 100, + "min": 1, + "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" + }, + "ProfileStatus": { + "type": "string", + "enum": ["ACTIVE", "CREATING", "CREATE_FAILED", "UPDATING", "UPDATE_FAILED", "DELETING", "DELETE_FAILED"] + }, + "ProfileType": { + "type": "string", + "enum": ["Q_DEVELOPER", "CODEWHISPERER"] + }, "ProgrammingLanguage": { "type": "structure", "required": ["languageName"], @@ -1812,6 +2195,14 @@ "type": "list", "member": { "shape": "TransformationProgressUpdate" } }, + "PromptLogging": { + "type": "structure", + "required": ["s3Uri", "toggle"], + "members": { + "s3Uri": { "shape": "S3Uri" }, + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "Range": { "type": "structure", "required": ["start", "end"], @@ -1885,7 +2276,7 @@ "RelevantDocumentList": { "type": "list", "member": { "shape": "RelevantTextDocument" }, - "max": 5, + "max": 30, "min": 0 }, "RelevantTextDocument": { @@ -1919,7 +2310,7 @@ }, "RelevantTextDocumentTextString": { "type": "string", - "max": 10240, + "max": 40960, "min": 0, "sensitive": true }, @@ -1956,12 +2347,24 @@ "documentation": "

This exception is thrown when describing a resource that does not exist.

", "exception": true }, + "ResourcePolicy": { + "type": "structure", + "required": ["effect"], + "members": { + "effect": { "shape": "ResourcePolicyEffect" } + } + }, + "ResourcePolicyEffect": { + "type": "string", + "enum": ["ALLOW", "DENY"] + }, "ResumeTransformationRequest": { "type": "structure", "required": ["transformationJobId"], "members": { "transformationJobId": { "shape": "TransformationJobId" }, - "userActionStatus": { "shape": "TransformationUserActionStatus" } + "userActionStatus": { "shape": "TransformationUserActionStatus" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent stop code transformation request.

" }, @@ -2004,6 +2407,27 @@ "min": 0, "sensitive": true }, + "S3Uri": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "s3://((?!xn--)[a-z0-9](?![^/]*[.]{2})[a-z0-9-.]{1,61}[a-z0-9](?Represents span in a text

" + "documentation": "

Represents span in a text.

" }, "SpanEndInteger": { "type": "integer", @@ -2143,7 +2573,8 @@ "idempotencyToken": true }, "scope": { "shape": "CodeAnalysisScope" }, - "codeScanName": { "shape": "CodeScanName" } + "codeScanName": { "shape": "CodeScanName" }, + "profileArn": { "shape": "ProfileArn" } } }, "StartCodeAnalysisRequestClientTokenString": { @@ -2173,7 +2604,9 @@ "uploadId": { "shape": "UploadId" }, "description": { "shape": "StartCodeFixJobRequestDescriptionString" }, "ruleId": { "shape": "StartCodeFixJobRequestRuleIdString" }, - "codeFixName": { "shape": "CodeFixName" } + "codeFixName": { "shape": "CodeFixName" }, + "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" }, + "profileArn": { "shape": "ProfileArn" } } }, "StartCodeFixJobRequestDescriptionString": { @@ -2211,7 +2644,8 @@ "codeGenerationId": { "shape": "CodeGenerationId" }, "currentCodeGenerationId": { "shape": "CodeGenerationId" }, "intent": { "shape": "Intent" }, - "intentContext": { "shape": "IntentContext" } + "intentContext": { "shape": "IntentContext" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent start code generation request.

" }, @@ -2238,7 +2672,8 @@ "clientToken": { "shape": "StartTestGenerationRequestClientTokenString", "idempotencyToken": true - } + }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent test generation request.

" }, @@ -2265,7 +2700,8 @@ "required": ["workspaceState", "transformationSpec"], "members": { "workspaceState": { "shape": "WorkspaceState" }, - "transformationSpec": { "shape": "TransformationSpec" } + "transformationSpec": { "shape": "TransformationSpec" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent code transformation request.

" }, @@ -2286,7 +2722,8 @@ "type": "structure", "required": ["transformationJobId"], "members": { - "transformationJobId": { "shape": "TransformationJobId" } + "transformationJobId": { "shape": "TransformationJobId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent stop code transformation request.

" }, @@ -2355,15 +2792,15 @@ "members": { "url": { "shape": "SupplementaryWebLinkUrlString", - "documentation": "

URL of the web reference link

" + "documentation": "

URL of the web reference link.

" }, "title": { "shape": "SupplementaryWebLinkTitleString", - "documentation": "

Title of the web reference link

" + "documentation": "

Title of the web reference link.

" }, "snippet": { "shape": "SupplementaryWebLinkSnippetString", - "documentation": "

Relevant text snippet from the link

" + "documentation": "

Relevant text snippet from the link.

" } }, "documentation": "

Represents an additional reference link retured with the Chat message

" @@ -2418,6 +2855,45 @@ "min": 1, "sensitive": true }, + "TargetFileInfo": { + "type": "structure", + "members": { + "filePath": { "shape": "SensitiveString" }, + "testFilePath": { "shape": "SensitiveString" }, + "testCoverage": { "shape": "TargetFileInfoTestCoverageInteger" }, + "fileSummary": { "shape": "TargetFileInfoFileSummaryString" }, + "filePlan": { "shape": "TargetFileInfoFilePlanString" }, + "codeReferences": { "shape": "References" }, + "numberOfTestMethods": { "shape": "TargetFileInfoNumberOfTestMethodsInteger" } + } + }, + "TargetFileInfoFilePlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "TargetFileInfoFileSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "TargetFileInfoList": { + "type": "list", + "member": { "shape": "TargetFileInfo" } + }, + "TargetFileInfoNumberOfTestMethodsInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "TargetFileInfoTestCoverageInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 0 + }, "TaskAssistPlan": { "type": "list", "member": { "shape": "TaskAssistPlanStep" }, @@ -2511,6 +2987,11 @@ }, "union": true }, + "TenantId": { + "type": "string", + "max": 1024, + "min": 1 + }, "TerminalUserInteractionEvent": { "type": "structure", "members": { @@ -2556,7 +3037,11 @@ "status": { "shape": "TestGenerationJobStatus" }, "shortAnswer": { "shape": "SensitiveString" }, "creationTime": { "shape": "Timestamp" }, - "progressRate": { "shape": "TestGenerationJobProgressRateInteger" } + "progressRate": { "shape": "TestGenerationJobProgressRateInteger" }, + "jobStatusReason": { "shape": "String" }, + "jobSummary": { "shape": "TestGenerationJobJobSummaryString" }, + "jobPlan": { "shape": "TestGenerationJobJobPlanString" }, + "packageInfoList": { "shape": "PackageInfoList" } }, "documentation": "

Represents a test generation job

" }, @@ -2567,6 +3052,18 @@ "min": 1, "pattern": "[a-zA-Z0-9-_]+" }, + "TestGenerationJobJobPlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "TestGenerationJobJobSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, "TestGenerationJobProgressRateInteger": { "type": "integer", "box": true, @@ -2641,7 +3138,7 @@ }, "TextDocumentTextString": { "type": "string", - "max": 10240, + "max": 40000, "min": 0, "sensitive": true }, @@ -2662,6 +3159,124 @@ "enum": ["MONTHLY_REQUEST_COUNT"] }, "Timestamp": { "type": "timestamp" }, + "Tool": { + "type": "structure", + "members": { + "toolSpecification": { "shape": "ToolSpecification" } + }, + "documentation": "

Information about a tool that can be used.

", + "union": true + }, + "ToolDescription": { + "type": "string", + "documentation": "

The description for the tool.

", + "max": 10240, + "min": 1, + "sensitive": true + }, + "ToolInputSchema": { + "type": "structure", + "members": { + "json": { "shape": "SensitiveDocument" } + }, + "documentation": "

The input schema for the tool in JSON format.

" + }, + "ToolName": { + "type": "string", + "documentation": "

The name for the tool.

", + "max": 64, + "min": 0, + "pattern": "[a-zA-Z][a-zA-Z0-9_]*", + "sensitive": true + }, + "ToolResult": { + "type": "structure", + "required": ["toolUseId", "content"], + "members": { + "toolUseId": { "shape": "ToolUseId" }, + "content": { + "shape": "ToolResultContent", + "documentation": "

Content of the tool result.

" + }, + "status": { "shape": "ToolResultStatus" } + }, + "documentation": "

A tool result that contains the results for a tool request that was previously made.

" + }, + "ToolResultContent": { + "type": "list", + "member": { "shape": "ToolResultContentBlock" } + }, + "ToolResultContentBlock": { + "type": "structure", + "members": { + "text": { + "shape": "ToolResultContentBlockTextString", + "documentation": "

A tool result that is text.

" + }, + "json": { + "shape": "SensitiveDocument", + "documentation": "

A tool result that is JSON format data.

" + } + }, + "union": true + }, + "ToolResultContentBlockTextString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "ToolResultStatus": { + "type": "string", + "documentation": "

Status of the tools result.

", + "enum": ["success", "error"] + }, + "ToolResults": { + "type": "list", + "member": { "shape": "ToolResult" }, + "max": 10, + "min": 0 + }, + "ToolSpecification": { + "type": "structure", + "required": ["inputSchema", "name"], + "members": { + "inputSchema": { "shape": "ToolInputSchema" }, + "name": { "shape": "ToolName" }, + "description": { "shape": "ToolDescription" } + }, + "documentation": "

The specification for the tool.

" + }, + "ToolUse": { + "type": "structure", + "required": ["toolUseId", "name", "input"], + "members": { + "toolUseId": { "shape": "ToolUseId" }, + "name": { "shape": "ToolName" }, + "input": { + "shape": "SensitiveDocument", + "documentation": "

The input to pass to the tool.

" + } + }, + "documentation": "

Contains information about a tool that the model is requesting be run. The model uses the result from the tool to generate a response.

" + }, + "ToolUseId": { + "type": "string", + "documentation": "

The ID for the tool request.

", + "max": 64, + "min": 0, + "pattern": "[a-zA-Z0-9_-]+" + }, + "ToolUses": { + "type": "list", + "member": { "shape": "ToolUse" }, + "max": 10, + "min": 0 + }, + "Tools": { + "type": "list", + "member": { "shape": "Tool" } + }, "TransformEvent": { "type": "structure", "required": ["jobId"], @@ -2878,7 +3493,8 @@ "taskAssistPlanningUploadContext": { "shape": "TaskAssistPlanningUploadContext" }, "transformationUploadContext": { "shape": "TransformationUploadContext" }, "codeAnalysisUploadContext": { "shape": "CodeAnalysisUploadContext" }, - "codeFixUploadContext": { "shape": "CodeFixUploadContext" } + "codeFixUploadContext": { "shape": "CodeFixUploadContext" }, + "workspaceContextUploadContext": { "shape": "WorkspaceContextUploadContext" } }, "union": true }, @@ -2897,9 +3513,15 @@ "AUTOMATIC_FILE_SECURITY_SCAN", "FULL_PROJECT_SECURITY_SCAN", "UNIT_TESTS_GENERATION", - "CODE_FIX_GENERATION" + "CODE_FIX_GENERATION", + "WORKSPACE_CONTEXT" ] }, + "Url": { + "type": "string", + "max": 1024, + "min": 1 + }, "UserContext": { "type": "structure", "required": ["ideCategory", "operatingSystem", "product"], @@ -2927,18 +3549,22 @@ }, "userInputMessageContext": { "shape": "UserInputMessageContext", - "documentation": "

Chat message context associated with the Chat Message

" + "documentation": "

Chat message context associated with the Chat Message.

" }, "userIntent": { "shape": "UserIntent", - "documentation": "

User Intent

" + "documentation": "

User Intent.

" + }, + "origin": { + "shape": "Origin", + "documentation": "

User Input Origin.

" } }, - "documentation": "

Structure to represent a chat input message from User

" + "documentation": "

Structure to represent a chat input message from User.

" }, "UserInputMessageContentString": { "type": "string", - "max": 4096, + "max": 160000, "min": 0, "sensitive": true }, @@ -2976,6 +3602,18 @@ "userSettings": { "shape": "UserSettings", "documentation": "

Settings information, e.g., whether the user has enabled cross-region API calls.

" + }, + "additionalContext": { + "shape": "AdditionalContentList", + "documentation": "

List of additional contextual content entries that can be included with the message.

" + }, + "toolResults": { + "shape": "ToolResults", + "documentation": "

ToolResults for the requested ToolUses.

" + }, + "tools": { + "shape": "Tools", + "documentation": "

Tools that can be used.

" } }, "documentation": "

Additional Chat message context associated with the Chat Message

" @@ -3068,6 +3706,35 @@ "documentation": "

Reason for ValidationException

", "enum": ["INVALID_CONVERSATION_ID", "CONTENT_LENGTH_EXCEEDS_THRESHOLD", "INVALID_KMS_GRANT"] }, + "WorkspaceContext": { + "type": "structure", + "required": ["toggle"], + "members": { + "toggle": { "shape": "OptInFeatureToggle" } + } + }, + "WorkspaceContextUploadContext": { + "type": "structure", + "required": ["workspaceId", "relativePath", "programmingLanguage"], + "members": { + "workspaceId": { "shape": "UUID" }, + "relativePath": { "shape": "SensitiveString" }, + "programmingLanguage": { "shape": "ProgrammingLanguage" } + } + }, + "WorkspaceList": { + "type": "list", + "member": { "shape": "WorkspaceMetadata" } + }, + "WorkspaceMetadata": { + "type": "structure", + "required": ["workspaceId", "workspaceStatus"], + "members": { + "workspaceId": { "shape": "UUID" }, + "workspaceStatus": { "shape": "WorkspaceStatus" }, + "environmentId": { "shape": "SensitiveString" } + } + }, "WorkspaceState": { "type": "structure", "required": ["uploadId", "programmingLanguage"], @@ -3087,6 +3754,10 @@ }, "documentation": "

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 ![image](https://github.com/user-attachments/assets/cd6f42e8-5414-4d24-83d6-39c92f3d94ad) --- - 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 ![Screenshot 2025-03-13 at 3 11 39 PM](https://github.com/user-attachments/assets/57f4adb5-6aa5-4eed-be17-164ae163cce5) 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 @@ + + + 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. +
+ +
+ +
@@ -119,10 +129,7 @@ export default defineComponent({ this.doShow = true }, async beforeMount() { - this.availableRegionProfiles = await client.listRegionProfiles() - if (this.availableRegionProfiles.length === 1) { - await client.selectRegionProfile(this.availableRegionProfiles[0]) - } + await this.listAvailableProfiles() }, methods: { toggleItemSelection(itemId: number) { @@ -131,16 +138,32 @@ export default defineComponent({ onClickContinue() { if (this.availableRegionProfiles[this.selectedRegionProfileIndex] !== undefined) { const selectedProfile = this.availableRegionProfiles[this.selectedRegionProfileIndex] - console.log(`user selects ${selectedProfile.name}`) client.selectRegionProfile(selectedProfile) } else { // TODO: handle error } }, + async signout() { + client.emitUiClick('auth_signout') + await client.signout() + }, + async listAvailableProfiles() { + this.errorMessage = '' + const r = await client.listRegionProfiles() + if (typeof r === 'string') { + this.errorMessage = r + } else { + this.availableRegionProfiles = r + // auto select and bypass this profile view if profile count === 1 + if (this.availableRegionProfiles.length === 1) { + await client.selectRegionProfile(this.availableRegionProfiles[0]) + } + } + }, }, }) - diff --git a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts index 0184b44e5ca..aa136b44863 100644 --- a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts +++ b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts @@ -178,7 +178,7 @@ export class ToolkitLoginWebview extends CommonAuthWebview { await this.showResourceExplorer() } - override listRegionProfiles(): Promise { + override listRegionProfiles(): Promise { throw new Error('Method not implemented') } From 9b39743dc049c2138ff66369d92f19847f4be22a Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:05:26 -0700 Subject: [PATCH 09/49] feat(inline): send profile arn with inline APIs #2103 --- packages/core/src/codewhisperer/client/codewhisperer.ts | 3 ++- .../codewhisperer/tracker/codewhispererCodeCoverageTracker.ts | 1 + .../core/src/codewhisperer/tracker/codewhispererTracker.ts | 1 + packages/core/src/codewhisperer/util/editorContext.ts | 4 ++++ packages/core/src/codewhisperer/util/telemetryHelper.ts | 4 ++++ packages/core/src/shared/featureConfig.ts | 2 ++ 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 80dc94ee6df..6f65f542c3a 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -219,9 +219,10 @@ export class DefaultCodeWhispererClient { public async listAvailableCustomizations(): Promise { const client = await this.createUserSdkClient() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const requester = async (request: CodeWhispererUserClient.ListAvailableCustomizationsRequest) => client.listAvailableCustomizations(request).promise() - return pageableToCollection(requester, {}, 'nextToken') + return pageableToCollection(requester, { profileArn: profile?.arn }, 'nextToken') .promise() .then((resps) => { let logStr = 'amazonq: listAvailableCustomizations API request:' diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts index 39416eafe70..0989f022245 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts @@ -145,6 +145,7 @@ export class CodeWhispererCodeCoverageTracker { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { diff --git a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts index dbcfb3ab134..ca19c87505f 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts @@ -161,6 +161,7 @@ export class CodeWhispererTracker { ), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { diff --git a/packages/core/src/codewhisperer/util/editorContext.ts b/packages/core/src/codewhisperer/util/editorContext.ts index 11598cfe20c..4e2173a043e 100644 --- a/packages/core/src/codewhisperer/util/editorContext.ts +++ b/packages/core/src/codewhisperer/util/editorContext.ts @@ -18,6 +18,7 @@ import { checkLeftContextKeywordsForJson } from './commonUtil' import { CodeWhispererSupplementalContext } from '../models/model' import { getOptOutPreference } from '../../shared/telemetry/util' import { indent } from '../../shared/utilities/textUtilities' +import { AuthUtil } from './authUtil' let tabSize: number = getTabSizeSetting() @@ -108,6 +109,8 @@ export async function buildListRecommendationRequest( }) : [] + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile + return { request: { fileContext: fileContext, @@ -118,6 +121,7 @@ export async function buildListRecommendationRequest( supplementalContexts: supplementalContext, customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, optOutPreference: getOptOutPreference(), + profileArn: profile?.arn, }, supplementalMetadata: supplementalContexts, } diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index ced228dba51..1ee4e9328bf 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -160,6 +160,7 @@ export class TelemetryHelper { supplementalContextMetadata?: CodeWhispererSupplementalContext | undefined ) { const selectedCustomization = getSelectedCustomization() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile telemetry.codewhisperer_userTriggerDecision.emit({ codewhispererAutomatedTriggerType: session.autoTriggerType, @@ -218,6 +219,7 @@ export class TelemetryHelper { acceptedCharacterCount: 0, }, }, + profileArn: profile?.arn, }) .then() .catch((error) => { @@ -364,6 +366,7 @@ export class TelemetryHelper { const aggregatedCompletionType = this.sessionDecisions[0].codewhispererCompletionType const aggregatedSuggestionState = this.getAggregatedSuggestionState(this.sessionDecisions) const selectedCustomization = getSelectedCustomization() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const generatedLines = acceptedRecommendationContent.trim() === '' ? 0 : acceptedRecommendationContent.split('\n').length const suggestionCount = this.sessionDecisions @@ -443,6 +446,7 @@ export class TelemetryHelper { acceptedCharacterCount: acceptedRecommendationContent.length, }, }, + profileArn: profile?.arn, }) .then() .catch((error) => { diff --git a/packages/core/src/shared/featureConfig.ts b/packages/core/src/shared/featureConfig.ts index 5ad84b1ded4..6d59fe0782a 100644 --- a/packages/core/src/shared/featureConfig.ts +++ b/packages/core/src/shared/featureConfig.ts @@ -105,6 +105,7 @@ export class FeatureConfigProvider { } public async listFeatureEvaluations(): Promise { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const request: ListFeatureEvaluationsRequest = { userContext: { ideCategory: 'VSCODE', @@ -113,6 +114,7 @@ export class FeatureConfigProvider { clientId: getClientId(globals.globalState), ideVersion: extensionVersion, }, + profileArn: profile?.arn, } return (await client.createUserSdkClient()).listFeatureEvaluations(request).promise() } From 50ea884953ab78da6a61c30b47f37ce49af3fb66 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:05:52 -0700 Subject: [PATCH 10/49] feat(generic chat): send profile arn with chat APIs #2104 --- .../amazonq/src/inlineChat/provider/inlineChatProvider.ts | 1 + .../controllers/chat/chatRequest/converter.test.ts | 1 + packages/core/src/amazonqTest/chat/controller/controller.ts | 1 + .../controllers/chat/chatRequest/converter.ts | 6 +++++- .../src/codewhispererChat/controllers/chat/controller.ts | 3 +++ .../core/src/codewhispererChat/controllers/chat/model.ts | 2 ++ .../codewhispererChat/controllers/chat/telemetryHelper.ts | 2 ++ 7 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index af4ac95a036..e6534d65532 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -66,6 +66,7 @@ export class InlineChatProvider { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromPromptChatMessage(message), customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, context: [], relevantTextDocuments: [], additionalContents: [], diff --git a/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts b/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts index ea0c7889426..6bcbd99bc2e 100644 --- a/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts +++ b/packages/amazonq/test/unit/codewhispererChat/controllers/chat/chatRequest/converter.test.ts @@ -40,6 +40,7 @@ describe('triggerPayloadToChatRequest', () => { userInputContextLength: 0, focusFileContextLength: 0, }, + profile: undefined, context: [], documentReferences: [], query: undefined, diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts index cb5cbc2e851..03e28279b0b 100644 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ b/packages/core/src/amazonqTest/chat/controller/controller.ts @@ -933,6 +933,7 @@ export class TestController { codeQuery: undefined, userIntent: UserIntent.GENERATE_UNIT_TESTS, customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, context: [], relevantTextDocuments: [], additionalContents: [], diff --git a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts index be286122dc6..286ac957d76 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts @@ -30,7 +30,10 @@ export const supportedLanguagesList = [ export const filePathSizeLimit = 4_000 -export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { conversationState: ConversationState } { +export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { + conversationState: ConversationState + profileArn?: string +} { // Flexible truncation logic const remainingPayloadSize = 100_000 @@ -168,6 +171,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c chatTriggerType, customizationArn: customizationArn, }, + profileArn: triggerPayload.profile?.arn, } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index bc0ba63c50d..09d534096e3 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -763,6 +763,7 @@ export class ChatController { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromContextMenuCommand(command), customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, additionalContents: [], relevantTextDocuments: [], documentReferences: [], @@ -848,6 +849,7 @@ export class ChatController { codeQuery: lastTriggerEvent.context?.focusAreaContext?.names, userIntent: message.userIntent, customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, contextLengths: { ...defaultContextLengths, }, @@ -889,6 +891,7 @@ export class ChatController { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromPromptChatMessage(message), customization: getSelectedCustomization(), + profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, context: message.context ?? [], relevantTextDocuments: [], additionalContents: [], diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 8e13360a486..4bc89d6283d 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -11,6 +11,7 @@ import { TabOpenType } from '../../../amazonq/webview/ui/storages/tabsStorage' import { CodeReference } from '../../view/connector/connector' import { Customization } from '../../../codewhisperer/client/codewhispereruserclient' import { QuickActionCommand } from '@aws/mynah-ui' +import { RegionProfile } from '../../../codewhisperer/models/model' export interface TriggerTabIDReceived { tabID: string @@ -186,6 +187,7 @@ export interface TriggerPayload { readonly codeQuery: CodeQuery | undefined readonly userIntent: UserIntent | undefined readonly customization: Customization + readonly profile: RegionProfile | undefined readonly context: string[] | QuickActionCommand[] relevantTextDocuments: RelevantTextDocumentAddition[] additionalContents: AdditionalContentEntryAddition[] diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 5d5cc09056d..f2c447500da 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -375,6 +375,7 @@ export class CWCTelemetryHelper { customizationArn: undefinedIfEmpty(getSelectedCustomization().arn), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch(logSendTelemetryEventFailure) @@ -577,6 +578,7 @@ export class CWCTelemetryHelper { customizationArn: undefinedIfEmpty(getSelectedCustomization().arn), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch(logSendTelemetryEventFailure) From b98000a58eb6d4441831615a08ad40673df62af2 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:06:23 -0700 Subject: [PATCH 11/49] feat(review): send profile arn with codeScan, testGen APIs --- .../commands/startCodeFixGeneration.ts | 8 ++++--- .../commands/startSecurityScan.ts | 10 ++++++--- .../commands/startTestGeneration.ts | 9 ++++++-- .../codewhisperer/service/codeFixHandler.ts | 16 +++++++++----- .../service/securityScanHandler.ts | 22 ++++++++++++++----- .../codewhisperer/service/testGenHandler.ts | 10 ++++++--- .../src/codewhisperer/util/telemetryHelper.ts | 7 ++++++ 7 files changed, 61 insertions(+), 21 deletions(-) diff --git a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts index 8e9a5240812..ea26de5c5fc 100644 --- a/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts +++ b/packages/core/src/codewhisperer/commands/startCodeFixGeneration.ts @@ -19,6 +19,7 @@ import path from 'path' import { TelemetryHelper } from '../util/telemetryHelper' import { tempDirPath } from '../../shared/filesystemUtilities' import { CodeWhispererSettings } from '../util/codewhispererSettings' +import { AuthUtil } from '../util/authUtil' export async function startCodeFixGeneration( client: DefaultCodeWhispererClient, @@ -26,6 +27,7 @@ export async function startCodeFixGeneration( filePath: string, codeFixName: string ) { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile /** * Step 0: Initial code fix telemetry */ @@ -53,7 +55,7 @@ export async function startCodeFixGeneration( */ let artifactMap: ArtifactMap = {} try { - artifactMap = await getPresignedUrlAndUpload(client, zipFilePath, codeFixName) + artifactMap = await getPresignedUrlAndUpload(client, zipFilePath, codeFixName, profile) } finally { await fs.delete(zipFilePath) } @@ -89,7 +91,7 @@ export async function startCodeFixGeneration( * Step 4: Polling mechanism on code fix job status */ throwIfCancelled() - const jobStatus = await pollCodeFixJobStatus(client, String(codeFixJob.jobId)) + const jobStatus = await pollCodeFixJobStatus(client, String(codeFixJob.jobId), profile) if (jobStatus === 'Failed') { getLogger().verbose(`Code fix generation failed.`) throw new CreateCodeFixError() @@ -101,7 +103,7 @@ export async function startCodeFixGeneration( throwIfCancelled() getLogger().verbose(`Code fix job succeeded and start processing result.`) - const { suggestedFix } = await getCodeFixJob(client, String(codeFixJob.jobId)) + const { suggestedFix } = await getCodeFixJob(client, String(codeFixJob.jobId), profile) // eslint-disable-next-line aws-toolkits/no-json-stringify-in-log getLogger().verbose(`Suggested fix: ${JSON.stringify(suggestedFix)}`) return { suggestedFix, jobId } diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index cc9b733f56d..d6c9d5441e4 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -108,6 +108,7 @@ export async function startSecurityScan( zipUtil: ZipUtil = new ZipUtil(), scanUuid?: string ) { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const logger = getLoggerForScope(scope) /** * Step 0: Initial Code Scan telemetry @@ -212,7 +213,8 @@ export async function startSecurityScan( artifactMap, codeScanTelemetryEntry.codewhispererLanguage, scope, - scanName + scanName, + profile ) if (scanJob.status === 'Failed') { logger.verbose(`${scanJob.errorMessage}`) @@ -235,7 +237,8 @@ export async function startSecurityScan( scanUuid, }) } - const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope, codeScanStartTime) + // pass profile + const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope, codeScanStartTime, profile) if (jobStatus === 'Failed') { logger.verbose(`Security scan failed.`) throw new CodeScanJobFailedError() @@ -261,7 +264,8 @@ export async function startSecurityScan( CodeWhispererConstants.codeScanFindingsSchema, projectPaths, scope, - editor + editor, + profile ) for (const issue of securityRecommendationCollection .flatMap(({ issues }) => issues) diff --git a/packages/core/src/codewhisperer/commands/startTestGeneration.ts b/packages/core/src/codewhisperer/commands/startTestGeneration.ts index 29e7148a17b..003790937c0 100644 --- a/packages/core/src/codewhisperer/commands/startTestGeneration.ts +++ b/packages/core/src/codewhisperer/commands/startTestGeneration.ts @@ -21,6 +21,7 @@ import { ChildProcess, spawn } from 'child_process' // eslint-disable-line no-re import { BuildStatus } from '../../amazonqTest/chat/session/session' import { fs } from '../../shared/fs/fs' import { Range } from '../client/codewhispereruserclient' +import { AuthUtil } from '../indexNode' // eslint-disable-next-line unicorn/no-null let spawnResult: ChildProcess | null = null @@ -34,6 +35,7 @@ export async function startTestGenerationProcess( ) { const logger = getLogger() const session = ChatSessionManager.Instance.getSession() + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile // TODO: Step 0: Initial Test Gen telemetry try { logger.verbose(`Starting Test Generation `) @@ -96,7 +98,9 @@ export async function startTestGenerationProcess( targetLineRangeList: selectionRange ? [selectionRange] : [], }, ], - userInputPrompt + userInputPrompt, + undefined, + profile ) if (!testJob.testGenerationJob) { throw Error('Test job not found') @@ -114,7 +118,8 @@ export async function startTestGenerationProcess( testJob.testGenerationJob.testGenerationJobId, testJob.testGenerationJob.testGenerationJobGroupName, filePath, - initialExecution + initialExecution, + profile ) // TODO: Send status to test summary throwIfCancelled() diff --git a/packages/core/src/codewhisperer/service/codeFixHandler.ts b/packages/core/src/codewhisperer/service/codeFixHandler.ts index 4aa74a91ac7..68dc25e7cf3 100644 --- a/packages/core/src/codewhisperer/service/codeFixHandler.ts +++ b/packages/core/src/codewhisperer/service/codeFixHandler.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CodeWhispererUserClient } from '../indexNode' +import { CodeWhispererUserClient, RegionProfile } from '../indexNode' import * as CodeWhispererConstants from '../models/constants' import { codeFixState } from '../models/model' import { ArtifactMap, CreateUploadUrlRequest, DefaultCodeWhispererClient } from '../client/codewhisperer' @@ -22,12 +22,14 @@ import { sleep } from '../../shared/utilities/timeoutUtils' export async function getPresignedUrlAndUpload( client: DefaultCodeWhispererClient, zipFilePath: string, - codeFixName: string + codeFixName: string, + profile: RegionProfile | undefined ) { const srcReq: CreateUploadUrlRequest = { artifactType: 'SourceCode', uploadIntent: CodeWhispererConstants.codeFixUploadIntent, uploadContext: { codeFixUploadContext: { codeFixName } }, + profileArn: profile?.arn, } getLogger().verbose(`Prepare for uploading src context...`) const srcResp = await client.createUploadUrl(srcReq).catch((err) => { @@ -52,7 +54,8 @@ export async function createCodeFixJob( description: string, referenceTrackerConfiguration: CodeWhispererUserClient.ReferenceTrackerConfiguration, codeFixName?: string, - ruleId?: string + ruleId?: string, + profile?: RegionProfile ) { getLogger().verbose(`Creating code fix job...`) const req: CodeWhispererUserClient.StartCodeFixJobRequest = { @@ -62,6 +65,7 @@ export async function createCodeFixJob( ruleId, description, referenceTrackerConfiguration, + profileArn: profile?.arn, } const resp = await client.startCodeFixJob(req).catch((err) => { @@ -75,7 +79,7 @@ export async function createCodeFixJob( return resp } -export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, jobId: string) { +export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, jobId: string, profile?: RegionProfile) { const pollingStartTime = performance.now() await sleep(CodeWhispererConstants.codeFixJobPollingDelayMs) @@ -85,6 +89,7 @@ export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, j throwIfCancelled() const req: CodeWhispererUserClient.GetCodeFixJobRequest = { jobId, + profileArn: profile?.arn, } const resp = await client.getCodeFixJob(req) getLogger().verbose(`GetCodeFixJobRequest requestId: ${resp.$response.requestId}`) @@ -106,9 +111,10 @@ export async function pollCodeFixJobStatus(client: DefaultCodeWhispererClient, j return status } -export async function getCodeFixJob(client: DefaultCodeWhispererClient, jobId: string) { +export async function getCodeFixJob(client: DefaultCodeWhispererClient, jobId: string, profile?: RegionProfile) { const req: CodeWhispererUserClient.GetCodeFixJobRequest = { jobId, + profileArn: profile?.arn, } const resp = await client.getCodeFixJob(req) return resp diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index 7f7a1cbf326..00d46e3184d 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -13,6 +13,7 @@ import { codeScanState, CodeScanStoppedError, onDemandFileScanState, + RegionProfile, } from '../models/model' import { sleep } from '../../shared/utilities/timeoutUtils' import * as codewhispererClient from '../client/codewhisperer' @@ -50,13 +51,18 @@ export async function listScanResults( codeScanFindingsSchema: string, projectPaths: string[], scope: CodeWhispererConstants.CodeAnalysisScope, - editor: vscode.TextEditor | undefined + editor: vscode.TextEditor | undefined, + profile?: RegionProfile ) { const logger = getLoggerForScope(scope) const codeScanIssueMap: Map = new Map() const aggregatedCodeScanIssueList: AggregatedCodeScanIssue[] = [] const requester = (request: codewhispererClient.ListCodeScanFindingsRequest) => client.listCodeScanFindings(request) - const request: codewhispererClient.ListCodeScanFindingsRequest = { jobId, codeScanFindingsSchema } + const request: codewhispererClient.ListCodeScanFindingsRequest = { + jobId, + codeAnalysisFindingsSchema: codeScanFindingsSchema, + profileArn: profile?.arn, + } const collection = pageableToCollection(requester, request, 'nextToken') const issues = await collection .flatten() @@ -203,7 +209,8 @@ export async function pollScanJobStatus( client: DefaultCodeWhispererClient, jobId: string, scope: CodeWhispererConstants.CodeAnalysisScope, - codeScanStartTime: number + codeScanStartTime: number, + profile?: RegionProfile ) { const pollingStartTime = performance.now() // We don't expect to get results immediately, so sleep for some time initially to not make unnecessary calls @@ -216,6 +223,7 @@ export async function pollScanJobStatus( throwIfCancelled(scope, codeScanStartTime) const req: codewhispererClient.GetCodeScanRequest = { jobId: jobId, + profileArn: profile?.arn, } const resp = await client.getCodeScan(req) logger.verbose(`GetCodeScanRequest requestId: ${resp.$response.requestId}`) @@ -242,7 +250,8 @@ export async function createScanJob( artifactMap: codewhispererClient.ArtifactMap, languageId: string, scope: CodeWhispererConstants.CodeAnalysisScope, - scanName: string + scanName: string, + profile?: RegionProfile ) { const logger = getLoggerForScope(scope) logger.verbose(`Creating scan job...`) @@ -254,6 +263,7 @@ export async function createScanJob( }, scope: codeAnalysisScope, codeScanName: scanName, + profileArn: profile?.arn, } const resp = await client.createCodeScan(req).catch((err) => { getLogger().error(`Failed creating scan job. Request id: ${err.requestId}`) @@ -276,7 +286,8 @@ export async function getPresignedUrlAndUpload( client: DefaultCodeWhispererClient, zipMetadata: ZipMetadata, scope: CodeWhispererConstants.CodeAnalysisScope, - scanName: string + scanName: string, + profile?: RegionProfile ) { const artifactMap = await telemetry.amazonq_createUpload.run(async (span) => { const logger = getLoggerForScope(scope) @@ -299,6 +310,7 @@ export async function getPresignedUrlAndUpload( codeScanName: scanName, }, }, + profileArn: profile?.arn, } logger.verbose(`Prepare for uploading src context...`) const srcResp = await client.createUploadUrl(srcReq).catch((err) => { diff --git a/packages/core/src/codewhisperer/service/testGenHandler.ts b/packages/core/src/codewhisperer/service/testGenHandler.ts index 3f4825d37ca..8573dd0dcc6 100644 --- a/packages/core/src/codewhisperer/service/testGenHandler.ts +++ b/packages/core/src/codewhisperer/service/testGenHandler.ts @@ -23,7 +23,7 @@ import { TestGenTimedOutError, } from '../../amazonqTest/error' import { getMd5, uploadArtifactToS3 } from './securityScanHandler' -import { testGenState, Reference } from '../models/model' +import { testGenState, Reference, RegionProfile } from '../models/model' import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient' import { downloadExportResultArchive } from '../../shared/utilities/download' @@ -76,7 +76,8 @@ export async function createTestJob( artifactMap: codewhispererClient.ArtifactMap, relativeTargetPath: TargetCode[], userInputPrompt: string, - clientToken?: string + clientToken?: string, + profile?: RegionProfile ) { const logger = getLogger() logger.verbose(`Creating test job and starting startTestGeneration...`) @@ -96,6 +97,7 @@ export async function createTestJob( userInput: userInputPrompt, testGenerationJobGroupName: ChatSessionManager.Instance.getSession().testGenerationJobGroupName ?? randomUUID(), // TODO: remove fallback clientToken, + profileArn: profile?.arn, } logger.debug('Unit test generation request body: %O', req) logger.debug('target code list: %O', req.targetCodeList[0]) @@ -130,7 +132,8 @@ export async function pollTestJobStatus( jobId: string, jobGroupName: string, filePath: string, - initialExecution: boolean + initialExecution: boolean, + profile?: RegionProfile ) { const session = ChatSessionManager.Instance.getSession() const pollingStartTime = performance.now() @@ -145,6 +148,7 @@ export async function pollTestJobStatus( const req: CodeWhispererUserClient.GetTestGenerationRequest = { testGenerationJobId: jobId, testGenerationJobGroupName: jobGroupName, + profileArn: profile?.arn, } const resp = await codewhispererClient.codeWhispererClient.getTestGeneration(req) logger.verbose('pollTestJobStatus request id: %s', resp.$response.requestId) diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index 1ee4e9328bf..4bb3b92dc33 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -662,6 +662,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -697,6 +698,7 @@ export class TelemetryHelper { codeAnalysisScope: scope === CodeAnalysisScopeClientSide.FILE_AUTO ? 'FILE' : 'PROJECT', }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -726,6 +728,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -763,6 +766,7 @@ export class TelemetryHelper { charsOfCodeGenerated, }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -800,6 +804,7 @@ export class TelemetryHelper { charsOfCodeAccepted, }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -845,6 +850,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { @@ -893,6 +899,7 @@ export class TelemetryHelper { timestamp: new Date(Date.now()), }, }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, }) .then() .catch((error) => { From 64b66e62705a6c99c42e85bee472e65b276b25b1 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:40:33 -0700 Subject: [PATCH 12/49] feat(transform): pass profile arn to transformation APIs ## Problem in scope - transform - APIs used by transform feature - STE --- .../commands/startTransformByQ.ts | 39 ++++++++++++------- .../transformByQ/transformApiHandler.ts | 17 ++++++-- .../transformationHubViewProvider.ts | 4 +- .../commands/transformByQ.test.ts | 6 ++- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index a7ea1365cca..cad260e7012 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -19,6 +19,7 @@ import { TransformByQStatus, TransformationType, TransformationCandidateProject, + RegionProfile, } from '../models/model' import { createZipManifest, @@ -80,6 +81,7 @@ import globals from '../../shared/extensionGlobals' import { convertDateToTimestamp } from '../../shared/datetime' import { findStringInDirectory } from '../../shared/utilities/workspaceUtils' import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' +import { AuthUtil } from '../util/authUtil' export function getFeedbackCommentData() { const jobId = transformByQState.getJobId() @@ -172,6 +174,7 @@ export async function startTransformByQ() { await setTransformationToRunningState() try { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile // Set webview UI to poll for progress startInterval() @@ -179,13 +182,13 @@ export async function startTransformByQ() { const uploadId = await preTransformationUploadCode() // step 2: StartJob and store the returned jobId in TransformByQState - const jobId = await startTransformationJob(uploadId, transformStartTime) + const jobId = await startTransformationJob(uploadId, transformStartTime, profile) // step 3 (intermediate step): show transformation-plan.md file await pollTransformationStatusUntilPlanReady(jobId) // step 4: poll until artifacts are ready to download - await humanInTheLoopRetryLogic(jobId) + await humanInTheLoopRetryLogic(jobId, profile) } catch (error: any) { await transformationJobErrorHandler(error) } finally { @@ -201,16 +204,16 @@ export async function startTransformByQ() { * We only don't want to continue calling pollTransformationStatusUntilComplete if there is no HIL * state ever engaged or we have reached our max amount of HIL retries. */ -export async function humanInTheLoopRetryLogic(jobId: string) { +export async function humanInTheLoopRetryLogic(jobId: string, profile: RegionProfile | undefined) { let status = '' try { - status = await pollTransformationStatusUntilComplete(jobId) + status = await pollTransformationStatusUntilComplete(jobId, profile) if (status === 'PAUSED') { const hilStatusFailure = await initiateHumanInTheLoopPrompt(jobId) if (hilStatusFailure) { // We rejected the changes and resumed the job and should // try to resume normal polling asynchronously - void humanInTheLoopRetryLogic(jobId) + void humanInTheLoopRetryLogic(jobId, profile) } } else { await finalizeTransformByQ(status) @@ -294,9 +297,10 @@ export async function preTransformationUploadCode() { export async function initiateHumanInTheLoopPrompt(jobId: string) { try { + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const humanInTheLoopManager = HumanInTheLoopManager.instance // 1) We need to call GetTransformationPlan to get artifactId - const transformationSteps = await getTransformationSteps(jobId, false) + const transformationSteps = await getTransformationSteps(jobId, false, profile) const { transformationStep, progressUpdate } = findDownloadArtifactStep(transformationSteps) if (!transformationStep || !progressUpdate) { @@ -418,6 +422,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { let successfulFeedbackLoop = true const jobId = transformByQState.getJobId() let hilResult: MetadataResult = MetadataResult.Pass + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile try { if (!selectedDependency) { throw new Error('No dependency selected') @@ -467,7 +472,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { // 8) Once code has been uploaded we will restart the job await resumeTransformationJob(jobId, 'COMPLETED') - void humanInTheLoopRetryLogic(jobId) + void humanInTheLoopRetryLogic(jobId, profile) } catch (err: any) { successfulFeedbackLoop = false CodeTransformTelemetryState.instance.setCodeTransformMetaDataField({ @@ -478,7 +483,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { // If anything went wrong in HIL state, we should restart the job // with the rejected state await terminateHILEarly(jobId) - void humanInTheLoopRetryLogic(jobId) + void humanInTheLoopRetryLogic(jobId, profile) } finally { // Always delete the dependency directories telemetry.codeTransform_humanInTheLoop.emit({ @@ -495,13 +500,17 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { return successfulFeedbackLoop } -export async function startTransformationJob(uploadId: string, transformStartTime: number) { +export async function startTransformationJob( + uploadId: string, + transformStartTime: number, + profile: RegionProfile | undefined +) { let jobId = '' try { await telemetry.codeTransform_jobStart.run(async () => { telemetry.record({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId() }) - jobId = await startJob(uploadId) + jobId = await startJob(uploadId, profile) getLogger().info(`CodeTransformation: jobId: ${jobId}`) telemetry.record({ @@ -537,9 +546,9 @@ export async function startTransformationJob(uploadId: string, transformStartTim return jobId } -export async function pollTransformationStatusUntilPlanReady(jobId: string) { +export async function pollTransformationStatusUntilPlanReady(jobId: string, profile?: RegionProfile) { try { - await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForPlanGenerated) + await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForPlanGenerated, profile) } catch (error) { getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) @@ -582,7 +591,7 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string) { } let plan = undefined try { - plan = await getTransformationPlan(jobId) + plan = await getTransformationPlan(jobId, profile) } catch (error) { // means API call failed getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) @@ -606,10 +615,10 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string) { throwIfCancelled() } -export async function pollTransformationStatusUntilComplete(jobId: string) { +export async function pollTransformationStatusUntilComplete(jobId: string, profile: RegionProfile | undefined) { let status = '' try { - status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl) + status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl, profile) } catch (error) { getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) if (!transformByQState.getJobFailureErrorNotification()) { diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index 37643263861..4965637a416 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -14,6 +14,7 @@ import { HilZipManifest, IHilZipManifestParams, jobPlanProgress, + RegionProfile, sessionJobHistory, StepProgress, TransformationType, @@ -427,7 +428,7 @@ export async function zipCode( return { dependenciesCopied: dependenciesCopied, tempFilePath: tempFilePath, fileSize: zipSize } as ZipCodeResult } -export async function startJob(uploadId: string) { +export async function startJob(uploadId: string, profile: RegionProfile | undefined) { const sourceLanguageVersion = `JAVA_${transformByQState.getSourceJDKVersion()}` const targetLanguageVersion = `JAVA_${transformByQState.getTargetJDKVersion()}` try { @@ -441,6 +442,7 @@ export async function startJob(uploadId: string) { source: { language: sourceLanguageVersion }, // dummy value of JDK8 used for SQL conversions just so that this API can be called target: { language: targetLanguageVersion }, // JAVA_17 or JAVA_21 }, + profileArn: profile?.arn, }) getLogger().info('CodeTransformation: called startJob API successfully') return response.transformationJobId @@ -564,11 +566,12 @@ export function getJobStatisticsHtml(jobStatistics: any) { return htmlString } -export async function getTransformationPlan(jobId: string) { +export async function getTransformationPlan(jobId: string, profile: RegionProfile | undefined) { let response = undefined try { response = await codeWhisperer.codeWhispererClient.codeModernizerGetCodeTransformationPlan({ transformationJobId: jobId, + profileArn: profile?.arn, }) const stepZeroProgressUpdates = response.transformationPlan.transformationSteps[0].progressUpdates @@ -624,7 +627,11 @@ export async function getTransformationPlan(jobId: string) { } } -export async function getTransformationSteps(jobId: string, handleThrottleFlag: boolean) { +export async function getTransformationSteps( + jobId: string, + handleThrottleFlag: boolean, + profile: RegionProfile | undefined +) { try { // prevent ThrottlingException if (handleThrottleFlag) { @@ -632,6 +639,7 @@ export async function getTransformationSteps(jobId: string, handleThrottleFlag: } const response = await codeWhisperer.codeWhispererClient.codeModernizerGetCodeTransformationPlan({ transformationJobId: jobId, + profileArn: profile?.arn, }) return response.transformationPlan.transformationSteps.slice(1) // skip step 0 (contains supplemental info) } catch (e: any) { @@ -641,13 +649,14 @@ export async function getTransformationSteps(jobId: string, handleThrottleFlag: } } -export async function pollTransformationJob(jobId: string, validStates: string[]) { +export async function pollTransformationJob(jobId: string, validStates: string[], profile: RegionProfile | undefined) { let status: string = '' while (true) { throwIfCancelled() try { const response = await codeWhisperer.codeWhispererClient.codeModernizerGetCodeTransformation({ transformationJobId: jobId, + profileArn: profile?.arn, }) status = response.transformationJob.status! if (CodeWhispererConstants.validStatesForBuildSucceeded.includes(status)) { diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 63eae8606bb..2d0585085a9 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -23,6 +23,7 @@ import { import { startInterval } from '../../commands/startTransformByQ' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { convertToTimeString } from '../../../shared/datetime' +import { AuthUtil } from '../../util/authUtil' export class TransformationHubViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.amazonq.transformationHub' @@ -325,7 +326,8 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider jobPlanProgress['generatePlan'] === StepProgress.Succeeded && transformByQState.isRunning() ) { - planSteps = await getTransformationSteps(transformByQState.getJobId(), false) + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile + planSteps = await getTransformationSteps(transformByQState.getJobId(), false, profile) transformByQState.setPlanSteps(planSteps) } let progressHtml diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index ce00b0c52c3..3ec69e70b04 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -233,7 +233,11 @@ describe('transformByQ', function () { } sinon.stub(codeWhisperer.codeWhispererClient, 'codeModernizerGetCodeTransformation').resolves(mockJobResponse) transformByQState.setToSucceeded() - const status = await pollTransformationJob('dummyId', CodeWhispererConstants.validStatesForCheckingDownloadUrl) + const status = await pollTransformationJob( + 'dummyId', + CodeWhispererConstants.validStatesForCheckingDownloadUrl, + undefined + ) assert.strictEqual(status, 'COMPLETED') }) From 0d7ec02db6309ded145e15b6f74610f49895d4e2 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:43:18 -0700 Subject: [PATCH 13/49] feat(feature-dev): pass profile arn to feature dev API ## Problem in scope - Feature dev - Feature dev APIs - STE --- .../codewhispererruntime-2022-11-11.json | 1665 ++++++++++++++++- .../amazonqFeatureDev/client/featureDev.ts | 21 +- 2 files changed, 1624 insertions(+), 62 deletions(-) diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json index 43537b6df2d..812bbd4fd69 100644 --- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json +++ b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json @@ -110,6 +110,64 @@ ], "idempotent": true }, + "CreateUserMemoryEntry": { + "name": "CreateUserMemoryEntry", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "CreateUserMemoryEntryInput" + }, + "output": { + "shape": "CreateUserMemoryEntryOutput" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ], + "idempotent": true + }, + "CreateWorkspace": { + "name": "CreateWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "CreateWorkspaceRequest" + }, + "output": { + "shape": "CreateWorkspaceResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "ConflictException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "DeleteTaskAssistConversation": { "name": "DeleteTaskAssistConversation", "http": { @@ -140,6 +198,64 @@ } ] }, + "DeleteUserMemoryEntry": { + "name": "DeleteUserMemoryEntry", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "DeleteUserMemoryEntryInput" + }, + "output": { + "shape": "DeleteUserMemoryEntryOutput" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "ResourceNotFoundException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ], + "idempotent": true + }, + "DeleteWorkspace": { + "name": "DeleteWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "DeleteWorkspaceRequest" + }, + "output": { + "shape": "DeleteWorkspaceResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "GenerateCompletions": { "name": "GenerateCompletions", "http": { @@ -374,6 +490,33 @@ } ] }, + "ListAvailableProfiles": { + "name": "ListAvailableProfiles", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListAvailableProfilesRequest" + }, + "output": { + "shape": "ListAvailableProfilesResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "ListCodeAnalysisFindings": { "name": "ListCodeAnalysisFindings", "http": { @@ -404,6 +547,33 @@ } ] }, + "ListEvents": { + "name": "ListEvents", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListEventsRequest" + }, + "output": { + "shape": "ListEventsResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "ListFeatureEvaluations": { "name": "ListFeatureEvaluations", "http": { @@ -431,6 +601,60 @@ } ] }, + "ListUserMemoryEntries": { + "name": "ListUserMemoryEntries", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListUserMemoryEntriesInput" + }, + "output": { + "shape": "ListUserMemoryEntriesOutput" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, + "ListWorkspaceMetadata": { + "name": "ListWorkspaceMetadata", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListWorkspaceMetadataRequest" + }, + "output": { + "shape": "ListWorkspaceMetadataResponse" + }, + "errors": [ + { + "shape": "ThrottlingException" + }, + { + "shape": "InternalServerException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, "ResumeTransformation": { "name": "ResumeTransformation", "http": { @@ -696,6 +920,56 @@ "type": "string", "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] }, + "ActiveFunctionalityList": { + "type": "list", + "member": { + "shape": "FunctionalityName" + }, + "max": 10, + "min": 0 + }, + "AdditionalContentEntry": { + "type": "structure", + "required": ["name", "description"], + "members": { + "name": { + "shape": "AdditionalContentEntryNameString" + }, + "description": { + "shape": "AdditionalContentEntryDescriptionString" + }, + "innerContext": { + "shape": "AdditionalContentEntryInnerContextString" + } + } + }, + "AdditionalContentEntryDescriptionString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryInnerContextString": { + "type": "string", + "max": 8192, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryNameString": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "[a-z]+(?:-[a-z0-9]+)*", + "sensitive": true + }, + "AdditionalContentList": { + "type": "list", + "member": { + "shape": "AdditionalContentEntry" + }, + "max": 20, + "min": 0 + }, "AppStudioState": { "type": "structure", "required": ["namespace", "propertyName", "propertyContext"], @@ -738,6 +1012,30 @@ "min": 0, "sensitive": true }, + "ApplicationProperties": { + "type": "structure", + "required": ["tenantId", "applicationArn", "tenantUrl", "applicationType"], + "members": { + "tenantId": { + "shape": "TenantId" + }, + "applicationArn": { + "shape": "ResourceArn" + }, + "tenantUrl": { + "shape": "Url" + }, + "applicationType": { + "shape": "FunctionalityName" + } + } + }, + "ApplicationPropertiesList": { + "type": "list", + "member": { + "shape": "ApplicationProperties" + } + }, "ArtifactId": { "type": "string", "max": 126, @@ -777,15 +1075,32 @@ }, "followupPrompt": { "shape": "FollowupPrompt" + }, + "toolUses": { + "shape": "ToolUses" } } }, "AssistantResponseMessageContentString": { "type": "string", - "max": 4096, + "max": 100000, "min": 0, "sensitive": true }, + "AttributesMap": { + "type": "map", + "key": { + "shape": "AttributesMapKeyString" + }, + "value": { + "shape": "StringList" + } + }, + "AttributesMapKeyString": { + "type": "string", + "max": 128, + "min": 1 + }, "Base64EncodedPaginationToken": { "type": "string", "max": 2048, @@ -796,12 +1111,24 @@ "type": "boolean", "box": true }, - "ChatAddMessageEvent": { + "ByUserAnalytics": { "type": "structure", - "required": ["conversationId", "messageId"], + "required": ["toggle"], "members": { - "conversationId": { - "shape": "ConversationId" + "s3Uri": { + "shape": "S3Uri" + }, + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, + "ChatAddMessageEvent": { + "type": "structure", + "required": ["conversationId", "messageId"], + "members": { + "conversationId": { + "shape": "ConversationId" }, "messageId": { "shape": "MessageId" @@ -849,7 +1176,7 @@ "member": { "shape": "ChatMessage" }, - "max": 10, + "max": 250, "min": 0 }, "ChatInteractWithMessageEvent": { @@ -885,6 +1212,12 @@ }, "userIntent": { "shape": "UserIntent" + }, + "addedIdeDiagnostics": { + "shape": "IdeDiagnosticList" + }, + "removedIdeDiagnostics": { + "shape": "IdeDiagnosticList" } } }, @@ -947,6 +1280,11 @@ } } }, + "ClientId": { + "type": "string", + "max": 255, + "min": 1 + }, "CodeAnalysisFindingsSchema": { "type": "string", "enum": ["codeanalysis/findings/1.0"] @@ -1012,6 +1350,21 @@ "type": "integer", "min": 0 }, + "CodeDescription": { + "type": "structure", + "required": ["href"], + "members": { + "href": { + "shape": "CodeDescriptionHrefString" + } + } + }, + "CodeDescriptionHrefString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, "CodeFixAcceptanceEvent": { "type": "structure", "required": ["jobId"], @@ -1329,7 +1682,11 @@ }, "CreateTaskAssistConversationRequest": { "type": "structure", - "members": {} + "members": { + "profileArn": { + "shape": "ProfileArn" + } + } }, "CreateTaskAssistConversationResponse": { "type": "structure", @@ -1366,6 +1723,9 @@ }, "uploadId": { "shape": "UploadId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -1404,6 +1764,71 @@ } } }, + "CreateUserMemoryEntryInput": { + "type": "structure", + "required": ["memoryEntryString", "origin"], + "members": { + "memoryEntryString": { + "shape": "CreateUserMemoryEntryInputMemoryEntryStringString" + }, + "origin": { + "shape": "Origin" + }, + "profileArn": { + "shape": "CreateUserMemoryEntryInputProfileArnString" + }, + "clientToken": { + "shape": "String", + "idempotencyToken": true + } + } + }, + "CreateUserMemoryEntryInputMemoryEntryStringString": { + "type": "string", + "min": 1, + "sensitive": true + }, + "CreateUserMemoryEntryInputProfileArnString": { + "type": "string", + "min": 1, + "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + }, + "CreateUserMemoryEntryOutput": { + "type": "structure", + "required": ["memoryEntry"], + "members": { + "memoryEntry": { + "shape": "MemoryEntry" + } + } + }, + "CreateWorkspaceRequest": { + "type": "structure", + "required": ["workspaceRoot"], + "members": { + "workspaceRoot": { + "shape": "CreateWorkspaceRequestWorkspaceRootString" + }, + "profileArn": { + "shape": "ProfileArn" + } + } + }, + "CreateWorkspaceRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "CreateWorkspaceResponse": { + "type": "structure", + "required": ["workspace"], + "members": { + "workspace": { + "shape": "WorkspaceMetadata" + } + } + }, "CursorState": { "type": "structure", "members": { @@ -1449,12 +1874,24 @@ "shape": "Customization" } }, + "DashboardAnalytics": { + "type": "structure", + "required": ["toggle"], + "members": { + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, "DeleteTaskAssistConversationRequest": { "type": "structure", "required": ["conversationId"], "members": { "conversationId": { "shape": "ConversationId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -1467,6 +1904,49 @@ } } }, + "DeleteUserMemoryEntryInput": { + "type": "structure", + "required": ["id"], + "members": { + "id": { + "shape": "DeleteUserMemoryEntryInputIdString" + }, + "profileArn": { + "shape": "DeleteUserMemoryEntryInputProfileArnString" + } + } + }, + "DeleteUserMemoryEntryInputIdString": { + "type": "string", + "max": 36, + "min": 36, + "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + }, + "DeleteUserMemoryEntryInputProfileArnString": { + "type": "string", + "min": 1, + "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + }, + "DeleteUserMemoryEntryOutput": { + "type": "structure", + "members": {} + }, + "DeleteWorkspaceRequest": { + "type": "structure", + "required": ["workspaceId"], + "members": { + "workspaceId": { + "shape": "UUID" + }, + "profileArn": { + "shape": "ProfileArn" + } + } + }, + "DeleteWorkspaceResponse": { + "type": "structure", + "members": {} + }, "Description": { "type": "string", "max": 256, @@ -1485,10 +1965,66 @@ }, "union": true }, + "DiagnosticLocation": { + "type": "structure", + "required": ["uri", "range"], + "members": { + "uri": { + "shape": "DiagnosticLocationUriString" + }, + "range": { + "shape": "Range" + } + } + }, + "DiagnosticLocationUriString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "DiagnosticRelatedInformation": { + "type": "structure", + "required": ["location", "message"], + "members": { + "location": { + "shape": "DiagnosticLocation" + }, + "message": { + "shape": "DiagnosticRelatedInformationMessageString" + } + } + }, + "DiagnosticRelatedInformationList": { + "type": "list", + "member": { + "shape": "DiagnosticRelatedInformation" + }, + "max": 1024, + "min": 0 + }, + "DiagnosticRelatedInformationMessageString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, "DiagnosticSeverity": { "type": "string", "enum": ["ERROR", "WARNING", "INFORMATION", "HINT"] }, + "DiagnosticTag": { + "type": "string", + "enum": ["UNNECESSARY", "DEPRECATED"] + }, + "DiagnosticTagList": { + "type": "list", + "member": { + "shape": "DiagnosticTag" + }, + "max": 1024, + "min": 0 + }, "Dimension": { "type": "structure", "members": { @@ -1741,6 +2277,9 @@ }, "useRelevantDocuments": { "shape": "Boolean" + }, + "workspaceFolders": { + "shape": "WorkspaceFolderList" } } }, @@ -1810,6 +2349,65 @@ "max": 100, "min": 0 }, + "ErrorDetails": { + "type": "string", + "max": 2048, + "min": 0 + }, + "Event": { + "type": "structure", + "required": ["eventId", "generationId", "eventTimestamp", "eventType", "eventBlob"], + "members": { + "eventId": { + "shape": "UUID" + }, + "generationId": { + "shape": "UUID" + }, + "eventTimestamp": { + "shape": "SyntheticTimestamp_date_time" + }, + "eventType": { + "shape": "EventType" + }, + "eventBlob": { + "shape": "EventBlob" + } + } + }, + "EventBlob": { + "type": "blob", + "max": 400000, + "min": 1, + "sensitive": true + }, + "EventList": { + "type": "list", + "member": { + "shape": "Event" + }, + "max": 10, + "min": 1 + }, + "EventType": { + "type": "string", + "max": 100, + "min": 1 + }, + "ExternalIdentityDetails": { + "type": "structure", + "members": { + "issuerUrl": { + "shape": "IssuerUrl" + }, + "clientId": { + "shape": "ClientId" + }, + "scimEndpoint": { + "shape": "String" + } + } + }, "FeatureDevCodeAcceptanceEvent": { "type": "structure", "required": ["conversationId", "linesOfCodeAccepted", "charactersOfCodeAccepted"], @@ -1983,6 +2581,21 @@ "min": 0, "sensitive": true }, + "FunctionalityName": { + "type": "string", + "enum": [ + "COMPLETIONS", + "ANALYSIS", + "CONVERSATIONS", + "TASK_ASSIST", + "TRANSFORMATIONS", + "CHAT_CUSTOMIZATION", + "TRANSFORMATIONS_WEBAPP", + "FEATURE_DEVELOPMENT" + ], + "max": 64, + "min": 1 + }, "GenerateCompletionsRequest": { "type": "structure", "required": ["fileContext"], @@ -2013,6 +2626,9 @@ }, "profileArn": { "shape": "ProfileArn" + }, + "workspaceId": { + "shape": "UUID" } } }, @@ -2046,6 +2662,9 @@ "members": { "jobId": { "shape": "GetCodeAnalysisRequestJobIdString" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2072,6 +2691,9 @@ "members": { "jobId": { "shape": "GetCodeFixJobRequestJobIdString" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2101,6 +2723,9 @@ }, "codeGenerationId": { "shape": "CodeGenerationId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2134,6 +2759,9 @@ }, "testGenerationJobId": { "shape": "UUID" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2151,6 +2779,9 @@ "members": { "transformationJobId": { "shape": "TransformationJobId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2169,6 +2800,9 @@ "members": { "transformationJobId": { "shape": "TransformationJobId" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2201,44 +2835,131 @@ "max": 64, "min": 1 }, - "IdempotencyToken": { - "type": "string", - "max": 256, - "min": 1 - }, - "Import": { + "IdeDiagnostic": { "type": "structure", + "required": ["ideDiagnosticType"], "members": { - "statement": { - "shape": "ImportStatementString" + "range": { + "shape": "Range" + }, + "source": { + "shape": "IdeDiagnosticSourceString" + }, + "severity": { + "shape": "DiagnosticSeverity" + }, + "ideDiagnosticType": { + "shape": "IdeDiagnosticType" } } }, - "ImportStatementString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "Imports": { + "IdeDiagnosticList": { "type": "list", "member": { - "shape": "Import" + "shape": "IdeDiagnostic" }, - "max": 10, + "max": 1024, "min": 0 }, - "InlineChatEvent": { - "type": "structure", - "required": ["requestId", "timestamp"], - "members": { - "requestId": { - "shape": "UUID" - }, - "timestamp": { - "shape": "Timestamp" - }, - "inputLength": { + "IdeDiagnosticSourceString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "IdeDiagnosticType": { + "type": "string", + "enum": ["SYNTAX_ERROR", "TYPE_ERROR", "REFERENCE_ERROR", "BEST_PRACTICE", "SECURITY", "OTHER"] + }, + "IdempotencyToken": { + "type": "string", + "max": 256, + "min": 1 + }, + "IdentityDetails": { + "type": "structure", + "members": { + "ssoIdentityDetails": { + "shape": "SSOIdentityDetails" + }, + "externalIdentityDetails": { + "shape": "ExternalIdentityDetails" + } + }, + "union": true + }, + "ImageBlock": { + "type": "structure", + "required": ["format", "source"], + "members": { + "format": { + "shape": "ImageFormat" + }, + "source": { + "shape": "ImageSource" + } + } + }, + "ImageBlocks": { + "type": "list", + "member": { + "shape": "ImageBlock" + }, + "max": 10, + "min": 0 + }, + "ImageFormat": { + "type": "string", + "enum": ["png", "jpeg", "gif", "webp"] + }, + "ImageSource": { + "type": "structure", + "members": { + "bytes": { + "shape": "ImageSourceBytesBlob" + } + }, + "sensitive": true, + "union": true + }, + "ImageSourceBytesBlob": { + "type": "blob", + "max": 1500000, + "min": 1 + }, + "Import": { + "type": "structure", + "members": { + "statement": { + "shape": "ImportStatementString" + } + } + }, + "ImportStatementString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "Imports": { + "type": "list", + "member": { + "shape": "Import" + }, + "max": 10, + "min": 0 + }, + "InlineChatEvent": { + "type": "structure", + "required": ["requestId", "timestamp"], + "members": { + "requestId": { + "shape": "UUID" + }, + "timestamp": { + "shape": "Timestamp" + }, + "inputLength": { "shape": "PrimitiveInteger" }, "numSelectedLines": { @@ -2308,6 +3029,11 @@ "throttling": false } }, + "IssuerUrl": { + "type": "string", + "max": 255, + "min": 1 + }, "LineRangeList": { "type": "list", "member": { @@ -2322,6 +3048,9 @@ }, "nextToken": { "shape": "Base64EncodedPaginationToken" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2343,6 +3072,35 @@ } } }, + "ListAvailableProfilesRequest": { + "type": "structure", + "members": { + "maxResults": { + "shape": "ListAvailableProfilesRequestMaxResultsInteger" + }, + "nextToken": { + "shape": "Base64EncodedPaginationToken" + } + } + }, + "ListAvailableProfilesRequestMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 10, + "min": 1 + }, + "ListAvailableProfilesResponse": { + "type": "structure", + "required": ["profiles"], + "members": { + "profiles": { + "shape": "ProfileList" + }, + "nextToken": { + "shape": "Base64EncodedPaginationToken" + } + } + }, "ListCodeAnalysisFindingsRequest": { "type": "structure", "required": ["jobId", "codeAnalysisFindingsSchema"], @@ -2355,6 +3113,9 @@ }, "codeAnalysisFindingsSchema": { "shape": "CodeAnalysisFindingsSchema" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2375,12 +3136,51 @@ } } }, + "ListEventsRequest": { + "type": "structure", + "required": ["conversationId"], + "members": { + "conversationId": { + "shape": "UUID" + }, + "maxResults": { + "shape": "ListEventsRequestMaxResultsInteger" + }, + "nextToken": { + "shape": "NextToken" + } + } + }, + "ListEventsRequestMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 50, + "min": 1 + }, + "ListEventsResponse": { + "type": "structure", + "required": ["conversationId", "events"], + "members": { + "conversationId": { + "shape": "UUID" + }, + "events": { + "shape": "EventList" + }, + "nextToken": { + "shape": "NextToken" + } + } + }, "ListFeatureEvaluationsRequest": { "type": "structure", "required": ["userContext"], "members": { "userContext": { "shape": "UserContext" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2393,10 +3193,141 @@ } } }, + "ListUserMemoryEntriesInput": { + "type": "structure", + "members": { + "maxResults": { + "shape": "ListUserMemoryEntriesInputMaxResultsInteger" + }, + "profileArn": { + "shape": "ListUserMemoryEntriesInputProfileArnString" + }, + "nextToken": { + "shape": "ListUserMemoryEntriesInputNextTokenString" + } + } + }, + "ListUserMemoryEntriesInputMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 1 + }, + "ListUserMemoryEntriesInputNextTokenString": { + "type": "string", + "min": 1 + }, + "ListUserMemoryEntriesInputProfileArnString": { + "type": "string", + "min": 1, + "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + }, + "ListUserMemoryEntriesOutput": { + "type": "structure", + "required": ["memoryEntries"], + "members": { + "memoryEntries": { + "shape": "MemoryEntryList" + }, + "nextToken": { + "shape": "ListUserMemoryEntriesOutputNextTokenString" + } + } + }, + "ListUserMemoryEntriesOutputNextTokenString": { + "type": "string", + "min": 1 + }, + "ListWorkspaceMetadataRequest": { + "type": "structure", + "members": { + "workspaceRoot": { + "shape": "ListWorkspaceMetadataRequestWorkspaceRootString" + }, + "nextToken": { + "shape": "String" + }, + "maxResults": { + "shape": "Integer" + }, + "profileArn": { + "shape": "ProfileArn" + } + } + }, + "ListWorkspaceMetadataRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "ListWorkspaceMetadataResponse": { + "type": "structure", + "required": ["workspaces"], + "members": { + "workspaces": { + "shape": "WorkspaceList" + }, + "nextToken": { + "shape": "String" + } + } + }, "Long": { "type": "long", "box": true }, + "MemoryEntry": { + "type": "structure", + "required": ["id", "memoryEntryString", "metadata"], + "members": { + "id": { + "shape": "MemoryEntryIdString" + }, + "memoryEntryString": { + "shape": "MemoryEntryMemoryEntryStringString" + }, + "metadata": { + "shape": "MemoryEntryMetadata" + } + } + }, + "MemoryEntryIdString": { + "type": "string", + "max": 36, + "min": 36, + "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + }, + "MemoryEntryList": { + "type": "list", + "member": { + "shape": "MemoryEntry" + } + }, + "MemoryEntryMemoryEntryStringString": { + "type": "string", + "max": 500, + "min": 1, + "sensitive": true + }, + "MemoryEntryMetadata": { + "type": "structure", + "required": ["origin", "createdAt", "updatedAt"], + "members": { + "origin": { + "shape": "Origin" + }, + "attributes": { + "shape": "AttributesMap" + }, + "createdAt": { + "shape": "Timestamp" + }, + "updatedAt": { + "shape": "Timestamp" + } + } + }, "MessageId": { "type": "string", "max": 128, @@ -2435,16 +3366,134 @@ "min": 1, "pattern": "[-a-zA-Z0-9._]*" }, + "NextToken": { + "type": "string", + "max": 1000, + "min": 0 + }, + "Notifications": { + "type": "list", + "member": { + "shape": "NotificationsFeature" + }, + "max": 10, + "min": 0 + }, + "NotificationsFeature": { + "type": "structure", + "required": ["feature", "toggle"], + "members": { + "feature": { + "shape": "FeatureName" + }, + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, "OperatingSystem": { "type": "string", "enum": ["MAC", "WINDOWS", "LINUX"], "max": 64, "min": 1 }, + "OptInFeatureToggle": { + "type": "string", + "enum": ["ON", "OFF"] + }, + "OptInFeatures": { + "type": "structure", + "members": { + "promptLogging": { + "shape": "PromptLogging" + }, + "byUserAnalytics": { + "shape": "ByUserAnalytics" + }, + "dashboardAnalytics": { + "shape": "DashboardAnalytics" + }, + "notifications": { + "shape": "Notifications" + }, + "workspaceContext": { + "shape": "WorkspaceContext" + } + } + }, "OptOutPreference": { "type": "string", "enum": ["OPTIN", "OPTOUT"] }, + "Origin": { + "type": "string", + "enum": [ + "CHATBOT", + "CONSOLE", + "DOCUMENTATION", + "MARKETING", + "MOBILE", + "SERVICE_INTERNAL", + "UNIFIED_SEARCH", + "UNKNOWN", + "MD", + "IDE", + "SAGE_MAKER", + "CLI", + "AI_EDITOR", + "OPENSEARCH_DASHBOARD", + "GITLAB" + ] + }, + "PackageInfo": { + "type": "structure", + "members": { + "executionCommand": { + "shape": "SensitiveString" + }, + "buildCommand": { + "shape": "SensitiveString" + }, + "buildOrder": { + "shape": "PackageInfoBuildOrderInteger" + }, + "testFramework": { + "shape": "String" + }, + "packageSummary": { + "shape": "PackageInfoPackageSummaryString" + }, + "packagePlan": { + "shape": "PackageInfoPackagePlanString" + }, + "targetFileInfoList": { + "shape": "TargetFileInfoList" + } + } + }, + "PackageInfoBuildOrderInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "PackageInfoList": { + "type": "list", + "member": { + "shape": "PackageInfo" + } + }, + "PackageInfoPackagePlanString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, + "PackageInfoPackageSummaryString": { + "type": "string", + "max": 30720, + "min": 0, + "sensitive": true + }, "PaginationToken": { "type": "string", "max": 2048, @@ -2472,12 +3521,86 @@ "PrimitiveInteger": { "type": "integer" }, + "Profile": { + "type": "structure", + "required": ["arn", "profileName"], + "members": { + "arn": { + "shape": "ProfileArn" + }, + "identityDetails": { + "shape": "IdentityDetails" + }, + "profileName": { + "shape": "ProfileName" + }, + "description": { + "shape": "ProfileDescription" + }, + "referenceTrackerConfiguration": { + "shape": "ReferenceTrackerConfiguration" + }, + "kmsKeyArn": { + "shape": "ResourceArn" + }, + "activeFunctionalities": { + "shape": "ActiveFunctionalityList" + }, + "status": { + "shape": "ProfileStatus" + }, + "errorDetails": { + "shape": "ErrorDetails" + }, + "resourcePolicy": { + "shape": "ResourcePolicy" + }, + "profileType": { + "shape": "ProfileType" + }, + "optInFeatures": { + "shape": "OptInFeatures" + }, + "permissionUpdateRequired": { + "shape": "Boolean" + }, + "applicationProperties": { + "shape": "ApplicationPropertiesList" + } + } + }, "ProfileArn": { "type": "string", "max": 950, "min": 0, "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, + "ProfileDescription": { + "type": "string", + "max": 256, + "min": 1, + "pattern": "[\\sa-zA-Z0-9_-]*" + }, + "ProfileList": { + "type": "list", + "member": { + "shape": "Profile" + } + }, + "ProfileName": { + "type": "string", + "max": 100, + "min": 1, + "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" + }, + "ProfileStatus": { + "type": "string", + "enum": ["ACTIVE", "CREATING", "CREATE_FAILED", "UPDATING", "UPDATE_FAILED", "DELETING", "DELETE_FAILED"] + }, + "ProfileType": { + "type": "string", + "enum": ["Q_DEVELOPER", "CODEWHISPERER"] + }, "ProgrammingLanguage": { "type": "structure", "required": ["languageName"], @@ -2499,6 +3622,18 @@ "shape": "TransformationProgressUpdate" } }, + "PromptLogging": { + "type": "structure", + "required": ["s3Uri", "toggle"], + "members": { + "s3Uri": { + "shape": "S3Uri" + }, + "toggle": { + "shape": "OptInFeatureToggle" + } + } + }, "Range": { "type": "structure", "required": ["start", "end"], @@ -2569,7 +3704,7 @@ "member": { "shape": "RelevantTextDocument" }, - "max": 5, + "max": 30, "min": 0 }, "RelevantTextDocument": { @@ -2587,12 +3722,6 @@ }, "documentSymbols": { "shape": "DocumentSymbols" - }, - "startLine": { - "shape": "Integer" - }, - "endLine": { - "shape": "Integer" } } }, @@ -2604,7 +3733,7 @@ }, "RelevantTextDocumentTextString": { "type": "string", - "max": 10240, + "max": 40960, "min": 0, "sensitive": true }, @@ -2646,6 +3775,19 @@ }, "exception": true }, + "ResourcePolicy": { + "type": "structure", + "required": ["effect"], + "members": { + "effect": { + "shape": "ResourcePolicyEffect" + } + } + }, + "ResourcePolicyEffect": { + "type": "string", + "enum": ["ALLOW", "DENY"] + }, "ResumeTransformationRequest": { "type": "structure", "required": ["transformationJobId"], @@ -2655,6 +3797,9 @@ }, "userActionStatus": { "shape": "TransformationUserActionStatus" + }, + "profileArn": { + "shape": "ProfileArn" } } }, @@ -2682,17 +3827,44 @@ } } }, - "RuntimeDiagnosticMessageString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "RuntimeDiagnosticSourceString": { + "RuntimeDiagnosticMessageString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "RuntimeDiagnosticSourceString": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "S3Uri": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "s3://((?!xn--)[a-z0-9](?![^/]*[.]{2})[a-z0-9-.]{1,61}[a-z0-9](? Date: Thu, 27 Mar 2025 15:43:46 -0700 Subject: [PATCH 14/49] telemetry(amazonq): didSelectProfile ## Problem when calling `switchProfile`, emit didSelectProfile with corresponding user intent 1. user -> when user click on menu `switch profile` action 2. auth -> when user log in and prompted to profile selection page 3. plugin -> when switchProfile is invoked by plugin (us) on users' behalf (this will happen when users start a new IDE instance and we want to auto-switch user's selection in other IDE instance etc) --- .../region/regionProfileManager.test.ts | 42 +++++---- .../webview/generators/webViewContent.ts | 5 +- .../region/regionProfileManager.ts | 90 ++++++++++++++----- .../codewhisperer/ui/codeWhispererNodes.ts | 6 +- .../webview/vue/amazonq/backend_amazonq.ts | 16 +++- .../core/src/login/webview/vue/backend.ts | 3 +- .../webview/vue/regionProfileSelector.vue | 4 +- .../webview/vue/toolkit/backend_toolkit.ts | 3 +- .../src/shared/telemetry/vscodeTelemetry.json | 40 +++++++++ 9 files changed, 154 insertions(+), 55 deletions(-) diff --git a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts index 3f948c5be10..26713b4caa9 100644 --- a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts @@ -93,13 +93,13 @@ describe('RegionProfileManager', function () { describe('switch and get profile', function () { it('should switch if connection is IdC', async function () { await setupConnection('idc') - await sut.switchRegionProfile(profileFoo) + await sut.switchRegionProfile(profileFoo, 'user') 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) + await sut.switchRegionProfile(profileFoo, 'user') assert.deepStrictEqual(sut.activeRegionProfile, undefined) }) }) @@ -115,7 +115,7 @@ describe('RegionProfileManager', function () { it(`builder id should always use default profile IAD`, async function () { await setupConnection('builderId') - await sut.switchRegionProfile(profileFoo) + await sut.switchRegionProfile(profileFoo, 'user') assert.deepStrictEqual(sut.activeRegionProfile, undefined) const conn = authUtil.conn if (!conn) { @@ -127,12 +127,15 @@ describe('RegionProfileManager', function () { 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', - }) + await sut.switchRegionProfile( + { + name: 'foo', + region: 'eu-central-1', + arn: 'foo arn', + description: 'foo description', + }, + 'user' + ) assert.ok(sut.activeRegionProfile) assert.deepStrictEqual(sut.clientConfig, { region: 'eu-central-1', @@ -142,12 +145,15 @@ describe('RegionProfileManager', function () { 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', - }) + await sut.switchRegionProfile( + { + name: 'foo', + region: 'unknown region', + arn: 'foo arn', + description: 'foo description', + }, + 'user' + ) assert.throws(() => { sut.clientConfig @@ -158,7 +164,7 @@ describe('RegionProfileManager', function () { describe('persistence', function () { it('persistSelectedRegionProfile', async function () { await setupConnection('idc') - await sut.switchRegionProfile(profileFoo) + await sut.switchRegionProfile(profileFoo, 'user') assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) const conn = authUtil.conn if (!conn) { @@ -199,7 +205,7 @@ describe('RegionProfileManager', function () { it('should reset activeProfile and global state', async function () { // setup await setupConnection('idc') - await sut.switchRegionProfile(profileFoo) + await sut.switchRegionProfile(profileFoo, 'user') assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) const conn = authUtil.conn if (!conn) { @@ -230,7 +236,7 @@ describe('RegionProfileManager', function () { describe('createQClient', function () { it(`should configure the endpoint and region correspondingly`, async function () { await setupConnection('idc') - await sut.switchRegionProfile(profileFoo) + await sut.switchRegionProfile(profileFoo, 'user') assert.deepStrictEqual(sut.activeRegionProfile, profileFoo) const conn = authUtil.conn as SsoConnection diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts index dbfce0c7f18..5fe9aea6b54 100644 --- a/packages/core/src/amazonq/webview/generators/webViewContent.ts +++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts @@ -89,10 +89,7 @@ export class WebViewContentGenerator { // 1. profile count >= 2 // 2. not default (fallback) which has empty arn let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile - if ( - (regionProfile && AuthUtil.instance.regionProfileManager.isDefault(regionProfile)) || - AuthUtil.instance.regionProfileManager.profiles.length === 1 - ) { + if (AuthUtil.instance.regionProfileManager.profiles.length === 1) { regionProfile = undefined } diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index 89456be7611..03e49d56f27 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -26,15 +26,9 @@ import { getLogger } from '../../shared/logger/logger' import { pageableToCollection } from '../../shared/utilities/collectionUtils' import { parse } from '@aws-sdk/util-arn-parser' import { isAwsError, ToolkitError } from '../../shared/errors' +import { telemetry } from '../../shared/telemetry/telemetry' import { localize } from '../../shared/utilities/vsCodeUtils' -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', @@ -48,6 +42,14 @@ const endpoints = createConstantMap({ 'eu-central-1': 'https://rts.prod-eu-central-1.codewhisperer.ai.aws.dev/', }) +/** + * 'user' -> users change the profile through Q menu + * 'auth' -> users change the profile through webview profile selector page + * 'update' -> plugin auto select the profile on users' behalf as there is only 1 profile + * 'reload' -> on plugin restart, plugin will try to reload previous selected profile + */ +export type ProfileSwitchIntent = 'user' | 'auth' | 'update' | 'reload' + export class RegionProfileManager { private static logger = getLogger() private _activeRegionProfile: RegionProfile | undefined @@ -103,6 +105,8 @@ export class RegionProfileManager { constructor(private readonly connectionProvider: () => Connection | undefined) {} async listRegionProfile(): Promise { + this._profiles = [] + const conn = this.connectionProvider() if (conn === undefined || !isSsoConnection(conn)) { return [] @@ -145,7 +149,7 @@ export class RegionProfileManager { return availableProfiles } - async switchRegionProfile(regionProfile: RegionProfile | undefined) { + async switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) { const conn = this.connectionProvider() if (conn === undefined || !isIdcSsoConnection(conn)) { return @@ -155,6 +159,9 @@ export class RegionProfileManager { return } + // TODO: make it typesafe + const ssoConn = this.connectionProvider() as SsoConnection + // only prompt to users when users switch from A profile to B profile if (this.activeRegionProfile !== undefined && regionProfile !== undefined) { const response = await showConfirmationMessage({ @@ -169,10 +176,35 @@ export class RegionProfileManager { }) if (!response) { + telemetry.amazonq_didSelectProfile.emit({ + source: source, + amazonQProfileRegion: this.activeRegionProfile?.region ?? 'not-set', + ssoRegion: ssoConn.ssoRegion, + result: 'Cancelled', + credentialStartUrl: ssoConn.startUrl, + profileCount: this.profiles.length, + }) return } } + if (source === 'reload' || source === 'update') { + telemetry.amazonq_profileState.emit({ + source: source, + amazonQProfileRegion: regionProfile?.region ?? 'not-set', + result: 'Succeeded', + }) + } else { + telemetry.amazonq_didSelectProfile.emit({ + source: source, + amazonQProfileRegion: regionProfile?.region ?? 'not-set', + ssoRegion: ssoConn.ssoRegion, + result: 'Succeeded', + credentialStartUrl: ssoConn.startUrl, + profileCount: this.profiles.length, + }) + } + await this._switchRegionProfile(regionProfile) } @@ -181,7 +213,7 @@ export class RegionProfileManager { this._onDidChangeRegionProfile.fire(regionProfile) // dont show if it's a default (fallback) - if (regionProfile && !this.isDefault(regionProfile) && this.profiles.length > 1) { + if (regionProfile && this.profiles.length > 1) { void vscode.window.showInformationMessage(`You are using the ${regionProfile.name} profile for Q.`).then() } @@ -203,10 +235,32 @@ export class RegionProfileManager { return } // cross-validation - const profiles = this.listRegionProfile() - const r = (await profiles).find((it) => it.arn === previousSelected) + let profiles: RegionProfile[] = [] + try { + profiles = await this.listRegionProfile() + } catch (e) { + telemetry.amazonq_profileState.emit({ + source: 'reload', + amazonQProfileRegion: 'not-set', + reason: (e as Error).message, + result: 'Failed', + }) - await this.switchRegionProfile(r) + return + } + + const r = profiles.find((it) => it.arn === previousSelected) + if (!r) { + telemetry.amazonq_profileState.emit({ + source: 'reload', + amazonQProfileRegion: 'not-set', + reason: 'profile could not be selected', + result: 'Failed', + }) + return + } + + await this.switchRegionProfile(r, 'reload') } private loadPersistedRegionProfle(): { [label: string]: string } { @@ -223,7 +277,7 @@ export class RegionProfileManager { const conn = this.connectionProvider() // default has empty arn and shouldn't be persisted because it's just a fallback - if (!conn || this.activeRegionProfile === undefined || this.isDefault(this.activeRegionProfile)) { + if (!conn || this.activeRegionProfile === undefined) { return } @@ -238,14 +292,6 @@ 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 let profiles: RegionProfile[] = [] @@ -264,7 +310,7 @@ export class RegionProfileManager { const quickPickItems: DataQuickPickItem[] = profiles.map((it) => { const label = it.name const onClick = async () => { - await this.switchRegionProfile(it) + await this.switchRegionProfile(it, 'user') } const data = it.arn const description = it.region diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts index 9f90189c2ff..d804cbedd46 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -139,11 +139,7 @@ export function createSelectCustomization(): DataQuickPickItem<'selectCustomizat } export function createSelectRegionProfileNode(): DataQuickPickItem<'selectRegionProfile'> { - 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 selectedRegionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile const label = 'Change Profile' const icon = getIcon('vscode-arrow-swap') 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 72d1f80e734..bc5e7c429d5 100644 --- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts +++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts @@ -23,6 +23,8 @@ import { ToolkitError } from '../../../../shared/errors' import { withTelemetryContext } from '../../../../shared/telemetry/util' import { builderIdStartUrl } from '../../../../auth/sso/constants' import { RegionProfile } from '../../../../codewhisperer/models/model' +import { telemetry } from '../../../../shared/telemetry/telemetry' +import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager' const className = 'AmazonQLoginWebview' export class AmazonQLoginWebview extends CommonAuthWebview { @@ -214,12 +216,22 @@ export class AmazonQLoginWebview extends CommonAuthWebview { try { return await AuthUtil.instance.regionProfileManager.listRegionProfile() } catch (e) { + const conn = AuthUtil.instance.conn as SsoConnection | undefined + telemetry.amazonq_didSelectProfile.emit({ + source: 'auth', + amazonQProfileRegion: AuthUtil.instance.regionProfileManager.activeRegionProfile?.region ?? 'not-set', + ssoRegion: conn?.ssoRegion, + result: 'Failed', + credentialStartUrl: conn?.startUrl, + reason: (e as Error).message, + }) + return (e as Error).message } } - override selectRegionProfile(profile: RegionProfile) { - return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile) + override selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent) { + return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile, source) } private setupConnectionEventEmitter(): void { diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts index 25e4b046205..100c7a7548e 100644 --- a/packages/core/src/login/webview/vue/backend.ts +++ b/packages/core/src/login/webview/vue/backend.ts @@ -33,6 +33,7 @@ import { AuthSSOServer } from '../../../auth/sso/server' import { getLogger } from '../../../shared/logger/logger' import { isValidUrl } from '../../../shared/utilities/uriUtils' import { RegionProfile } from '../../../codewhisperer/models/model' +import { ProfileSwitchIntent } from '../../../codewhisperer/region/regionProfileManager' export abstract class CommonAuthWebview extends VueWebview { private readonly className = 'CommonAuthWebview' @@ -209,7 +210,7 @@ export abstract class CommonAuthWebview extends VueWebview { abstract listRegionProfiles(): Promise - abstract selectRegionProfile(profile: RegionProfile): Promise + abstract selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent): Promise /** * Emit stored metric metadata. Does not reset the stored metric metadata, because it diff --git a/packages/core/src/login/webview/vue/regionProfileSelector.vue b/packages/core/src/login/webview/vue/regionProfileSelector.vue index c51570bd79a..35ab1228e28 100644 --- a/packages/core/src/login/webview/vue/regionProfileSelector.vue +++ b/packages/core/src/login/webview/vue/regionProfileSelector.vue @@ -138,7 +138,7 @@ export default defineComponent({ onClickContinue() { if (this.availableRegionProfiles[this.selectedRegionProfileIndex] !== undefined) { const selectedProfile = this.availableRegionProfiles[this.selectedRegionProfileIndex] - client.selectRegionProfile(selectedProfile) + client.selectRegionProfile(selectedProfile, 'auth') } else { // TODO: handle error } @@ -156,7 +156,7 @@ export default defineComponent({ this.availableRegionProfiles = r // auto select and bypass this profile view if profile count === 1 if (this.availableRegionProfiles.length === 1) { - await client.selectRegionProfile(this.availableRegionProfiles[0]) + await client.selectRegionProfile(this.availableRegionProfiles[0], 'update') } } }, diff --git a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts index aa136b44863..3c2a9e2e8ec 100644 --- a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts +++ b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts @@ -22,6 +22,7 @@ import { AuthError, AuthFlowState } from '../types' import { setContext } from '../../../../shared/vscode/setContext' import { builderIdStartUrl } from '../../../../auth/sso/constants' import { RegionProfile } from '../../../../codewhisperer/models/model' +import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager' export class ToolkitLoginWebview extends CommonAuthWebview { public override id: string = 'aws.toolkit.AmazonCommonAuth' @@ -182,7 +183,7 @@ export class ToolkitLoginWebview extends CommonAuthWebview { throw new Error('Method not implemented') } - override selectRegionProfile(profile: RegionProfile): Promise { + override selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent): Promise { throw new Error('Method not implemented') } } diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index 4bc7052e865..ffd715cbed8 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -1,5 +1,20 @@ { "types": [ + { + "name": "amazonQProfileRegion", + "type": "string", + "description": "Region of the Q Profile associated with a metric\n- \"n/a\" if metric is not associated with a profile or region.\n- \"not-set\" if metric is associated with a profile, but profile is unknown." + }, + { + "name": "ssoRegion", + "type": "string", + "description": "Region of the current SSO connection. Typically associated with credentialStartUrl\n- \"n/a\" if metric is not associated with a region.\n- \"not-set\" if metric is associated with a region, but region is unknown." + }, + { + "name": "profileCount", + "type": "int", + "description": "The number of profiles that were available to choose from" + }, { "name": "amazonGenerateApproachLatency", "type": "double", @@ -231,6 +246,31 @@ } ], "metrics": [ + { + "name": "amazonq_didSelectProfile", + "description": "Emitted after the user's Q Profile has been set, whether the user was prompted with a dialog, or a profile was automatically assigned after signing in.", + "metadata": [ + { "type": "source" }, + { "type": "amazonQProfileRegion" }, + { "type": "result" }, + { "type": "ssoRegion", "required": false }, + { "type": "credentialStartUrl", "required": false }, + { "type": "profileCount", "required": false } + ], + "passive": true + }, + { + "name": "amazonq_profileState", + "description": "Indicates a change in the user's Q Profile state", + "metadata": [ + { "type": "source" }, + { "type": "amazonQProfileRegion" }, + { "type": "result" }, + { "type": "ssoRegion", "required": false }, + { "type": "credentialStartUrl", "required": false } + ], + "passive": true + }, { "name": "ide_fileSystem", "description": "File System event on execution", From 77d066fb6eb9ab0bfd5ef796449c8081d77f3a4d Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Fri, 28 Mar 2025 07:42:22 -0700 Subject: [PATCH 15/49] fix(amazonq): chat enablement isn't updated correctly on profile changed --- packages/amazonq/src/app/amazonqScan/app.ts | 3 +++ .../core/src/amazonq/webview/generators/webViewContent.ts | 5 +---- packages/core/src/amazonqDoc/app.ts | 3 +++ packages/core/src/amazonqFeatureDev/app.ts | 3 +++ packages/core/src/amazonqGumby/app.ts | 3 +++ packages/core/src/amazonqTest/app.ts | 3 +++ 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/amazonq/src/app/amazonqScan/app.ts b/packages/amazonq/src/app/amazonqScan/app.ts index 0465048da77..21857163bd2 100644 --- a/packages/amazonq/src/app/amazonqScan/app.ts +++ b/packages/amazonq/src/app/amazonqScan/app.ts @@ -70,6 +70,9 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) Commands.register('aws.amazonq.security.scan-statusbar', async () => { if (AuthUtil.instance.isConnectionExpired()) { diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts index 5fe9aea6b54..a813ae35d50 100644 --- a/packages/core/src/amazonq/webview/generators/webViewContent.ts +++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts @@ -94,10 +94,7 @@ 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 + const authState = (await AuthUtil.instance.getChatAuthState()).amazonQ return ` diff --git a/packages/core/src/amazonqDoc/app.ts b/packages/core/src/amazonqDoc/app.ts index bf64c71c387..929cf1d45de 100644 --- a/packages/core/src/amazonqDoc/app.ts +++ b/packages/core/src/amazonqDoc/app.ts @@ -100,4 +100,7 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) } diff --git a/packages/core/src/amazonqFeatureDev/app.ts b/packages/core/src/amazonqFeatureDev/app.ts index 0af05cf0530..a016d2ba481 100644 --- a/packages/core/src/amazonqFeatureDev/app.ts +++ b/packages/core/src/amazonqFeatureDev/app.ts @@ -106,4 +106,7 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) } diff --git a/packages/core/src/amazonqGumby/app.ts b/packages/core/src/amazonqGumby/app.ts index 1638231ef24..21182b38155 100644 --- a/packages/core/src/amazonqGumby/app.ts +++ b/packages/core/src/amazonqGumby/app.ts @@ -67,6 +67,9 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) showTransformationHub.register() diff --git a/packages/core/src/amazonqTest/app.ts b/packages/core/src/amazonqTest/app.ts index 62ed24aee0d..6c638c13b71 100644 --- a/packages/core/src/amazonqTest/app.ts +++ b/packages/core/src/amazonqTest/app.ts @@ -68,6 +68,9 @@ export function init(appContext: AmazonQAppInitContext) { AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { return debouncedEvent() }) + AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { + return debouncedEvent() + }) testGenState.setChatControllers(testChatControllerEventEmitters) // TODO: Add testGen provider for creating new files after test generation if they does not exist } From e0227a263921d089780a40573a857c35978cc5e5 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Mon, 31 Mar 2025 08:47:30 -0700 Subject: [PATCH 16/49] telemetry(amazonq): webview didLoadModule --- .../webview/vue/amazonq/backend_amazonq.ts | 9 ++++++ .../core/src/login/webview/vue/backend.ts | 2 +- .../webview/vue/regionProfileSelector.vue | 20 ++++++++++++- packages/core/src/login/webview/vue/root.vue | 30 ++++++++++++++----- 4 files changed, 52 insertions(+), 9 deletions(-) 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 bc5e7c429d5..0853f80e952 100644 --- a/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts +++ b/packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts @@ -23,6 +23,8 @@ import { ToolkitError } from '../../../../shared/errors' import { withTelemetryContext } from '../../../../shared/telemetry/util' import { builderIdStartUrl } from '../../../../auth/sso/constants' import { RegionProfile } from '../../../../codewhisperer/models/model' +import { randomUUID } from '../../../../shared/crypto' +import globals from '../../../../shared/extensionGlobals' import { telemetry } from '../../../../shared/telemetry/telemetry' import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager' @@ -162,6 +164,13 @@ export class AmazonQLoginWebview extends CommonAuthWebview { return } else if (featureAuthStates.amazonQ === 'pendingProfileSelection') { this.authState = 'PENDING_PROFILE_SELECTION' + // possible that user starts with "profile selection" state therefore the timeout for auth flow should be disposed otherwise will emit failure + this.loadMetadata?.loadTimeout?.dispose() + this.loadMetadata = { + traceId: randomUUID(), + loadTimeout: undefined, + start: globals.clock.Date.now(), + } return } this.authState = 'LOGIN' diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts index 100c7a7548e..9fc91589e91 100644 --- a/packages/core/src/login/webview/vue/backend.ts +++ b/packages/core/src/login/webview/vue/backend.ts @@ -69,7 +69,7 @@ export abstract class CommonAuthWebview extends VueWebview { * @param errorMessage IF an error is caught on the frontend, this is the message. It will result in a failure metric. * Otherwise we assume success. */ - public setUiReady(state: 'login' | 'reauth', errorMessage?: string) { + public setUiReady(state: 'login' | 'reauth' | 'selectProfile', errorMessage?: string) { if (errorMessage) { this.setLoadFailure(state, errorMessage) } else { diff --git a/packages/core/src/login/webview/vue/regionProfileSelector.vue b/packages/core/src/login/webview/vue/regionProfileSelector.vue index 35ab1228e28..1e3dbb45adb 100644 --- a/packages/core/src/login/webview/vue/regionProfileSelector.vue +++ b/packages/core/src/login/webview/vue/regionProfileSelector.vue @@ -79,7 +79,13 @@ @@ -162,6 +168,18 @@ export default defineComponent({ }, }, }) + +/** + * The ID of the element we will use to determine that the UI has completed its initial load. + * + * This makes assumptions that we will be in a certain state of the UI (eg showing a form vs. a loading bar). + * So if the UI flow changes, this may need to be updated. + */ +export function getReadyElementId() { + // On every initial load, we ASSUME that the user will always be in the connection selection state, + // which is why we specifically look for this button. + return 'profile-selection-continue-button' +} From bab1a7085c38de82f22b84da08c74c7b7d6398d8 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:01:01 -0700 Subject: [PATCH 22/49] fix(amazonq): make cross-validating async ## Problem https://quip-amazon.com/MOnPAkOTTGzQ/Amazon-Q-Developer-Bug-Bash-for-FRA#temp:C:aQWe6702ee5b8e6421785dd3e9eb Profile selection page briefly appears on startup before defaulting to a profile then going to chat. Ideally it is not shown at all ## Solution because on project startup, before plugin finish "validating" selected profile, `AuthUtils.requireProfileSeleciton` will return `true` therefore the webview page will briefly appear and be gone when the plugin receives API response. --- .../region/regionProfileManager.test.ts | 12 ++-- .../region/regionProfileManager.ts | 57 ++++++++++--------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts index 34f5654d80d..af79f7dc2e5 100644 --- a/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts @@ -173,13 +173,13 @@ describe('RegionProfileManager', function () { await sut.persistSelectRegionProfile() - const state = globals.globalState.tryGet<{ [label: string]: string }>( + const state = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', Object, {} ) - assert.strictEqual(state[conn.id], profileFoo.arn) + assert.strictEqual(state[conn.id], profileFoo) }) it(`restoreRegionProfile`, async function () { @@ -191,7 +191,7 @@ describe('RegionProfileManager', function () { } const state = {} as any - state[conn.id] = profileFoo.arn + state[conn.id] = profileFoo await globals.globalState.update('aws.amazonq.regionProfiles', state) @@ -212,19 +212,19 @@ describe('RegionProfileManager', function () { fail('connection should not be undefined') } await sut.persistSelectRegionProfile() - const state = globals.globalState.tryGet<{ [label: string]: string }>( + const state = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', Object, {} ) - assert.strictEqual(state[conn.id], profileFoo.arn) + assert.strictEqual(state[conn.id], profileFoo) // subject to test await sut.invalidateProfile(profileFoo.arn) // assertion assert.strictEqual(sut.activeRegionProfile, undefined) - const actualGlobalState = globals.globalState.tryGet<{ [label: string]: string }>( + const actualGlobalState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', Object, {} diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index 0a533e2342c..71f6338d82a 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -235,36 +235,37 @@ export class RegionProfileManager { return } // cross-validation - let profiles: RegionProfile[] = [] - try { - profiles = await this.listRegionProfile() - } catch (e) { - telemetry.amazonq_profileState.emit({ - source: 'reload', - amazonQProfileRegion: 'not-set', - reason: (e as Error).message, - result: 'Failed', - }) - - return - } + this.listRegionProfile() + .then(async (profiles) => { + const r = profiles.find((it) => it.arn === previousSelected.arn) + if (!r) { + telemetry.amazonq_profileState.emit({ + source: 'reload', + amazonQProfileRegion: 'not-set', + reason: 'profile could not be selected', + result: 'Failed', + }) - const r = profiles.find((it) => it.arn === previousSelected) - if (!r) { - telemetry.amazonq_profileState.emit({ - source: 'reload', - amazonQProfileRegion: 'not-set', - reason: 'profile could not be selected', - result: 'Failed', + await this.invalidateProfile(previousSelected.arn) + RegionProfileManager.logger.warn( + `invlaidating ${previousSelected.name} profile, arn=${previousSelected.arn}` + ) + } + }) + .catch((e) => { + telemetry.amazonq_profileState.emit({ + source: 'reload', + amazonQProfileRegion: 'not-set', + reason: (e as Error).message, + result: 'Failed', + }) }) - return - } - await this.switchRegionProfile(r, 'reload') + await this.switchRegionProfile(previousSelected, 'reload') } - private loadPersistedRegionProfle(): { [label: string]: string } { - const previousPersistedState = globals.globalState.tryGet<{ [label: string]: string }>( + private loadPersistedRegionProfle(): { [label: string]: RegionProfile } { + const previousPersistedState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', Object, {} @@ -282,13 +283,13 @@ export class RegionProfileManager { } // persist connectionId to profileArn - const previousPersistedState = globals.globalState.tryGet<{ [label: string]: string }>( + const previousPersistedState = globals.globalState.tryGet<{ [label: string]: RegionProfile }>( 'aws.amazonq.regionProfiles', Object, {} ) - previousPersistedState[conn.id] = this.activeRegionProfile.arn + previousPersistedState[conn.id] = this.activeRegionProfile await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState) } @@ -337,7 +338,7 @@ export class RegionProfileManager { const profiles = this.loadPersistedRegionProfle() const updatedProfiles = Object.fromEntries( - Object.entries(profiles).filter(([connId, profileArn]) => profileArn !== arn) + Object.entries(profiles).filter(([connId, profile]) => profile.arn !== arn) ) await globals.globalState.update('aws.amazonq.regionProfiles', updatedProfiles) } From 14c269abf580b7836c6b22298441f087cf18a154 Mon Sep 17 00:00:00 2001 From: Will Lo Date: Mon, 7 Apr 2025 12:10:58 -0700 Subject: [PATCH 23/49] retry ci From b283ffc23023a010cd24b93cb3b3f487f132d9d8 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:11:33 -0700 Subject: [PATCH 24/49] changelog --- .../Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json diff --git a/packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json b/packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json new file mode 100644 index 00000000000..8daaad0237b --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions" +} From 9c5ee2f5f80c33f8182228fc3634798e6b29c9ca Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:04:12 -0700 Subject: [PATCH 25/49] fix(amazonq): profile arn not passed causing ResourceNotFound #2123 ## Problem service will throw resourceNotFound if account doesn't match within the same code scan job, and here we miss profile arn in the `createUploadUrl` API --- packages/core/src/codewhisperer/commands/startSecurityScan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index d6c9d5441e4..d04fe6effc3 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -188,7 +188,7 @@ export async function startSecurityScan( const uploadStartTime = performance.now() const scanName = randomUUID() try { - artifactMap = await getPresignedUrlAndUpload(client, zipMetadata, scope, scanName) + artifactMap = await getPresignedUrlAndUpload(client, zipMetadata, scope, scanName, profile) } finally { await zipUtil.removeTmpFiles(zipMetadata, scope) codeScanTelemetryEntry.artifactsUploadDuration = performance.now() - uploadStartTime From 7a8d9aae78e5bb64844ec35ab4bd88cd143e8790 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 9 Apr 2025 15:28:22 +0000 Subject: [PATCH 26/49] Release 3.54.0 --- package-lock.json | 4 ++-- packages/toolkit/.changes/3.54.0.json | 5 +++++ packages/toolkit/CHANGELOG.md | 4 ++++ packages/toolkit/package.json | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 packages/toolkit/.changes/3.54.0.json diff --git a/package-lock.json b/package-lock.json index b18e9fcd9b4..567ca802302 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", @@ -28571,7 +28571,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.54.0-SNAPSHOT", + "version": "3.54.0", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/toolkit/.changes/3.54.0.json b/packages/toolkit/.changes/3.54.0.json new file mode 100644 index 00000000000..4fd39ef1882 --- /dev/null +++ b/packages/toolkit/.changes/3.54.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-04-09", + "version": "3.54.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index ef72984cca3..b89201f8d4f 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.54.0 2025-04-09 + +- Miscellaneous non-user-facing changes + ## 3.53.0 2025-04-03 - **Feature** Step Functions: Use WorkflowStudio to render StateMachine Graph in CDK applications diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index ac20f9e163a..c75cc854957 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.54.0-SNAPSHOT", + "version": "3.54.0", "extensionKind": [ "workspace" ], From f310286dbece434829ab39077cacc940cf4db771 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 9 Apr 2025 15:28:56 +0000 Subject: [PATCH 27/49] Release 1.55.0 --- package-lock.json | 4 +- packages/amazonq/.changes/1.55.0.json | 38 +++++++++++++++++++ ...-2daaf655-9a45-4104-94f9-06b9d8c703e2.json | 4 -- ...-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json | 4 -- ...-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json | 4 -- ...-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json | 4 -- ...-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json | 4 -- ...-80edcce3-4e32-4e74-a35a-af2242782181.json | 4 -- ...-846afd38-d618-4bd1-a3aa-7c74597502f1.json | 4 -- ...-dc96290e-5b35-4337-b0e3-bc08db97f660.json | 4 -- packages/amazonq/CHANGELOG.md | 11 ++++++ packages/amazonq/package.json | 2 +- 12 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 packages/amazonq/.changes/1.55.0.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-2daaf655-9a45-4104-94f9-06b9d8c703e2.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-80edcce3-4e32-4e74-a35a-af2242782181.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-846afd38-d618-4bd1-a3aa-7c74597502f1.json delete mode 100644 packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json diff --git a/package-lock.json b/package-lock.json index b18e9fcd9b4..629579f0abe 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.55.0-SNAPSHOT", + "version": "1.55.0", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/amazonq/.changes/1.55.0.json b/packages/amazonq/.changes/1.55.0.json new file mode 100644 index 00000000000..194814fc3be --- /dev/null +++ b/packages/amazonq/.changes/1.55.0.json @@ -0,0 +1,38 @@ +{ + "date": "2025-04-09", + "version": "1.55.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q Chat: Update chat history icon" + }, + { + "type": "Bug Fix", + "description": "Amazon Q Chat: chat occasionally freezes and displays gray screen" + }, + { + "type": "Bug Fix", + "description": "Amazon Q Chat: Set owner-only permissions for chat history and saved prompt files" + }, + { + "type": "Feature", + "description": "`/test` generates tests in all languages, not only Java/Python" + }, + { + "type": "Feature", + "description": "Amazon Q chat: Click export icon to save chat transcript in Markdown or HTML" + }, + { + "type": "Feature", + "description": "SageMaker: Disable the unsupported agentic commands and welcome prompt" + }, + { + "type": "Feature", + "description": "Amazon Q Chat: Add `@code` context for PHP, Ruby, Scala, Shell, and Swift projects" + }, + { + "type": "Feature", + "description": "Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Bug Fix-2daaf655-9a45-4104-94f9-06b9d8c703e2.json b/packages/amazonq/.changes/next-release/Bug Fix-2daaf655-9a45-4104-94f9-06b9d8c703e2.json deleted file mode 100644 index a1775ad8258..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-2daaf655-9a45-4104-94f9-06b9d8c703e2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q Chat: Update chat history icon" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json b/packages/amazonq/.changes/next-release/Bug Fix-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json deleted file mode 100644 index b39eceeec80..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-338b0a7b-81fd-460a-82f4-8e65e4a0614c.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q Chat: chat occasionally freezes and displays gray screen" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json b/packages/amazonq/.changes/next-release/Bug Fix-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json deleted file mode 100644 index a6a20c5bdf2..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-b6e474f1-b7ef-4016-8e1c-c9e7e6a45cc2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q Chat: Set owner-only permissions for chat history and saved prompt files" -} diff --git a/packages/amazonq/.changes/next-release/Feature-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json b/packages/amazonq/.changes/next-release/Feature-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json deleted file mode 100644 index f8286079401..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-2df6f9aa-0927-47ec-9e5e-d4c2ebe241c9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "`/test` generates tests in all languages, not only Java/Python" -} diff --git a/packages/amazonq/.changes/next-release/Feature-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json b/packages/amazonq/.changes/next-release/Feature-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json deleted file mode 100644 index 14c7484e001..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-4b7ab0af-2fe3-4e21-be78-5fcb1896257c.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Amazon Q chat: Click export icon to save chat transcript in Markdown or HTML" -} diff --git a/packages/amazonq/.changes/next-release/Feature-80edcce3-4e32-4e74-a35a-af2242782181.json b/packages/amazonq/.changes/next-release/Feature-80edcce3-4e32-4e74-a35a-af2242782181.json deleted file mode 100644 index cd6456594e4..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-80edcce3-4e32-4e74-a35a-af2242782181.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "SageMaker: Disable the unsupported agentic commands and welcome prompt" -} diff --git a/packages/amazonq/.changes/next-release/Feature-846afd38-d618-4bd1-a3aa-7c74597502f1.json b/packages/amazonq/.changes/next-release/Feature-846afd38-d618-4bd1-a3aa-7c74597502f1.json deleted file mode 100644 index 19fa59c69e9..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-846afd38-d618-4bd1-a3aa-7c74597502f1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Amazon Q Chat: Add `@code` context for PHP, Ruby, Scala, Shell, and Swift projects" -} diff --git a/packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json b/packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json deleted file mode 100644 index 8daaad0237b..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-dc96290e-5b35-4337-b0e3-bc08db97f660.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions" -} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 2d5fa9eb411..07127b72fd4 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.55.0 2025-04-09 + +- **Bug Fix** Amazon Q Chat: Update chat history icon +- **Bug Fix** Amazon Q Chat: chat occasionally freezes and displays gray screen +- **Bug Fix** Amazon Q Chat: Set owner-only permissions for chat history and saved prompt files +- **Feature** `/test` generates tests in all languages, not only Java/Python +- **Feature** Amazon Q chat: Click export icon to save chat transcript in Markdown or HTML +- **Feature** SageMaker: Disable the unsupported agentic commands and welcome prompt +- **Feature** Amazon Q Chat: Add `@code` context for PHP, Ruby, Scala, Shell, and Swift projects +- **Feature** Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions + ## 1.54.0 2025-04-03 - **Bug Fix** Amazon Q chat: `@prompts` not added to context diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index eab30300f64..801ba6127b1 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.55.0-SNAPSHOT", + "version": "1.55.0", "extensionKind": [ "workspace" ], From bec4f9e89dfe449f5f663f7da7dfc109de374538 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 9 Apr 2025 15:50:44 +0000 Subject: [PATCH 28/49] Update version to snapshot version: 3.55.0-SNAPSHOT --- package-lock.json | 4 ++-- packages/toolkit/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 567ca802302..2957b790409 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", @@ -28571,7 +28571,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.54.0", + "version": "3.55.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index c75cc854957..b84e3d2ee47 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.54.0", + "version": "3.55.0-SNAPSHOT", "extensionKind": [ "workspace" ], From 76977add99a0fe7b1c5f2ca8711b80ccae1b1d9b Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Wed, 9 Apr 2025 15:50:48 +0000 Subject: [PATCH 29/49] Update version to snapshot version: 1.56.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 629579f0abe..4fe86ba8452 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.55.0", + "version": "1.56.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 801ba6127b1..2587b750c41 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.55.0", + "version": "1.56.0-SNAPSHOT", "extensionKind": [ "workspace" ], From 8e00eefa33f4eee99eed162582c32c270e9e798e Mon Sep 17 00:00:00 2001 From: Na Yue Date: Wed, 9 Apr 2025 10:50:35 -0700 Subject: [PATCH 30/49] fix(chat): Use shell icon in shell command header (#6974) Remove static response string for shell command reject ## Problem - Use shell icon in shell command header - Remove static response string for shell command reject ## Solution ![image](https://github.com/user-attachments/assets/d67b87c5-b103-4b84-a9ba-e08ed457b1d4) --- - 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. --- .../src/amazonq/webview/ui/apps/cwChatConnector.ts | 4 ++-- .../controllers/chat/controller.ts | 3 --- .../controllers/chat/messenger/messenger.ts | 13 ++----------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 3486b1841a1..71946f06500 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -361,7 +361,7 @@ export class Connector extends BaseConnector { break case 'run-shell-command': answer.header = { - icon: 'code-block' as MynahIconsType, + icon: 'shell' as MynahIconsType, body: 'shell', status: { icon: 'ok' as MynahIconsType, @@ -372,7 +372,7 @@ export class Connector extends BaseConnector { break case 'reject-shell-command': answer.header = { - icon: 'code-block' as MynahIconsType, + icon: 'shell' as MynahIconsType, body: 'shell', status: { icon: 'cancel' as MynahIconsType, diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 1f75f33c5ed..7a0921724c7 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -812,10 +812,7 @@ export class ChatController { getLogger().error( `toolUse name: ${currentToolUse!.name} of toolUseWithError in the stored session doesn't match when click shell command reject button.` ) - return } - - await this.generateStaticTextResponse('reject-shell-command', triggerId) } private async processCustomFormAction(message: CustomFormActionMessage) { diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index ff079e422a6..c9894b04596 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -69,12 +69,7 @@ import { AsyncEventProgressMessage } from '../../../../amazonq/commons/connector import { localize } from '../../../../shared/utilities/vsCodeUtils' import { getDiffLinesFromChanges } from '../../../../shared/utilities/diffUtils' -export type StaticTextResponseType = - | 'quick-action-help' - | 'onboarding-help' - | 'transform' - | 'help' - | 'reject-shell-command' +export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' export type MessengerResponseType = { $metadata: { requestId?: string; httpStatusCode?: number } @@ -555,7 +550,7 @@ export class Messenger { }, ] header = { - icon: 'code-block' as MynahIconsType, + icon: 'shell' as MynahIconsType, body: 'shell', buttons, } @@ -676,10 +671,6 @@ export class Messenger { ] followUpsHeader = 'Try Examples:' break - case 'reject-shell-command': - // need to update the string later - message = 'The shell command execution rejected. Abort.' - break } this.dispatcher.sendChatMessage( From 01ff102c2531ad2943fd44078c35d5116713ef76 Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:17:15 -0700 Subject: [PATCH 31/49] fix(amazonq): confusing message during loading profiles --- packages/core/src/login/webview/vue/regionProfileSelector.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/login/webview/vue/regionProfileSelector.vue b/packages/core/src/login/webview/vue/regionProfileSelector.vue index 0726a01c3a1..d762f254192 100644 --- a/packages/core/src/login/webview/vue/regionProfileSelector.vue +++ b/packages/core/src/login/webview/vue/regionProfileSelector.vue @@ -46,8 +46,7 @@