Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ jobs:
continue-on-error: true
steps:
# Check out the current repository
# Free GitHub Actions Environment Disk Space
- name: Maximize Build Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
large-packages: false

- name: Fetch Sources
uses: actions/checkout@v4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import cc.unitmesh.devti.util.relativePath
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiCompiledFile
import com.intellij.psi.PsiManager
import com.intellij.psi.impl.compiled.ClsFileImpl
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.PsiShortNamesCache
Expand Down Expand Up @@ -65,23 +65,19 @@ class FileInsCommand(private val myProject: Project, private val prop: String) :
val language = psiFile?.language?.displayName ?: ""

val fileContent = when (psiFile) {
is ClsFileImpl -> {
psiFile.text
is PsiCompiledFile -> {
// For compiled files (like .class files), get the decompiled text
psiFile.decompiledPsiFile?.text ?: virtualFile.readText()
}

else -> {
runReadAction { virtualFile.readText() }
virtualFile.readText()
}
}

Pair(fileContent, language)
}

if (content == null) {
AutoDevNotifications.warn(myProject, "Cannot read file: $prop")
return "Cannot read file: $prop"
}

val fileContent = splitLines(range, content)

val realPath = virtualFile.relativePath(myProject)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.GenericProgramRunner
import com.intellij.execution.runners.showRunContent
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.util.Disposer
import com.intellij.util.messages.MessageBusConnection
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicReference

class DevInsProgramRunner : GenericProgramRunner<RunnerSettings>(), Disposable {
class DevInsProgramRunner : GenericProgramRunner<RunnerSettings>() {
private val RUNNER_ID: String = "DevInsProgramRunner"

private val connection = ApplicationManager.getApplication().messageBus.connect(this)

// Use lazy initialization to avoid memory leak - connection is created per execution
// and tied to the project's lifecycle, not the runner's lifecycle
private var connection: MessageBusConnection? = null
private var isSubscribed = false
Comment on lines +25 to 28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: isSubscribed flag is never reset when the connection is disposed.

DevInsProgramRunner is a singleton extension. When a project closes, the connection is disposed via Disposer.register, but isSubscribed remains true. Subsequent runs on a different project (or the same project after reopening) will skip subscription entirely because isSubscribed is still true, causing the DevInsRunListener to never be registered.

Consider using a per-project subscription map or resetting state when the connection is disposed:

-    // Use lazy initialization to avoid memory leak - connection is created per execution
-    // and tied to the project's lifecycle, not the runner's lifecycle
-    private var connection: MessageBusConnection? = null
-    private var isSubscribed = false
+    // Track subscriptions per project to handle multiple projects and project reopening
+    private val subscribedProjects = mutableSetOf<String>()

Then in doExecute, use the project's unique identifier and add a dispose listener:

val projectId = environment.project.locationHash
if (projectId !in subscribedProjects) {
    val projectConnection = environment.project.messageBus.connect()
    Disposer.register(environment.project, projectConnection)
    
    // Reset subscription state when project is disposed
    Disposer.register(environment.project) {
        subscribedProjects.remove(projectId)
    }
    
    projectConnection.subscribe(DevInsRunListener.TOPIC, object : DevInsRunListener {
        // ... listener implementation
    })
    
    subscribedProjects.add(projectId)
}
🤖 Prompt for AI Agents
In
exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/run/DevInsProgramRunner.kt
around lines 25 to 28, the singleton runner uses a single boolean isSubscribed
that is never reset when a project's MessageBusConnection is disposed, causing
listeners to be skipped on subsequent project runs; change the approach to track
subscriptions per project (e.g., a Set or Map keyed by project.locationHash or
project) or ensure isSubscribed is cleared when the connection is disposed:
create the project-specific connection via project.messageBus.connect(),
register that connection with Disposer.register(project, connection), register a
Disposer callback to remove the project key (or set isSubscribed=false) when the
project is disposed, and only subscribe the DevInsRunListener when the project
key is not already tracked.


override fun getRunnerId(): String = RUNNER_ID
Expand All @@ -40,7 +41,15 @@ class DevInsProgramRunner : GenericProgramRunner<RunnerSettings>(), Disposable {

ApplicationManager.getApplication().invokeAndWait {
if (!isSubscribed) {
connection.subscribe(DevInsRunListener.TOPIC, object : DevInsRunListener {
// Connect to project's message bus instead of application's
// This ensures proper disposal when the project is closed
val projectConnection = environment.project.messageBus.connect()
connection = projectConnection

// Register for disposal with the project
Disposer.register(environment.project, projectConnection)

projectConnection.subscribe(DevInsRunListener.TOPIC, object : DevInsRunListener {
override fun runFinish(
allOutput: String,
llmOutput: String,
Expand All @@ -67,8 +76,4 @@ class DevInsProgramRunner : GenericProgramRunner<RunnerSettings>(), Disposable {

return result.get()
}

override fun dispose() {
connection.disconnect()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cc.unitmesh.devins.idea.services.IdeaToolConfigService
import cc.unitmesh.devins.idea.toolwindow.IdeaAgentViewModel
import cc.unitmesh.devins.ui.compose.theme.AutoDevColors
import com.intellij.openapi.project.Project
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.Text

Expand All @@ -23,12 +25,18 @@ import org.jetbrains.jewel.ui.component.Text
@Composable
fun IdeaToolLoadingStatusBar(
viewModel: IdeaAgentViewModel,
project: Project,
modifier: Modifier = Modifier
) {
val mcpPreloadingMessage by viewModel.mcpPreloadingMessage.collectAsState()
val mcpPreloadingStatus by viewModel.mcpPreloadingStatus.collectAsState()
// Recompute when preloading status changes to make it reactive
val toolStatus = remember(mcpPreloadingStatus) { viewModel.getToolLoadingStatus() }

// Observe tool config service for configuration changes
val toolConfigService = remember { IdeaToolConfigService.getInstance(project) }
val configVersion by toolConfigService.configVersion.collectAsState()

// Recompute when preloading status OR config version changes
val toolStatus = remember(mcpPreloadingStatus, configVersion) { viewModel.getToolLoadingStatus() }

Row(
modifier = modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package cc.unitmesh.devins.idea.editor

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cc.unitmesh.devins.idea.toolwindow.IdeaComposeIcons
import cc.unitmesh.devins.ui.compose.theme.AutoDevColors
import cc.unitmesh.llm.NamedModelConfig
import com.intellij.openapi.project.Project
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.component.Icon
Expand All @@ -29,11 +26,13 @@ import org.jetbrains.jewel.ui.component.Icon
*/
@Composable
fun IdeaBottomToolbar(
project: Project? = null,
onSendClick: () -> Unit,
sendEnabled: Boolean,
isExecuting: Boolean = false,
onStopClick: () -> Unit = {},
onPromptOptimizationClick: () -> Unit = {},
isEnhancing: Boolean = false,
totalTokens: Int? = null,
// Model selector props
availableConfigs: List<NamedModelConfig> = emptyList(),
Expand All @@ -42,7 +41,6 @@ fun IdeaBottomToolbar(
onConfigureClick: () -> Unit = {},
modifier: Modifier = Modifier
) {
var showMcpConfigDialog by remember { mutableStateOf(false) }
Row(
modifier = modifier
.fillMaxWidth()
Expand Down Expand Up @@ -81,9 +79,9 @@ fun IdeaBottomToolbar(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// MCP Config button - opens MCP configuration dialog
// MCP Config button - opens MCP configuration dialog using DialogWrapper
IconButton(
onClick = { showMcpConfigDialog = true },
onClick = { IdeaMcpConfigDialogWrapper.show(project) },
modifier = Modifier.size(32.dp)
) {
Icon(
Expand All @@ -95,16 +93,22 @@ fun IdeaBottomToolbar(
}

// Prompt Optimization button
IconButton(
onClick = onPromptOptimizationClick,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = IdeaComposeIcons.AutoAwesome,
contentDescription = "Prompt Optimization",
tint = JewelTheme.globalColors.text.normal,
modifier = Modifier.size(16.dp)
)
Tooltip({
Text(if (isEnhancing) "Enhancing prompt..." else "Enhance prompt with AI")
}) {
IconButton(
onClick = onPromptOptimizationClick,
enabled = !isEnhancing && !isExecuting,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = IdeaComposeIcons.AutoAwesome,
contentDescription = "Prompt Optimization",
tint = if (isEnhancing) JewelTheme.globalColors.text.info
else JewelTheme.globalColors.text.normal,
modifier = Modifier.size(16.dp)
)
}
}

// Send or Stop button
Expand Down Expand Up @@ -156,12 +160,5 @@ fun IdeaBottomToolbar(
}
}
}

// MCP Configuration Dialog
if (showMcpConfigDialog) {
IdeaMcpConfigDialog(
onDismiss = { showMcpConfigDialog = false }
)
}
}

Loading
Loading