Skip to content

Commit e3d0eea

Browse files
authored
Merge branch 'feature/q-lsp-chat' into rli/artifact-fallback
2 parents a0b974e + 535b8e2 commit e3d0eea

File tree

13 files changed

+344
-17
lines changed

13 files changed

+344
-17
lines changed

plugins/amazonq/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ val downloadFlareArtifacts by tasks.registering(Download::class) {
9797
val latest = manifest.map { it.versions.first() }
9898
val latestVersion = latest.map { it.serverVersion }
9999
val licensesUrl = latest.map { it.thirdPartyLicenses }
100-
val darwin = latest.map { it.targets.first { it.platform == "darwin" && it.arch == "arm64" } }
100+
val darwin = latest.map { it.targets.first { target -> target.platform == "darwin" && target.arch == "arm64" } }
101101
val contentUrls = darwin.map { it.contents.map { content -> content.url } }
102102

103103
val destination = layout.buildDirectory.dir(latestVersion.map { "flare/$it" })
@@ -115,8 +115,8 @@ val prepareBundledFlare by tasks.registering(Copy::class) {
115115

116116
val dest = layout.buildDirectory.dir("tmp/extractFlare")
117117
into(dest)
118+
from(downloadFlareArtifacts.map { it.outputFiles.filterNot { file -> file.name.endsWith(".zip") } })
118119

119-
from(downloadFlareArtifacts.map { it.outputFiles.filterNot { it.name.endsWith(".zip") } })
120120
doLast {
121121
copy {
122122
into(dest)

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcNotification
4141
import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcRequest
4242
import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
4343
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsServerCapabilitiesProvider
44+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatAsyncResultManager
4445
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager
4546
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage
4647
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.AUTH_FOLLOW_UP_CLICKED
@@ -106,6 +107,7 @@ class BrowserConnector(
106107
) {
107108
var uiReady = CompletableDeferred<Boolean>()
108109
private val chatCommunicationManager = ChatCommunicationManager.getInstance(project)
110+
private val chatAsyncResultManager = ChatAsyncResultManager.getInstance(project)
109111

110112
suspend fun connect(
111113
browser: Browser,
@@ -227,6 +229,7 @@ class BrowserConnector(
227229

228230
val tabId = requestFromUi.params.tabId
229231
val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId)
232+
chatCommunicationManager.registerPartialResultToken(partialResultToken)
230233

231234
var encryptionManager: JwtEncryptionManager? = null
232235
val result = AmazonQLspService.executeIfRunning(project) { server ->
@@ -247,6 +250,7 @@ class BrowserConnector(
247250
val tabId = requestFromUi.params.tabId
248251
val quickActionParams = node.params ?: error("empty payload")
249252
val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId)
253+
chatCommunicationManager.registerPartialResultToken(partialResultToken)
250254
var encryptionManager: JwtEncryptionManager? = null
251255
val result = AmazonQLspService.executeIfRunning(project) { server ->
252256
encryptionManager = this.encryptionManager
@@ -476,15 +480,32 @@ class BrowserConnector(
476480
)
477481
browser.postChat(messageToChat)
478482
chatCommunicationManager.removeInflightRequestForTab(tabId)
479-
} catch (_: CancellationException) {
483+
} catch (e: CancellationException) {
480484
LOG.warn { "Cancelled chat generation" }
485+
try {
486+
chatAsyncResultManager.createRequestId(partialResultToken)
487+
chatAsyncResultManager.getResult(partialResultToken)
488+
handleCancellation(tabId, browser)
489+
} catch (ex: Exception) {
490+
LOG.warn(ex) { "An error occurred while processing cancellation" }
491+
} finally {
492+
chatAsyncResultManager.removeRequestId(partialResultToken)
493+
chatCommunicationManager.removePartialResultLock(partialResultToken)
494+
chatCommunicationManager.removeFinalResultProcessed(partialResultToken)
495+
}
481496
} catch (e: Exception) {
482497
LOG.warn(e) { "Failed to send chat message" }
483498
browser.postChat(chatCommunicationManager.getErrorUiMessage(tabId, e, partialResultToken))
484499
}
485500
}
486501
}
487502

503+
private fun handleCancellation(tabId: String, browser: Browser) {
504+
// Send a message to hide the stop button without showing an error
505+
val cancelMessage = chatCommunicationManager.getCancellationUiMessage(tabId)
506+
browser.postChat(cancelMessage)
507+
}
508+
488509
private fun cancelInflightRequests(tabId: String) {
489510
chatCommunicationManager.getInflightRequestForTab(tabId)?.let { request ->
490511
request.cancel(true)

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ data class AmazonQTheme(
1616
val defaultText: Color,
1717
val inactiveText: Color,
1818
val linkText: Color,
19+
val lightText: Color,
20+
val emptyText: Color,
1921

2022
val background: Color,
2123
val border: Color,
@@ -31,6 +33,8 @@ data class AmazonQTheme(
3133
val buttonBackground: Color,
3234
val secondaryButtonForeground: Color,
3335
val secondaryButtonBackground: Color,
36+
val inputBorderFocused: Color,
37+
val inputBorderUnfocused: Color,
3438

3539
val info: Color,
3640
val success: Color,

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ enum class CssVariable(
1616
TextColorAlt("--mynah-color-text-alternate"),
1717
TextColorStrong("--mynah-color-text-strong"),
1818
TextColorWeak("--mynah-color-text-weak"),
19+
TextColorLight("--mynah-color-light"),
1920
TextColorLink("--mynah-color-text-link"),
2021
TextColorInput("--mynah-color-text-input"),
2122
TextColorDisabled("--mynah-color-text-disabled"),
@@ -27,6 +28,8 @@ enum class CssVariable(
2728
ColorDeep("--mynah-color-deep"),
2829
ColorDeepReverse("--mynah-color-deep-reverse"),
2930
BorderDefault("--mynah-color-border-default"),
31+
BorderFocused("--mynah-color-text-input-border-focused"),
32+
BorderUnfocused("--mynah-color-text-input-border"),
3033
InputBackground("--mynah-input-bg"),
3134

3235
SyntaxBackground("--mynah-color-syntax-bg"),

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ class EditorThemeAdapter {
124124
editorString = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.STRING),
125125
editorProperty = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.INSTANCE_FIELD),
126126
editorClassName = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.CLASS_NAME),
127+
lightText = themeColor("TextField.inactiveForeground", default = 0xA8ADBD, darkDefault = 0x5A5D63),
128+
emptyText = themeColor("TextField.inactiveForeground", default = 0xA8ADBD, darkDefault = 0x5A5D63),
129+
inputBorderFocused = themeColor("ActionButton.focusedBorderColor", default = 0x4682FA, darkDefault = 0x3574f0),
130+
inputBorderUnfocused = themeColor("TextField.borderColor", default = 0xEBECF0, darkDefault = 0x4E5157),
127131
)
128132
}
129133

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class ThemeBrowserAdapter {
2626
}
2727

2828
private fun buildJsCodeToUpdateTheme(theme: AmazonQTheme) = buildString {
29+
val (bg, altBg, inputBg) = determineInputAndBgColor(theme)
2930
appendDarkMode(theme.darkMode)
3031

3132
append("{\n")
@@ -39,17 +40,20 @@ class ThemeBrowserAdapter {
3940
append(CssVariable.TextColorStrong, theme.textFieldForeground)
4041
append(CssVariable.TextColorInput, theme.textFieldForeground)
4142
append(CssVariable.TextColorLink, theme.linkText)
42-
append(CssVariable.TextColorWeak, theme.inactiveText)
43+
append(CssVariable.TextColorWeak, theme.emptyText)
44+
append(CssVariable.TextColorLight, theme.emptyText)
4345
append(CssVariable.TextColorDisabled, theme.inactiveText)
4446

45-
append(CssVariable.Background, theme.editorBackground)
46-
append(CssVariable.BackgroundAlt, theme.background)
47-
append(CssVariable.CardBackground, theme.editorBackground)
48-
append(CssVariable.CardBackgroundAlt, theme.background)
47+
append(CssVariable.Background, bg)
48+
append(CssVariable.BackgroundAlt, altBg)
49+
append(CssVariable.CardBackground, bg)
50+
append(CssVariable.CardBackgroundAlt, altBg)
4951
append(CssVariable.BorderDefault, theme.border)
52+
append(CssVariable.BorderFocused, theme.inputBorderFocused)
53+
append(CssVariable.BorderUnfocused, theme.inputBorderUnfocused)
5054
append(CssVariable.TabActive, theme.activeTab)
5155

52-
append(CssVariable.InputBackground, theme.textFieldBackground)
56+
append(CssVariable.InputBackground, inputBg)
5357

5458
append(CssVariable.ButtonBackground, theme.buttonBackground)
5559
append(CssVariable.ButtonForeground, theme.buttonForeground)
@@ -110,4 +114,15 @@ class ThemeBrowserAdapter {
110114

111115
// Some font names have characters that require them to be wrapped in quotes in the CSS variable, for example if they have spaces or a period.
112116
private fun Font.toCssFontFamily(fallback: String = "system-ui") = "\"$family\", $fallback"
117+
118+
// darkest = bg, second darkest is alt bg, lightest is input bg
119+
private fun determineInputAndBgColor(theme: AmazonQTheme): Triple<Color, Color, Color> {
120+
val colors = arrayOf(theme.editorBackground, theme.background, theme.textFieldBackground).sortedWith(
121+
Comparator.comparing {
122+
// luma calculation for brightness
123+
(0.2126 * it.red) + (0.7152 * it.green) + (0.0722 * it.blue)
124+
}
125+
)
126+
return Triple(colors[0], colors[1], colors[2])
127+
}
113128
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,21 @@ class CodeWhispererConfigurable(private val project: Project) :
8383
.resizableColumn()
8484
.align(Align.FILL)
8585
}
86+
row(message("amazonqFeatureDev.placeholder.node_runtime_path")) {
87+
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor()
88+
fileChooserDescriptor.isForcedToUseIdeaFileChooser = true
89+
90+
textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor)
91+
.bindText(
92+
{ LspSettings.getInstance().getNodeRuntimePath().orEmpty() },
93+
{ LspSettings.getInstance().setNodeRuntimePath(it) }
94+
)
95+
.applyToComponent {
96+
emptyText.text = message("executableCommon.auto_managed")
97+
}
98+
.resizableColumn()
99+
.align(Align.FILL)
100+
}
86101
}
87102

88103
group(message("aws.settings.codewhisperer.group.general")) {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp
55

66
import com.google.gson.ToNumberPolicy
77
import com.intellij.execution.configurations.GeneralCommandLine
8+
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
89
import com.intellij.execution.impl.ExecutionManagerImpl
910
import com.intellij.execution.process.KillableColoredProcessHandler
1011
import com.intellij.execution.process.KillableProcessHandler
1112
import com.intellij.execution.process.ProcessEvent
1213
import com.intellij.execution.process.ProcessListener
1314
import com.intellij.execution.process.ProcessOutputType
15+
import com.intellij.notification.NotificationAction
1416
import com.intellij.openapi.Disposable
1517
import com.intellij.openapi.components.Service
1618
import com.intellij.openapi.components.service
1719
import com.intellij.openapi.components.serviceIfCreated
20+
import com.intellij.openapi.options.ShowSettingsUtil
1821
import com.intellij.openapi.project.Project
1922
import com.intellij.openapi.util.Disposer
2023
import com.intellij.openapi.util.Key
@@ -76,6 +79,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceS
7679
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
7780
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
7881
import software.aws.toolkits.jetbrains.settings.LspSettings
82+
import software.aws.toolkits.jetbrains.utils.notifyInfo
83+
import software.aws.toolkits.resources.message
7984
import java.io.IOException
8085
import java.io.OutputStreamWriter
8186
import java.io.PipedInputStream
@@ -86,7 +91,9 @@ import java.net.Proxy
8691
import java.net.URI
8792
import java.nio.charset.StandardCharsets
8893
import java.nio.file.Files
94+
import java.nio.file.Path
8995
import java.util.concurrent.Future
96+
import java.util.concurrent.TimeUnit
9097
import kotlin.time.Duration.Companion.seconds
9198

9299
// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java
@@ -372,7 +379,9 @@ private class AmazonQServerInstance(private val project: Project, private val cs
372379
}
373380

374381
val node = if (SystemInfo.isWindows) "node.exe" else "node"
375-
val cmd = NodeExePatcher.patch(artifact.resolve(node))
382+
var nodePath = getNodeRuntimePath(artifact.resolve(node))
383+
384+
val cmd = NodeExePatcher.patch(nodePath)
376385
.withParameters(
377386
LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
378387
"--stdio",
@@ -492,6 +501,103 @@ private class AmazonQServerInstance(private val project: Project, private val cs
492501
}
493502
}
494503

504+
/**
505+
* Resolves the path to a valid Node.js runtime in the following order of preference:
506+
* 1. Uses the provided nodePath if it exists and is executable
507+
* 2. Uses user-specified runtime path from LSP settings if available
508+
* 3. Uses system Node.js if version 18+ is available
509+
* 4. Falls back to original nodePath with a notification to configure runtime
510+
*
511+
* @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
512+
* @return Path The resolved Node.js runtime path to use for the LSP server
513+
*
514+
* Side effects:
515+
* - Logs warnings if initial runtime path is invalid
516+
* - Logs info when using alternative runtime path
517+
* - Shows notification to user if no valid Node.js runtime is found
518+
*
519+
* Note: The function will return a path even if no valid runtime is found, but the LSP server
520+
* may fail to start in that case. The caller should handle potential runtime initialization failures.
521+
*/
522+
private fun getNodeRuntimePath(nodePath: Path): Path {
523+
if (Files.exists(nodePath) && Files.isExecutable(nodePath)) {
524+
return nodePath
525+
}
526+
// use alternative node runtime if it is not found
527+
LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " }
528+
// attempt to use user provided node runtime path
529+
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
530+
if (!nodeRuntime.isNullOrEmpty()) {
531+
LOG.info { "Using node from $nodeRuntime " }
532+
return Path.of(nodeRuntime)
533+
} else {
534+
val localNode = locateNodeCommand()
535+
if (localNode != null) {
536+
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }
537+
return localNode
538+
}
539+
notifyInfo(
540+
"Amazon Q",
541+
message("amazonqFeatureDev.placeholder.node_runtime_message"),
542+
project = project,
543+
listOf(
544+
NotificationAction.create(
545+
message("codewhisperer.actions.open_settings.title")
546+
) { _, notification ->
547+
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
548+
},
549+
NotificationAction.create(
550+
message("codewhisperer.notification.custom.simple.button.got_it")
551+
) { _, notification -> notification.expire() }
552+
)
553+
)
554+
return nodePath
555+
}
556+
}
557+
558+
/**
559+
* Locates node executable ≥18 in system PATH.
560+
* Uses IntelliJ's PathEnvironmentVariableUtil to find executables.
561+
*
562+
* @return Path? The absolute path to node ≥18 if found, null otherwise
563+
*/
564+
private fun locateNodeCommand(): Path? {
565+
val exeName = if (SystemInfo.isWindows) "node.exe" else "node"
566+
567+
return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName)
568+
.asSequence()
569+
.map { it.toPath() }
570+
.filter { Files.isRegularFile(it) && Files.isExecutable(it) }
571+
.firstNotNullOfOrNull { path ->
572+
try {
573+
val process = ProcessBuilder(path.toString(), "--version")
574+
.redirectErrorStream(true)
575+
.start()
576+
577+
if (!process.waitFor(5, TimeUnit.SECONDS)) {
578+
process.destroy()
579+
null
580+
} else if (process.exitValue() == 0) {
581+
val version = process.inputStream.bufferedReader().readText().trim()
582+
val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull()
583+
584+
if (majorVersion != null && majorVersion >= 18) {
585+
path.toAbsolutePath()
586+
} else {
587+
LOG.debug { "Node version < 18 found at: $path (version: $version)" }
588+
null
589+
}
590+
} else {
591+
LOG.debug { "Failed to get version from node at: $path" }
592+
null
593+
}
594+
} catch (e: Exception) {
595+
LOG.debug(e) { "Failed to check version for node at: $path" }
596+
null
597+
}
598+
}
599+
}
600+
495601
override fun dispose() {
496602
if (!launcherFuture.isDone) {
497603
try {

0 commit comments

Comments
 (0)