-
Notifications
You must be signed in to change notification settings - Fork 1
feat(mpp-idea): implement remote agent support for IntelliJ IDEA plugin #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,9 @@ import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewViewModel | |
| import cc.unitmesh.devins.idea.toolwindow.header.IdeaAgentTabsHeader | ||
| import cc.unitmesh.devins.idea.toolwindow.knowledge.IdeaKnowledgeContent | ||
| import cc.unitmesh.devins.idea.toolwindow.knowledge.IdeaKnowledgeViewModel | ||
| import cc.unitmesh.devins.idea.toolwindow.remote.IdeaRemoteAgentContent | ||
| import cc.unitmesh.devins.idea.toolwindow.remote.IdeaRemoteAgentViewModel | ||
| import cc.unitmesh.devins.idea.toolwindow.remote.getEffectiveProjectId | ||
| import cc.unitmesh.devins.idea.toolwindow.status.IdeaToolLoadingStatusBar | ||
| import cc.unitmesh.devins.idea.toolwindow.timeline.IdeaEmptyStateMessage | ||
| import cc.unitmesh.devins.idea.toolwindow.timeline.IdeaTimelineContent | ||
|
|
@@ -77,6 +80,13 @@ fun IdeaAgentApp( | |
| // Knowledge ViewModel (created lazily when needed) | ||
| var knowledgeViewModel by remember { mutableStateOf<IdeaKnowledgeViewModel?>(null) } | ||
|
|
||
| // Remote Agent ViewModel (created lazily when needed) | ||
| var remoteAgentViewModel by remember { mutableStateOf<IdeaRemoteAgentViewModel?>(null) } | ||
|
|
||
| // Remote agent state for input handling | ||
| var remoteProjectId by remember { mutableStateOf("") } | ||
| var remoteGitUrl by remember { mutableStateOf("") } | ||
|
|
||
| // Auto-scroll to bottom when new items arrive | ||
| LaunchedEffect(timeline.size, streamingOutput) { | ||
| if (timeline.isNotEmpty() || streamingOutput.isNotEmpty()) { | ||
|
|
@@ -87,14 +97,21 @@ fun IdeaAgentApp( | |
| } | ||
| } | ||
|
|
||
| // Create CodeReviewViewModel when switching to CODE_REVIEW tab | ||
| // Create ViewModels when switching tabs | ||
| LaunchedEffect(currentAgentType) { | ||
| if (currentAgentType == AgentType.CODE_REVIEW && codeReviewViewModel == null) { | ||
| codeReviewViewModel = IdeaCodeReviewViewModel(project, coroutineScope) | ||
| } | ||
| if (currentAgentType == AgentType.KNOWLEDGE && knowledgeViewModel == null) { | ||
| knowledgeViewModel = IdeaKnowledgeViewModel(project, coroutineScope) | ||
| } | ||
| if (currentAgentType == AgentType.REMOTE && remoteAgentViewModel == null) { | ||
| remoteAgentViewModel = IdeaRemoteAgentViewModel( | ||
| project = project, | ||
| coroutineScope = coroutineScope, | ||
| serverUrl = "http://localhost:8080" | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Dispose ViewModels when leaving their tabs | ||
|
|
@@ -108,6 +125,10 @@ fun IdeaAgentApp( | |
| knowledgeViewModel?.dispose() | ||
| knowledgeViewModel = null | ||
| } | ||
| if (currentAgentType != AgentType.REMOTE) { | ||
| remoteAgentViewModel?.dispose() | ||
| remoteAgentViewModel = null | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -133,13 +154,23 @@ fun IdeaAgentApp( | |
| .weight(1f) | ||
| ) { | ||
| when (currentAgentType) { | ||
| AgentType.CODING, AgentType.REMOTE, AgentType.LOCAL_CHAT -> { | ||
| AgentType.CODING, AgentType.LOCAL_CHAT -> { | ||
| IdeaTimelineContent( | ||
| timeline = timeline, | ||
| streamingOutput = streamingOutput, | ||
| listState = listState | ||
| ) | ||
| } | ||
| AgentType.REMOTE -> { | ||
| remoteAgentViewModel?.let { vm -> | ||
| IdeaRemoteAgentContent( | ||
| viewModel = vm, | ||
| listState = listState, | ||
| onProjectIdChange = { remoteProjectId = it }, | ||
| onGitUrlChange = { remoteGitUrl = it } | ||
| ) | ||
| } ?: IdeaEmptyStateMessage("Loading Remote Agent...") | ||
| } | ||
| AgentType.CODE_REVIEW -> { | ||
| codeReviewViewModel?.let { vm -> | ||
| IdeaCodeReviewContent( | ||
|
|
@@ -159,7 +190,7 @@ fun IdeaAgentApp( | |
| Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().height(1.dp)) | ||
|
|
||
| // Input area (only for chat-based modes) | ||
| if (currentAgentType == AgentType.CODING || currentAgentType == AgentType.REMOTE || currentAgentType == AgentType.LOCAL_CHAT) { | ||
| if (currentAgentType == AgentType.CODING || currentAgentType == AgentType.LOCAL_CHAT) { | ||
| IdeaDevInInputArea( | ||
| project = project, | ||
| parentDisposable = viewModel, | ||
|
|
@@ -181,6 +212,39 @@ fun IdeaAgentApp( | |
| ) | ||
| } | ||
|
|
||
| // Remote agent input area | ||
| if (currentAgentType == AgentType.REMOTE) { | ||
| remoteAgentViewModel?.let { remoteVm -> | ||
| val remoteIsExecuting by remoteVm.isExecuting.collectAsState() | ||
| val remoteIsConnected by remoteVm.isConnected.collectAsState() | ||
|
|
||
| IdeaDevInInputArea( | ||
| project = project, | ||
| parentDisposable = viewModel, | ||
| isProcessing = remoteIsExecuting, | ||
| onSend = { task -> | ||
| val effectiveProjectId = getEffectiveProjectId(remoteProjectId, remoteGitUrl) | ||
| if (effectiveProjectId.isNotBlank()) { | ||
| remoteVm.executeTask(effectiveProjectId, task, remoteGitUrl) | ||
| } else { | ||
| remoteVm.renderer.renderError("Please provide a project or Git URL") | ||
| } | ||
|
Comment on lines
+225
to
+231
|
||
| }, | ||
| onAbort = { remoteVm.cancelTask() }, | ||
| workspacePath = project.basePath, | ||
| totalTokens = null, | ||
| onSettingsClick = { viewModel.setShowConfigDialog(true) }, | ||
| onAtClick = {}, | ||
| availableConfigs = availableConfigs, | ||
| currentConfigName = currentConfigName, | ||
| onConfigSelect = { config -> | ||
| viewModel.setActiveConfig(config.name) | ||
| }, | ||
| onConfigureClick = { viewModel.setShowConfigDialog(true) } | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Tool loading status bar | ||
| IdeaToolLoadingStatusBar( | ||
| viewModel = viewModel, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,162 @@ | ||||
| package cc.unitmesh.devins.idea.toolwindow.remote | ||||
|
|
||||
| import cc.unitmesh.agent.RemoteAgentEvent | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.engine.cio.* | ||||
| import io.ktor.client.plugins.sse.* | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.client.statement.* | ||||
| import io.ktor.http.* | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.flow | ||||
| import kotlinx.coroutines.flow.mapNotNull | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.json.Json | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
|
|
||||
| /** | ||||
| * Remote Agent Client for IntelliJ IDEA plugin. | ||||
| * | ||||
| * Connects to mpp-server and streams agent execution events via SSE. | ||||
| * This is adapted from mpp-ui's RemoteAgentClient for use in the IDE plugin. | ||||
| */ | ||||
| class IdeaRemoteAgentClient( | ||||
| private val baseUrl: String = "http://localhost:8080" | ||||
| ) { | ||||
| private val httpClient: HttpClient = HttpClient(CIO) { | ||||
| install(SSE) { | ||||
| reconnectionTime = 30.seconds | ||||
| maxReconnectionAttempts = 3 | ||||
| } | ||||
|
|
||||
| // We handle HTTP errors manually to provide better error messages | ||||
| // SSE connections need explicit status checking | ||||
| expectSuccess = false | ||||
|
||||
| expectSuccess = false |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The printStackTrace() call should be replaced with proper logging using IntelliJ's logging framework. In IntelliJ plugins, use com.intellij.openapi.diagnostic.Logger for consistent error tracking. Example: private val LOG = Logger.getInstance(IdeaRemoteAgentClient::class.java) and then LOG.error("Stream connection failed", e).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server URL is hardcoded to
"http://localhost:8080". This should be configurable, either through user preferences, environment variables, or a configuration file. Consider loading this fromConfigManageror providing a UI setting to allow users to change the server URL without modifying code.