Skip to content

Commit ec50363

Browse files
committed
Merge remote-tracking branch 'origin/feature/q-lsp-chat' into HEAD
2 parents 5ae53fb + f6c58f5 commit ec50363

File tree

17 files changed

+173
-56
lines changed

17 files changed

+173
-56
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import com.intellij.openapi.project.DumbAwareAction
1111
import com.intellij.util.messages.Topic
1212
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow
1313
import software.aws.toolkits.resources.AmazonQBundle
14-
import software.aws.toolkits.resources.message
1514
import java.util.EventListener
1615

1716
class QRefreshPanelAction : DumbAwareAction(AmazonQBundle.message("amazonq.refresh.panel"), null, AllIcons.Actions.Refresh) {

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ class AmazonQToolWindow private constructor(
2424

2525
val component
2626
get() = chatPanel.component
27-
2827
fun disposeAndRecreate() {
2928
Disposer.dispose(chatPanel)
3029
chatPanel = AmazonQPanel(project, scope)

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
6666
ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.let { qConn ->
6767
openMeetQPage(project)
6868
}
69-
prepareChatContent(project, qPanel)
69+
preparePanelContent(project, qPanel)
7070
}
7171
}
7272
)
@@ -75,7 +75,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
7575
RefreshQChatPanelButtonPressedListener.TOPIC,
7676
object : RefreshQChatPanelButtonPressedListener {
7777
override fun onRefresh() {
78-
prepareChatContent(project, qPanel)
78+
preparePanelContent(project, qPanel)
7979
}
8080
}
8181
)
@@ -85,8 +85,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
8585
object : BearerTokenProviderListener {
8686
override fun onChange(providerId: String, newScopes: List<String>?) {
8787
if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) {
88-
AmazonQToolWindow.getInstance(project).disposeAndRecreate()
89-
prepareChatContent(project, qPanel)
88+
preparePanelContent(project, qPanel)
9089
}
9190
}
9291
}
@@ -98,13 +97,12 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
9897
// note we name myProject intentionally ow it will shadow the "project" provided by the IDE
9998
override fun onProfileSelected(myProject: Project, profile: QRegionProfile?) {
10099
if (project.isDisposed) return
101-
AmazonQToolWindow.getInstance(project).disposeAndRecreate()
102-
prepareChatContent(project, qPanel)
100+
preparePanelContent(project, qPanel)
103101
}
104102
}
105103
)
106104

107-
prepareChatContent(project, qPanel)
105+
preparePanelContent(project, qPanel)
108106

109107
val content = contentManager.factory.createContent(mainPanel, null, false).also {
110108
it.isCloseable = true
@@ -114,7 +112,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
114112
contentManager.addContent(content)
115113
}
116114

117-
private fun prepareChatContent(
115+
private fun preparePanelContent(
118116
project: Project,
119117
qPanel: Wrapper,
120118
) {

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import kotlinx.coroutines.flow.asSharedFlow
1111
import kotlinx.coroutines.runBlocking
1212
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener
1313
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage
14+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GENERIC_COMMAND
1415
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GenericCommandParams
16+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_TO_PROMPT
1517
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendToPromptParams
1618
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TriggerType
1719
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
@@ -36,23 +38,19 @@ class ActionRegistrar {
3638
val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ContextMenu)
3739
val codeSelection = "\n```\n${fileContext.focusAreaContext?.codeSelection?.trimIndent()?.trim()}\n```\n"
3840
var uiMessage: FlareUiMessage? = null
39-
if (command.verb != "sendToPrompt") {
41+
if (command.verb != SEND_TO_PROMPT) {
4042
val params = GenericCommandParams(selection = codeSelection, triggerType = TriggerType.CONTEXT_MENU, genericCommand = command.name)
41-
uiMessage = FlareUiMessage(command = "genericCommand", params = params)
43+
uiMessage = FlareUiMessage(command = GENERIC_COMMAND, params = params)
4244
} else {
4345
val params = SendToPromptParams(selection = codeSelection, triggerType = TriggerType.CONTEXT_MENU)
44-
uiMessage = FlareUiMessage(command = "sendToPrompt", params = params)
46+
uiMessage = FlareUiMessage(command = SEND_TO_PROMPT, params = params)
4547
}
4648
AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage)
4749
}
4850
}
4951
}
5052
}
5153

52-
fun reportMessageClick(command: EditorContextCommand, issue: MutableMap<String, String>, project: Project) {
53-
_messages.tryEmit(CodeScanIssueActionMessage(command, issue, project))
54-
}
55-
5654
// provide singleton access
5755
companion object {
5856
val instance = ActionRegistrar()

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/CodeScanQActions.kt

Lines changed: 0 additions & 25 deletions
This file was deleted.

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,55 @@
33

44
package software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions
55

6-
import software.aws.toolkits.jetbrains.services.cwc.commands.EditorContextCommand
6+
import com.intellij.openapi.actionSystem.ActionManager
7+
import com.intellij.openapi.actionSystem.AnAction
8+
import com.intellij.openapi.actionSystem.AnActionEvent
9+
import com.intellij.openapi.actionSystem.DataKey
10+
import com.intellij.openapi.application.ApplicationManager
11+
import com.intellij.openapi.project.DumbAware
12+
import kotlinx.coroutines.runBlocking
13+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener
14+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage
15+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatPrompt
16+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_TO_PROMPT
17+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendToPromptParams
18+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TriggerType
719

8-
class ExplainCodeIssueAction : CodeScanQActions(EditorContextCommand.ExplainCodeScanIssue)
20+
class ExplainCodeIssueAction : AnAction(), DumbAware {
21+
override fun actionPerformed(e: AnActionEvent) {
22+
val issueDataKey = DataKey.create<MutableMap<String, String>>("amazonq.codescan.explainissue")
23+
val issueContext = e.getData(issueDataKey) ?: return
24+
25+
ActionManager.getInstance().getAction("q.openchat").actionPerformed(e)
26+
27+
ApplicationManager.getApplication().executeOnPooledThread {
28+
runBlocking {
29+
// https://github.com/aws/aws-toolkit-vscode/blob/master/packages/amazonq/src/lsp/chat/commands.ts#L30
30+
val codeSelection = "\n```\n${issueContext["code"]?.trimIndent()?.trim()}\n```\n"
31+
32+
val prompt = "Explain the issue \n\n " +
33+
"Issue: \"${issueContext["title"]}\" \n" +
34+
"Code: $codeSelection"
35+
36+
val modelPrompt = "Explain the issue ${issueContext["title"]} \n\n " +
37+
"Issue: \"${issueContext["title"]}\" \n" +
38+
"Description: ${issueContext["description"]} \n" +
39+
"Code: $codeSelection and generate the code demonstrating the fix"
40+
41+
val params = SendToPromptParams(
42+
selection = codeSelection,
43+
triggerType = TriggerType.CONTEXT_MENU,
44+
prompt = ChatPrompt(
45+
prompt = prompt,
46+
escapedPrompt = modelPrompt,
47+
command = null
48+
),
49+
autoSubmit = true
50+
)
51+
52+
val uiMessage = FlareUiMessage(SEND_TO_PROMPT, params)
53+
AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage)
54+
}
55+
}
56+
}
57+
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorUtil.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ object CodeWhispererEditorUtil {
3232
val fileName = getFileName(psiFile)
3333
val programmingLanguage = psiFile.programmingLanguage()
3434
val fileRelativePath = getRelativePathToContentRoot(editor)
35-
return FileContextInfo(caretContext, fileName, programmingLanguage, fileRelativePath)
35+
val fileUri = getFileUri(psiFile)
36+
return FileContextInfo(caretContext, fileName, programmingLanguage, fileRelativePath, fileUri)
3637
}
3738

3839
fun extractCaretContext(editor: Editor): CaretContext {
@@ -73,6 +74,11 @@ object CodeWhispererEditorUtil {
7374
private fun getFileName(psiFile: PsiFile): String =
7475
psiFile.name.substring(0, psiFile.name.length.coerceAtMost(CodeWhispererConstants.FILENAME_CHARS_LIMIT))
7576

77+
private fun getFileUri(psiFile: PsiFile): String? =
78+
psiFile.virtualFile?.takeIf { it.isValid }?.let { vFile ->
79+
vFile.url.substring(0, vFile.url.length.coerceAtMost(CodeWhispererConstants.FILENAME_CHARS_LIMIT))
80+
}
81+
7682
fun getRelativePathToContentRoot(editor: Editor): String? =
7783
editor.project?.let { project ->
7884
FileDocumentManager.getInstance().getFile(editor.document)?.let { vFile ->

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ data class FileContextInfo(
4747
val filename: String,
4848
val programmingLanguage: CodeWhispererProgrammingLanguage,
4949
val fileRelativePath: String?,
50+
val fileUri: String?,
5051
)
5152

5253
data class RecommendationContext(

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ class CodeWhispererServiceTest : CodeWhispererTestBase() {
7474
CaretContext(leftFileContext = "", rightFileContext = "public class Main {}", leftContextOnCurrentLine = ""),
7575
"main.java",
7676
CodeWhispererJava.INSTANCE,
77-
"main.java"
77+
"main.java",
78+
file.virtualFile.url
7879
)
7980
)
8081
}

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ fun aFileContextInfo(language: CodeWhispererProgrammingLanguage? = null): FileCo
127127
CodeWhispererJava.INSTANCE
128128
).random()
129129

130-
return FileContextInfo(caretContextInfo, fileName, programmingLanguage, fileRelativePath)
130+
return FileContextInfo(caretContextInfo, fileName, programmingLanguage, fileRelativePath, null)
131131
}
132132

133133
fun aTriggerType(): CodewhispererTriggerType =

plugins/amazonq/mynah-ui/src/mynah-ui/ui/texts/disclaimer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ChatItem, MynahIcons } from '@aws/mynah-ui-chat'
88
export const disclaimerAcknowledgeButtonId = 'amazonq-disclaimer-acknowledge-button-id'
99
export const disclaimerCard: Partial<ChatItem> = {
1010
messageId: 'amazonq-disclaimer-card',
11-
body: 'Amazon Q Developer uses generative AI. You may need to verify responses. See the [AWS Responsible AI Policy](https://aws.amazon.com/machine-learning/responsible-ai/policy/). Amazon Q Developer processes data across all US Regions. See [here](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/cross-region-inference.html) for more info. Amazon Q may retain chats to provide and maintain the service.',
11+
body: 'Amazon Q Developer uses generative AI. You may need to verify responses. See the [AWS Responsible AI Policy](https://aws.amazon.com/machine-learning/responsible-ai/policy/). Amazon Q may retain chats to provide and maintain the service. For information on the AWS Regions where Amazon Q may perform inference, see [the documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/cross-region-processing.html#cross-region-inference).',
1212
buttons: [
1313
{
1414
text: 'Acknowledge',

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ import com.intellij.util.net.HttpConfigurable
2424
import com.intellij.util.net.JdkProxyProvider
2525
import kotlinx.coroutines.CoroutineScope
2626
import kotlinx.coroutines.Deferred
27+
import kotlinx.coroutines.Job
2728
import kotlinx.coroutines.async
2829
import kotlinx.coroutines.channels.BufferOverflow
30+
import kotlinx.coroutines.delay
2931
import kotlinx.coroutines.flow.MutableSharedFlow
3032
import kotlinx.coroutines.flow.asSharedFlow
3133
import kotlinx.coroutines.flow.map
3234
import kotlinx.coroutines.future.asCompletableFuture
35+
import kotlinx.coroutines.isActive
36+
import kotlinx.coroutines.launch
3337
import kotlinx.coroutines.runBlocking
3438
import kotlinx.coroutines.sync.Mutex
3539
import kotlinx.coroutines.sync.withLock
@@ -52,6 +56,7 @@ import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod
5256
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
5357
import org.eclipse.lsp4j.launch.LSPLauncher
5458
import org.slf4j.event.Level
59+
import software.aws.toolkits.core.utils.debug
5560
import software.aws.toolkits.core.utils.getLogger
5661
import software.aws.toolkits.core.utils.info
5762
import software.aws.toolkits.core.utils.warn
@@ -128,6 +133,9 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
128133

129134
val encryptionManager
130135
get() = instance.getCompleted().encryptionManager
136+
private val heartbeatJob: Job
137+
private val restartTimestamps = ArrayDeque<Long>()
138+
private val restartMutex = Mutex() // Separate mutex for restart tracking
131139

132140
val rawEndpoint
133141
get() = instance.getCompleted().rawEndpoint
@@ -166,9 +174,50 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
166174

167175
init {
168176
instance = start()
177+
178+
// Initialize heartbeat job
179+
heartbeatJob = cs.launch {
180+
while (isActive) {
181+
delay(5.seconds) // Check every 5 seconds
182+
val shouldLoop = checkConnectionStatus()
183+
if (!shouldLoop) {
184+
break
185+
}
186+
}
187+
}
188+
}
189+
190+
private suspend fun checkConnectionStatus(): Boolean {
191+
try {
192+
val currentInstance = mutex.withLock { instance }.await()
193+
194+
// Check if the launcher's Future (startListening) is done
195+
// If it's done, that means the connection has been terminated
196+
if (currentInstance.launcherFuture.isDone) {
197+
LOG.debug { "LSP server connection terminated, checking restart limits" }
198+
val canRestart = checkForRemainingRestartAttempts()
199+
if (!canRestart) {
200+
return false
201+
}
202+
LOG.debug { "Restarting LSP server" }
203+
restart()
204+
} else {
205+
LOG.debug { "LSP server is currently running" }
206+
}
207+
} catch (e: Exception) {
208+
LOG.debug(e) { "Connection status check failed, checking restart limits" }
209+
val canRestart = checkForRemainingRestartAttempts()
210+
if (!canRestart) {
211+
return false
212+
}
213+
LOG.debug { "Restarting LSP server" }
214+
restart()
215+
}
216+
return true
169217
}
170218

171219
override fun dispose() {
220+
heartbeatJob.cancel()
172221
}
173222

174223
suspend fun restart() = mutex.withLock {
@@ -195,6 +244,25 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
195244
instance = start()
196245
}
197246

247+
private suspend fun checkForRemainingRestartAttempts(): Boolean = restartMutex.withLock {
248+
val currentTime = System.currentTimeMillis()
249+
250+
while (restartTimestamps.isNotEmpty() &&
251+
currentTime - restartTimestamps.first() > RESTART_WINDOW_MS
252+
) {
253+
restartTimestamps.removeFirst()
254+
}
255+
256+
if (restartTimestamps.size < MAX_RESTARTS) {
257+
restartTimestamps.addLast(currentTime)
258+
return true
259+
}
260+
261+
LOG.info { "Rate limit reached for LSP server restarts. Stop attempting to restart." }
262+
263+
return false
264+
}
265+
198266
suspend fun<T> execute(runnable: suspend AmazonQLspService.(AmazonQLanguageServer) -> T): T {
199267
val lsp = withTimeout(10.seconds) {
200268
val holder = mutex.withLock { instance }.await()
@@ -212,6 +280,8 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
212280

213281
companion object {
214282
private val LOG = getLogger<AmazonQLspService>()
283+
private const val MAX_RESTARTS = 5
284+
private const val RESTART_WINDOW_MS = 3 * 60 * 1000
215285
fun getInstance(project: Project) = project.service<AmazonQLspService>()
216286

217287
@Deprecated("Easy to accidentally freeze EDT")
@@ -241,7 +311,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
241311
get() = launcher.remoteEndpoint
242312

243313
@Suppress("ForbiddenVoid")
244-
private val launcherFuture: Future<Void>
314+
val launcherFuture: Future<Void>
245315
private val launcherHandler: KillableProcessHandler
246316
val initializeResult: Deferred<InitializeResult>
247317

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.intellij.openapi.util.SystemInfo
77
import com.intellij.openapi.util.text.StringUtil
88
import com.intellij.util.io.DigestUtil
99
import com.intellij.util.system.CpuArch
10+
import org.apache.commons.io.FileUtils
1011
import software.aws.toolkits.core.utils.ZIP_PROPERTY_POSIX
1112
import software.aws.toolkits.core.utils.createParentDirectories
1213
import software.aws.toolkits.core.utils.exists
@@ -68,7 +69,8 @@ fun getSubFolders(basePath: Path): List<Path> = try {
6869
fun moveFilesFromSourceToDestination(sourceDir: Path, targetDir: Path) {
6970
try {
7071
Files.createDirectories(targetDir.parent)
71-
Files.move(sourceDir, targetDir, StandardCopyOption.REPLACE_EXISTING)
72+
// NIO move does not work when copying across mount points (i.e. /tmp is on tmpfs)
73+
FileUtils.moveDirectory(sourceDir.toFile(), targetDir.toFile())
7274
} catch (e: Exception) {
7375
throw IllegalStateException("Failed to move files from $sourceDir to $targetDir", e)
7476
}

0 commit comments

Comments
 (0)