Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
eb00356
chore(plugin): update plugin name, vendor email, and description
phodal Dec 1, 2025
20397b1
feat(ui): add platform-specific UTF-8 font support
phodal Dec 1, 2025
f74cbf1
Merge branch 'unit-mesh:master' into master
phodal Dec 1, 2025
9f9b009
feat: add pluggable DevInsCompilerService for switchable compiler core
phodal Dec 2, 2025
dba998e
feat(mpp-idea): add IdeaDiffActions for diff/patch operations
phodal Dec 2, 2025
48c66bb
feat(mpp-idea): add IdeaCodeActions for code operations
phodal Dec 2, 2025
ca4ae8f
feat(mpp-idea): add IdeaPlanActions and IdeaPlanRenderer
phodal Dec 2, 2025
75c5ec6
feat(mpp-idea): add IdeaTerminalActions and IdeaTerminalRenderer
phodal Dec 2, 2025
2490afa
feat(mpp-idea): add IdeaDiagramActions and enhanced IdeaMermaidRenderer
phodal Dec 2, 2025
4228157
feat(core): add async live terminal support for shell tools
phodal Dec 2, 2025
7763bb8
feat(shell): add live terminal output streaming in IDEA
phodal Dec 2, 2025
559f0ea
feat(mpp-idea): add cancel button to live terminal bubble
phodal Dec 2, 2025
63ac20f
feat(shell): add process management tools for shell sessions
phodal Dec 2, 2025
c8461fd
feat(shell): send output log to AI on process cancel
phodal Dec 2, 2025
4e9d3a4
fix(shell): fix cancel button not capturing PTY output
phodal Dec 2, 2025
0616d6f
fix(shell): make ShellSessionManager cross-platform compatible
phodal Dec 2, 2025
19f0892
fix(shell): get output from ShellSessionManager in startSessionMonito…
phodal Dec 2, 2025
dfde3c4
fix(shell): configure PTY process to preserve output after termination
phodal Dec 2, 2025
8e8d375
fix(shell): fix PTY output collection - only read inputStream
phodal Dec 2, 2025
03cadc6
Merge branch 'unit-mesh:master' into master
phodal Dec 2, 2025
c16c783
feat(terminal): add user cancellation tracking and ANSI stripping for…
phodal Dec 2, 2025
84edccc
feat(terminal): skip analysis and error rendering for user-cancelled …
phodal Dec 2, 2025
59e5d86
fix(review): address PR #27 review comments
phodal Dec 2, 2025
3a8534f
fix(kmp): remove synchronized for WASM/JS compatibility
phodal Dec 2, 2025
9a755cc
feat(mpp-idea): add file search popup and fix MCP config button
phodal Dec 2, 2025
98ead2f
fix: address PR review comments
phodal Dec 2, 2025
4863d85
feat(mpp-idea): improve file search popup with proper PopupMenu pattern
phodal Dec 2, 2025
ea875c7
fix(mpp-idea): fix folder selection and improve search UI
phodal Dec 2, 2025
679895b
fix(devins-lang): replace ClsFileImpl with PsiCompiledFile interface
phodal Dec 2, 2025
125394c
refactor(mpp-idea): move IdeaDevInInputArea to separate file
phodal Dec 2, 2025
9d117d2
feat(mpp-idea): redesign IdeaDevInInputArea layout with unified border
phodal Dec 2, 2025
f4b1e57
feat(mpp-idea): improve MCP config dialog and implement prompt enhanc…
phodal Dec 2, 2025
b54c313
fix(mpp-idea): fix prompt enhancement functionality
phodal Dec 2, 2025
4334722
feat(mpp-idea): use DialogWrapper for IdeaMcpConfigDialog to center i…
phodal Dec 2, 2025
b5d9e00
ci(github-actions): free disk space in build workflow
phodal Dec 2, 2025
45da8a8
fix(mpp-idea): address PR review comments
phodal Dec 2, 2025
236ca62
feat(mpp-idea): enhance IdeaMcpConfigDialog UI to match ToolConfigDialog
phodal Dec 2, 2025
fa5decf
feat(mpp-idea): add IdeaToolConfigService for tool config state manag…
phodal Dec 2, 2025
d9f8abc
fix(devins-lang): fix memory leak in DevInsProgramRunner
phodal Dec 2, 2025
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

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 @@ -86,7 +86,7 @@ class CodingAgent(
private val toolOrchestrator = ToolOrchestrator(toolRegistry, policyEngine, renderer, mcpConfigService = mcpToolConfigService)

private val errorRecoveryAgent = ErrorRecoveryAgent(projectPath, llmService)
private val analysisAgent = AnalysisAgent(llmService, contentThreshold = 5000)
private val analysisAgent = AnalysisAgent(llmService, contentThreshold = 15000)
private val mcpToolsInitializer = McpToolsInitializer()

// 执行器
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,27 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.datetime.Clock
import cc.unitmesh.agent.orchestrator.ToolExecutionContext as OrchestratorContext

/**
* Configuration for async shell execution timeout behavior
*/
data class AsyncShellConfig(
/** Initial wait timeout in milliseconds before notifying AI that process is still running */
val initialWaitTimeoutMs: Long = 60_000L, // 1 minute
/** Maximum total wait time in milliseconds */
val maxWaitTimeoutMs: Long = 300_000L, // 5 minutes
/** Interval for checking process status after initial timeout */
val checkIntervalMs: Long = 30_000L // 30 seconds
)

class CodingAgentExecutor(
projectPath: String,
llmService: KoogLLMService,
toolOrchestrator: ToolOrchestrator,
renderer: CodingAgentRenderer,
maxIterations: Int = 100,
private val subAgentManager: SubAgentManager? = null,
enableLLMStreaming: Boolean = true
enableLLMStreaming: Boolean = true,
private val asyncShellConfig: AsyncShellConfig = AsyncShellConfig()
) : BaseAgentExecutor(
projectPath = projectPath,
llmService = llmService,
Expand Down Expand Up @@ -202,12 +215,17 @@ class CodingAgentExecutor(
environment = emptyMap()
)

val executionResult = toolOrchestrator.executeToolCall(
var executionResult = toolOrchestrator.executeToolCall(
toolName,
params,
executionContext
)

// Handle Pending result (async shell execution)
if (executionResult.isPending) {
executionResult = handlePendingResult(executionResult, toolName, params)
}

results.add(Triple(toolName, params, executionResult))

val stepResult = AgentStep(
Expand Down Expand Up @@ -240,7 +258,8 @@ class CodingAgentExecutor(
}
}
is ToolResult.AgentResult -> if (!result.success) result.content else stepResult.result
else -> stepResult.result
is ToolResult.Pending -> stepResult.result // Should not happen after handlePendingResult
is ToolResult.Success -> stepResult.result
}

val contentHandlerResult = checkForLongContent(toolName, fullOutput ?: "", executionResult)
Expand All @@ -260,8 +279,9 @@ class CodingAgentExecutor(
}

// 错误恢复处理
if (!executionResult.isSuccess) {
val command = if (toolName == "shell") params["command"] as? String else null
// 跳过用户取消的场景 - 用户取消是明确的意图,不需要显示额外的错误消息
val wasCancelledByUser = executionResult.metadata["cancelled"] == "true"
if (!executionResult.isSuccess && !executionResult.isPending && !wasCancelledByUser) {
val errorMessage = executionResult.content ?: "Unknown error"

renderer.renderError("Tool execution failed: $errorMessage")
Expand All @@ -271,6 +291,112 @@ class CodingAgentExecutor(
results
}

/**
* Handle a Pending result from async shell execution.
* Waits for the session to complete with timeout handling.
* If the process takes longer than initialWaitTimeoutMs, returns a special result
* indicating the process is still running (similar to Augment's behavior).
*/
private suspend fun handlePendingResult(
pendingResult: ToolExecutionResult,
toolName: String,
params: Map<String, Any>
): ToolExecutionResult {
val pending = pendingResult.result as? ToolResult.Pending
?: return pendingResult

val sessionId = pending.sessionId
val command = pending.command
val startTime = pendingResult.startTime

// First, try to wait for the initial timeout
val initialResult = renderer.awaitSessionResult(sessionId, asyncShellConfig.initialWaitTimeoutMs)

return when (initialResult) {
is ToolResult.Success -> {
// Process completed within initial timeout
val endTime = Clock.System.now().toEpochMilliseconds()
ToolExecutionResult.success(
executionId = pendingResult.executionId,
toolName = toolName,
content = initialResult.content,
startTime = startTime,
endTime = endTime,
metadata = initialResult.metadata + mapOf("sessionId" to sessionId)
)
}
is ToolResult.Error -> {
// Process failed
val endTime = Clock.System.now().toEpochMilliseconds()
ToolExecutionResult.failure(
executionId = pendingResult.executionId,
toolName = toolName,
error = initialResult.message,
startTime = startTime,
endTime = endTime,
metadata = initialResult.metadata + mapOf("sessionId" to sessionId)
)
}
is ToolResult.Pending -> {
// Process is still running after initial timeout
// Return a special result to inform the AI
val elapsedSeconds = (Clock.System.now().toEpochMilliseconds() - startTime) / 1000
val stillRunningMessage = buildString {
appendLine("⏳ Process is still running after ${elapsedSeconds}s")
appendLine("Command: $command")
appendLine("Session ID: $sessionId")
appendLine()
appendLine("The process is executing in the background. You can:")
appendLine("1. Continue with other tasks while waiting")
appendLine("2. Check the terminal output in the UI for real-time progress")
appendLine("3. The result will be available when the process completes")
}

// Return as a "success" with the still-running message
// This allows the agent to continue and make decisions
val endTime = Clock.System.now().toEpochMilliseconds()
ToolExecutionResult(
executionId = pendingResult.executionId,
toolName = toolName,
result = ToolResult.Success(
content = stillRunningMessage,
metadata = mapOf(
"status" to "still_running",
"sessionId" to sessionId,
"command" to command,
"elapsedSeconds" to elapsedSeconds.toString()
)
),
startTime = startTime,
endTime = endTime,
state = ToolExecutionState.Executing(pendingResult.executionId, startTime),
metadata = mapOf(
"sessionId" to sessionId,
"isAsync" to "true",
"stillRunning" to "true"
)
)
}
is ToolResult.AgentResult -> {
// Unexpected, but handle it
val endTime = Clock.System.now().toEpochMilliseconds()
ToolExecutionResult(
executionId = pendingResult.executionId,
toolName = toolName,
result = initialResult,
startTime = startTime,
endTime = endTime,
state = if (initialResult.success) {
ToolExecutionState.Success(pendingResult.executionId, initialResult, endTime - startTime)
} else {
ToolExecutionState.Failed(pendingResult.executionId, initialResult.content, endTime - startTime)
},
metadata = mapOf("sessionId" to sessionId)
)
}
}
}

private fun recordFileEdit(params: Map<String, Any>) {
val path = params["path"] as? String
val content = params["content"] as? String
Expand Down Expand Up @@ -343,6 +469,13 @@ class CodingAgentExecutor(
return null
}

// 对于用户取消的命令,不需要分析输出
// 用户取消是明确的意图,不需要对取消前的输出做分析
val wasCancelledByUser = executionResult.metadata["cancelled"] == "true"
if (wasCancelledByUser) {
return null
}

// 检测内容类型
val contentType = when {
toolName == "glob" -> "file-list"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ class DocumentAgentExecutor(
* P1: Check for long content and delegate to AnalysisAgent for summarization
* NOTE: Code content (from $.code.* queries) is NOT summarized to preserve actual code
* NOTE: Live Session output is NOT summarized to preserve real-time terminal output
* NOTE: User cancelled commands are NOT summarized - cancellation is explicit user intent
*/
private suspend fun checkForLongContent(
toolName: String,
Expand All @@ -318,6 +319,13 @@ class DocumentAgentExecutor(
return null
}

// 对于用户取消的命令,不需要分析输出
// 用户取消是明确的意图,不需要对取消前的输出做分析
val wasCancelledByUser = executionResult.metadata["cancelled"] == "true"
if (wasCancelledByUser) {
return null
}

val isCodeContent = output.contains("📘 class ") ||
output.contains("⚡ fun ") ||
output.contains("Found") && output.contains("entities") ||
Expand Down
Loading
Loading