diff --git a/.changes/3.67.json b/.changes/3.67.json new file mode 100644 index 00000000000..ab39cadb25c --- /dev/null +++ b/.changes/3.67.json @@ -0,0 +1,20 @@ +{ + "date" : "2025-04-18", + "version" : "3.67", + "entries" : [ { + "type" : "bugfix", + "description" : "Amazon Q: Customization now resets with a warning if unavailable in the selected profile." + }, { + "type" : "bugfix", + "description" : "Q panel will get stuck while signin if users have multiple windows" + }, { + "type" : "bugfix", + "description" : "Fix integer overflow when local context index input is larger than 2GB" + }, { + "type" : "bugfix", + "description" : "Fix workspace index process quits when hitting a race condition" + }, { + "type" : "bugfix", + "description" : "Fix infinite loop when workspace indexing server fails to initialize" + } ] +} \ No newline at end of file diff --git a/.changes/next-release/bugfix-a70cb565-5de8-4302-9f1e-71925f78061b.json b/.changes/next-release/bugfix-a70cb565-5de8-4302-9f1e-71925f78061b.json deleted file mode 100644 index 7c369ea5be3..00000000000 --- a/.changes/next-release/bugfix-a70cb565-5de8-4302-9f1e-71925f78061b.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type" : "bugfix", - "description" : "Amazon Q: Customization now resets with a warning if unavailable in the selected profile." -} \ No newline at end of file diff --git a/.changes/next-release/bugfix-afb46d2b-24c7-48be-94b1-02a7f56cbde0.json b/.changes/next-release/bugfix-afb46d2b-24c7-48be-94b1-02a7f56cbde0.json deleted file mode 100644 index 090ccd90650..00000000000 --- a/.changes/next-release/bugfix-afb46d2b-24c7-48be-94b1-02a7f56cbde0.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type" : "bugfix", - "description" : "Q panel will get stuck while signin if users have multiple windows" -} \ No newline at end of file diff --git a/.changes/next-release/bugfix-f31946b6-3709-44a5-b23f-9fa202bd5cb5.json b/.changes/next-release/bugfix-f31946b6-3709-44a5-b23f-9fa202bd5cb5.json deleted file mode 100644 index f537fa0c825..00000000000 --- a/.changes/next-release/bugfix-f31946b6-3709-44a5-b23f-9fa202bd5cb5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type" : "bugfix", - "description" : "Fix infinite loop when workspace indexing server fails to initialize" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0dea89084..51d13d90b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# _3.67_ (2025-04-18) +- **(Bug Fix)** Amazon Q: Customization now resets with a warning if unavailable in the selected profile. +- **(Bug Fix)** Q panel will get stuck while signin if users have multiple windows +- **(Bug Fix)** Fix integer overflow when local context index input is larger than 2GB +- **(Bug Fix)** Fix workspace index process quits when hitting a race condition +- **(Bug Fix)** Fix infinite loop when workspace indexing server fails to initialize + # _3.66_ (2025-04-11) - **(Feature)** The logs emitted by the Agent during user command execution will be accepted and written to `.amazonq/dev/run_command.log` file in the user's local repository. - **(Bug Fix)** Unit test generation now completes successfully when using the `/test` command diff --git a/gradle.properties b/gradle.properties index 759ae79561e..f172e4d6a6f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Toolkit Version -toolkitVersion=3.67-SNAPSHOT +toolkitVersion=3.68-SNAPSHOT # Publish Settings publishToken= diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt index 6b9d6207cb7..77f901950c7 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt @@ -169,7 +169,7 @@ class CodeWhispererConfigurable(private val project: Project) : row(message("aws.settings.codewhisperer.project_context_index_thread")) { intTextField( - range = IntRange(0, 50) + range = CodeWhispererSettings.CONTEXT_INDEX_THREADS ).bindIntText(codeWhispererSettings::getProjectContextIndexThreadCount, codeWhispererSettings::setProjectContextIndexThreadCount) .apply { connect.subscribe( @@ -186,7 +186,7 @@ class CodeWhispererConfigurable(private val project: Project) : row(message("aws.settings.codewhisperer.project_context_index_max_size")) { intTextField( - range = IntRange(1, 4096) + range = CodeWhispererSettings.CONTEXT_INDEX_SIZE ).bindIntText(codeWhispererSettings::getProjectContextIndexMaxSize, codeWhispererSettings::setProjectContextIndexMaxSize) .apply { connect.subscribe( diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt index 982266f07bd..84263c62ad2 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt @@ -220,6 +220,41 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { assertThat(actual.autoBuildSetting["project1"]).isTrue() } + @Test + fun `context thread count is returned in range`() { + val sut = CodeWhispererSettings.getInstance() + + mapOf( + 1 to 1, + 0 to 0, + -1 to 0, + 123 to 50, + 50 to 50, + 51 to 50, + ).forEach { s, expected -> + sut.setProjectContextIndexThreadCount(s) + assertThat(sut.getProjectContextIndexThreadCount()).isEqualTo(expected) + } + } + + @Test + fun `context index size is returned in range`() { + val sut = CodeWhispererSettings.getInstance() + + mapOf( + 1 to 1, + 0 to 1, + -1 to 1, + 123 to 123, + 2047 to 2047, + 4096 to 4096, + 4097 to 4096, + ).forEach { s, expected -> + sut.setProjectContextIndexMaxSize(s) + assertThat(sut.getProjectContextIndexMaxSize()).isEqualTo(expected) + } + } + @Test fun `toggleMetricOptIn should trigger LSP didChangeConfiguration`() { mockkObject(AmazonQLspService) diff --git a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts index 8139a8c4532..315356836b5 100644 --- a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts +++ b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts @@ -22,10 +22,5 @@ dependencies { implementation(libs.commons.collections) implementation(libs.nimbus.jose.jwt) - // FIX_WHEN_MIN_IS_242 - if (providers.gradleProperty("ideProfileName").get() == "2024.1") { - implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.24.0") - } - testFixturesApi(testFixtures(project(":plugin-core:jetbrains-community"))) } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt index c7eefe33284..c699021315f 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt @@ -9,6 +9,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest import org.eclipse.lsp4j.services.LanguageServer import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.GetConfigurationFromServerParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.UpdateConfigurationParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams @@ -31,6 +32,9 @@ interface AmazonQLanguageServer : LanguageServer { @JsonRequest("aws/getConfigurationFromServer") fun getConfigurationFromServer(params: GetConfigurationFromServerParams): CompletableFuture + @JsonRequest("aws/updateConfiguration") + fun updateConfiguration(params: UpdateConfigurationParams): CompletableFuture + @JsonRequest("aws/chat/sendChatPrompt") fun sendChatPrompt(params: EncryptedChatParams): CompletableFuture } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt index c5be0abe56c..d3a99a1f4fe 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt @@ -15,9 +15,14 @@ import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenPr import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.UpdateConfigurationParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.BearerCredentials import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayloadData +import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile +import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager +import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener import software.aws.toolkits.jetbrains.utils.isQConnected import software.aws.toolkits.jetbrains.utils.isQExpired import java.util.concurrent.CompletableFuture @@ -28,16 +33,21 @@ class DefaultAuthCredentialsService( serverInstance: Disposable, ) : AuthCredentialsService, BearerTokenProviderListener, - ToolkitConnectionManagerListener { + ToolkitConnectionManagerListener, + QRegionProfileSelectedListener { init { project.messageBus.connect(serverInstance).apply { subscribe(BearerTokenProviderListener.TOPIC, this@DefaultAuthCredentialsService) subscribe(ToolkitConnectionManagerListener.TOPIC, this@DefaultAuthCredentialsService) + subscribe(QRegionProfileSelectedListener.TOPIC, this@DefaultAuthCredentialsService) } if (isQConnected(project) && !isQExpired(project)) { updateTokenFromActiveConnection() + .thenRun { + updateConfiguration() + } } } @@ -70,15 +80,15 @@ class DefaultAuthCredentialsService( updateTokenFromConnection(newConnection) } - private fun updateTokenFromActiveConnection() { + private fun updateTokenFromActiveConnection(): CompletableFuture { val connection = ToolkitConnectionManager.getInstance(project) .activeConnectionForFeature(QConnection.getInstance()) - ?: return + ?: return CompletableFuture.failedFuture(IllegalStateException("No active Q connection")) - updateTokenFromConnection(connection) + return updateTokenFromConnection(connection) } - private fun updateTokenFromConnection(connection: ToolkitConnection) { + private fun updateTokenFromConnection(connection: ToolkitConnection): CompletableFuture = (connection.getConnectionSettings() as? TokenConnectionSettings) ?.tokenProvider ?.delegate @@ -86,7 +96,7 @@ class DefaultAuthCredentialsService( ?.currentToken() ?.accessToken ?.let { token -> updateTokenCredentials(token, true) } - } + ?: CompletableFuture.failedFuture(IllegalStateException("Unable to get token from connection")) override fun invalidate(providerId: String) { deleteTokenCredentials() @@ -108,4 +118,20 @@ class DefaultAuthCredentialsService( encrypted = false ) } + + override fun onProfileSelected(project: Project, profile: QRegionProfile?) { + updateConfiguration() + } + + private fun updateConfiguration(): CompletableFuture { + val payload = UpdateConfigurationParams( + section = "aws.q", + settings = mapOf( + "profileArn" to QRegionProfileManager.getInstance().activeProfile(project)?.arn + ) + ) + return AmazonQLspService.executeIfRunning(project) { server -> + server.updateConfiguration(payload) + } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))) + } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LspServerConfigurations.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LspServerConfigurations.kt index e762b938d28..a0f23875b62 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LspServerConfigurations.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LspServerConfigurations.kt @@ -8,3 +8,10 @@ data class WorkspaceInfo(val workspaceRoot: String, val workspaceId: String) // This represents the entire array data class LspServerConfigurations(val workspaces: List) + +data class UpdateConfigurationParams( + val section: String, + val settings: LSPAny, +) + +typealias LSPAny = Any? diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt index 75ab0df4f46..854124a180f 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt @@ -70,7 +70,7 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En data class FileCollectionResult( val files: List, - val fileSize: Int, + val fileSize: Int, // in MB ) // TODO: move to LspMessage.kt @@ -246,59 +246,7 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En } } - private fun willExceedPayloadLimit(currentTotalFileSize: Long, currentFileSize: Long): Boolean { - val maxSize = CodeWhispererSettings.getInstance().getProjectContextIndexMaxSize() - return currentTotalFileSize.let { totalSize -> totalSize > (maxSize * 1024 * 1024 - currentFileSize) } - } - - private fun isBuildOrBin(fileName: String): Boolean { - val regex = Regex("""bin|build|node_modules|venv|\.venv|env|\.idea|\.conda""", RegexOption.IGNORE_CASE) - return regex.find(fileName) != null - } - - fun collectFiles(): FileCollectionResult { - val collectedFiles = mutableListOf() - var currentTotalFileSize = 0L - val allFiles = mutableListOf() - - val projectBaseDirectories = project.getBaseDirectories() - val changeListManager = ChangeListManager.getInstance(project) - - projectBaseDirectories.forEach { - VfsUtilCore.visitChildrenRecursively( - it, - object : VirtualFileVisitor(NO_FOLLOW_SYMLINKS) { - // TODO: refactor this along with /dev & codescan file traversing logic - override fun visitFile(file: VirtualFile): Boolean { - if ((file.isDirectory && isBuildOrBin(file.name)) || - !isWorkspaceSourceContent(file, projectBaseDirectories, changeListManager, additionalGlobalIgnoreRulesForStrictSources) || - (file.isFile && file.length > 10 * 1024 * 1024) - ) { - return false - } - if (file.isFile) { - allFiles.add(file) - return false - } - return true - } - } - ) - } - - for (file in allFiles) { - if (willExceedPayloadLimit(currentTotalFileSize, file.length)) { - break - } - collectedFiles.add(file.path) - currentTotalFileSize += file.length - } - - return FileCollectionResult( - files = collectedFiles.toList(), - fileSize = (currentTotalFileSize / 1024 / 1024).toInt() - ) - } + fun collectFiles(): FileCollectionResult = collectFiles(project.getBaseDirectories(), ChangeListManager.getInstance(project)) private fun queryResultToRelevantDocuments(queryResult: List): List { val documents: MutableList = mutableListOf() @@ -358,5 +306,57 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En companion object { private val logger = getLogger() + private val regex = Regex("""bin|build|node_modules|venv|\.venv|env|\.idea|\.conda""", RegexOption.IGNORE_CASE) + private val mega = (1024 * 1024).toULong() + private val tenMb = 10 * mega.toInt() + + private fun willExceedPayloadLimit(maxSize: ULong, currentTotalFileSize: ULong, currentFileSize: Long) = + currentTotalFileSize.let { totalSize -> totalSize > (maxSize - currentFileSize.toUInt()) } + + private fun isBuildOrBin(fileName: String): Boolean = + regex.find(fileName) != null + + fun collectFiles(projectBaseDirectories: Set, changeListManager: ChangeListManager): FileCollectionResult { + val maxSize = CodeWhispererSettings.getInstance() + .getProjectContextIndexMaxSize().toULong() * mega + val collectedFiles = mutableListOf() + var currentTotalFileSize = 0UL + val allFiles = mutableListOf() + + projectBaseDirectories.forEach { + VfsUtilCore.visitChildrenRecursively( + it, + object : VirtualFileVisitor(NO_FOLLOW_SYMLINKS) { + // TODO: refactor this along with /dev & codescan file traversing logic + override fun visitFile(file: VirtualFile): Boolean { + if ((file.isDirectory && isBuildOrBin(file.name)) || + !isWorkspaceSourceContent(file, projectBaseDirectories, changeListManager, additionalGlobalIgnoreRulesForStrictSources) || + (file.isFile && file.length > tenMb) + ) { + return false + } + if (file.isFile) { + allFiles.add(file) + return false + } + return true + } + } + ) + } + + for (file in allFiles) { + if (willExceedPayloadLimit(maxSize, currentTotalFileSize, file.length)) { + break + } + collectedFiles.add(file.path) + currentTotalFileSize += file.length.toUInt() + } + + return FileCollectionResult( + files = collectedFiles.toList(), + fileSize = (currentTotalFileSize / 1024u / 1024u).toInt() + ) + } } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt index dca4d460816..484c319db52 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt @@ -15,7 +15,7 @@ import software.aws.toolkits.jetbrains.core.getTextFromUrl class ManifestManager { private val cloudFrontUrl = "https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json" - val currentVersion = "0.1.46" + val currentVersion = "0.1.49" val currentOs = getOs() private val arch = CpuArch.CURRENT private val mapper = jacksonObjectMapper().apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt index 7f9e443c14f..2c5cac62e57 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt @@ -119,7 +119,7 @@ class CodeWhispererSettings : PersistentStateComponent