Skip to content

feat(mpp-idea): implement remote agent support for IntelliJ IDEA plugin#19

Merged
phodal merged 3 commits intomasterfrom
feat/mpp-idea-remote-agent
Dec 1, 2025
Merged

feat(mpp-idea): implement remote agent support for IntelliJ IDEA plugin#19
phodal merged 3 commits intomasterfrom
feat/mpp-idea-remote-agent

Conversation

@phodal
Copy link
Member

@phodal phodal commented Dec 1, 2025

Summary

This PR implements a Remote Agent for mpp-idea by referencing the existing RemoteAgentChatInterface implementation in mpp-ui.

Changes

New Files Created:

  1. IdeaRemoteAgentClient.kt

    • HTTP client for connecting to mpp-server
    • Uses Ktor with CIO engine and SSE support
    • Provides health check, project listing, and streaming agent execution
    • Data classes for requests/responses (RemoteAgentRequest, LLMConfig, HealthResponse, ProjectInfo, etc.)
  2. IdeaRemoteAgentViewModel.kt

    • ViewModel for managing remote agent state
    • Uses JewelRenderer (instead of ComposeRenderer like mpp-ui)
    • Handles connection to remote server, task execution, and event processing
    • Converts RemoteAgentEvent to renderer calls for UI display
  3. IdeaRemoteAgentContent.kt

    • Compose UI for Remote Agent configuration and display
    • Server URL input with connect button
    • Project selection (dropdown if projects available, text input otherwise)
    • Git URL input (optional)
    • Connection status indicator
    • Timeline content integration
  4. IdeaRemoteAgentViewModelTest.kt

    • Comprehensive tests for ViewModel and renderer interactions
    • Tests for event handling, state management, and data classes

Modified Files:

  • IdeaAgentApp.kt
    • Added imports for remote agent components
    • Added remoteAgentViewModel state management
    • Updated content switch to show IdeaRemoteAgentContent for REMOTE agent type
    • Added remote agent input area with task execution

Architecture

The implementation follows the same pattern as mpp-ui's RemoteAgentChatInterface:

  • Client → HTTP/SSE communication with mpp-server
  • ViewModel → State management and event handling
  • Content → UI composition with Jewel components

Key differences from mpp-ui:

  • Uses JewelRenderer instead of ComposeRenderer for native IntelliJ theming
  • Uses rememberTextFieldState() for Jewel TextField components
  • Uses StateFlow instead of Compose mutableStateOf for better IntelliJ integration

Testing

  • All existing tests pass
  • Added 16 new tests for the remote agent implementation

Pull Request opened by Augment Code with guidance from the PR author

Summary by CodeRabbit

  • New Features

    • Added a REMOTE tab for configuring a server, connecting, and executing tasks remotely with connection status, project selection, and Git URL support.
    • Dedicated remote input area supports task submission, streaming live output, cancel, clear history, and error handling.
    • Displays available projects from server and a visual connection status indicator.
  • Tests

    • Added comprehensive tests covering remote flows, streaming events, request building, and project-ID derivation.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add IdeaRemoteAgentClient for HTTP/SSE communication with mpp-server
- Add IdeaRemoteAgentViewModel for state management and event handling
- Add IdeaRemoteAgentContent UI with server configuration panel
- Update IdeaAgentApp to integrate REMOTE agent type
- Add comprehensive tests for ViewModel and renderer interactions

Referenced from mpp-ui's RemoteAgentChatInterface implementation.
@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a REMOTE agent: new SSE-based remote client, a remote ViewModel managing server connection and streaming execution, Compose UI for remote configuration and timeline, IdeaAgentApp wiring for lazy lifecycle and dedicated remote input state, and unit tests for viewmodel behavior and project ID derivation.

Changes

Cohort / File(s) Summary
Remote Client
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt
New Ktor CIO SSE client with healthCheck, getProjects, executeStream(SSE -> Flow), JSON serializers, error handling, close(), data classes (RemoteAgentRequest, LLMConfig, HealthResponse, ProjectInfo, ProjectListResponse) and RemoteAgentException.
Remote ViewModel
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt
New IdeaRemoteAgentViewModel managing serverUrl, client lifecycle, connection checks, project list, execute/cancel streaming tasks, event handling -> JewelRenderer, StateFlows (isExecuting, isConnected, connectionError, availableProjects), clear/dispose APIs.
Remote UI
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt
New Compose content rendering server config, connection status, project/Git inputs, Connect action, timeline integration, two-way input synchronization, and helper getEffectiveProjectId(projectId, gitUrl).
Integration / App
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt
Adds REMOTE tab support: lazy creation/mounting and disposal of remoteAgentViewModel on tab switch, dedicated remote input area with remoteProjectId/remoteGitUrl state, and hooks to propagate input changes and submit remote tasks.
Tests
mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt
New comprehensive test suite for viewmodel and helpers: initial state, streaming events (iteration/LLM/tool calls/results), error/completion behavior, timeline operations, request building (with/without Git URL), serialization/parsing, and many getEffectiveProjectId edge cases.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as IdeaAgentApp (UI)
    participant VM as IdeaRemoteAgentViewModel
    participant Client as IdeaRemoteAgentClient
    participant Server as Remote Server
    participant Renderer as JewelRenderer

    User->>UI: Switch to REMOTE tab
    UI->>VM: create/mount remoteAgentViewModel

    User->>UI: Enter server URL & Connect
    UI->>VM: checkConnection()
    VM->>Client: healthCheck()
    Client->>Server: GET /health
    Server-->>Client: HealthResponse
    Client-->>VM: health result
    VM->>Client: getProjects()
    Client->>Server: GET /api/projects
    Server-->>Client: ProjectListResponse
    Client-->>VM: projects
    VM-->>UI: update isConnected / availableProjects

    User->>UI: Submit task (projectId/gitUrl + task)
    UI->>VM: executeTask(...)
    VM->>Client: executeStream(RemoteAgentRequest) [SSE]
    Client->>Server: POST /api/agent/stream (SSE)

    loop streaming events
      Server-->>Client: RemoteAgentEvent (LLMChunk / ToolCall / ToolResult / etc.)
      Client->>VM: emit RemoteAgentEvent
      VM->>Renderer: handleRemoteEvent() -> render timeline item
      Renderer-->>UI: timeline updated
    end

    Server-->>Client: RemoteAgentEvent.Complete
    Client->>VM: stream closed
    VM-->>UI: isExecuting = false

    User->>UI: Leave REMOTE tab
    UI->>VM: dispose()
    VM->>Client: close()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas to focus on:

  • IdeaRemoteAgentClient.kt: SSE stream parsing, reconnection behavior, error message clarity, resource cleanup.
  • IdeaRemoteAgentViewModel.kt: coroutine scope and cancellation correctness, event-to-renderer mapping edge cases, stateflow updates.
  • IdeaRemoteAgentContent.kt: two-way state sync (LaunchedEffect/snapshotFlow) and input validation.
  • IdeaAgentApp.kt: lifecycle (lazy create/dispose) and input wiring to avoid leaks or missed disposals.

Possibly related PRs

Poem

🐰 I hopped to a server, ears atween the keys,

SSE winds whispered through timelines with ease,
I stitched project IDs from URLs and names,
Streams unfurled logs and LLMs like little flames,
A rabbit's soft cheer — remote tasks hum, code tames.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.48% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mpp-idea): implement remote agent support for IntelliJ IDEA plugin' clearly and specifically summarizes the main change - adding remote agent support to the mpp-idea plugin with appropriate conventional commit prefix.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mpp-idea-remote-agent

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 2 suggestions posted.

Comment augment review to trigger a new review at any time.

*/
fun getEffectiveProjectId(projectId: String, gitUrl: String): String {
return if (gitUrl.isNotBlank()) {
gitUrl.split('/').last().removeSuffix(".git")
Copy link

Choose a reason for hiding this comment

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

gitUrl.split('/').last() will return an empty string for URLs with a trailing slash (e.g., .../repo/), producing a blank project id; consider handling trailing slashes or selecting the last non-blank segment to avoid rejecting valid inputs.

🤖 Was this useful? React with 👍 or 👎

): RemoteAgentRequest {
return if (gitUrl.isNotBlank()) {
RemoteAgentRequest(
projectId = gitUrl.split('/').lastOrNull()?.removeSuffix(".git") ?: "temp-project",
Copy link

Choose a reason for hiding this comment

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

In buildRequest(), deriving projectId via split('/').lastOrNull()?.removeSuffix(".git") can yield an empty string when the URL ends with /; consider using the last non-blank segment or trimming trailing slashes before splitting (also applies to the similar branch below).

🤖 Was this useful? React with 👍 or 👎

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt (1)

117-133: Restructure DisposableEffect to dispose only the ViewModel for the tab being left, not all ViewModels except the current tab.

The current logic disposes ViewModels based on what currentAgentType is NOT, which is semantically inverted. When switching from CODE_REVIEW to CODING, onDispose runs with currentAgentType = CODING, and the conditions dispose all ViewModels that don't match the current tab.

While this works functionally, it's counterintuitive and harder to maintain. The pattern contradicts other DisposableEffect usages in the codebase (CodeReviewPage.kt, CodeReviewDemo.kt) which track and dispose the specific resource that changed.

Consider tracking the previous agentType or using separate DisposableEffects per ViewModel to make the cleanup intent explicit: only dispose the ViewModel for the tab being left.

🧹 Nitpick comments (10)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1)

44-62: Potential state sync loop between parent and child.

The parent IdeaRemoteAgentContent uses LaunchedEffect to propagate projectId and gitUrl changes to callbacks (lines 57-62), while RemoteConfigPanel also syncs these values bidirectionally (lines 128-139 and 147-156). When the user types in RemoteConfigPanel, the flow is:

  1. projectIdState updates → snapshotFlow fires → onProjectIdChange called (line 131)
  2. Parent receives callback → updates local projectId state (line 72)
  3. Parent's LaunchedEffect(projectId) fires → calls onProjectIdChange again (line 58)

This creates redundant callback invocations. Consider removing the parent-level LaunchedEffect blocks since the child already handles propagation.

-    // Propagate changes to parent
-    LaunchedEffect(projectId) {
-        onProjectIdChange(projectId)
-    }
-    LaunchedEffect(gitUrl) {
-        onGitUrlChange(gitUrl)
-    }
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt (1)

108-114: Consider making the default server URL configurable.

The hardcoded "http://localhost:8080" is fine for local development, but users may need to configure different server endpoints. The UI already allows URL input, so the initial default could be loaded from persisted settings.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (2)

94-97: Replace printStackTrace with proper logging.

Using e.printStackTrace() outputs to stderr without context. Consider using IntelliJ's logging infrastructure or removing debug output in favor of the thrown exception message.

         } catch (e: Exception) {
-            e.printStackTrace()
+            // Consider: com.intellij.openapi.diagnostic.Logger.getInstance(IdeaRemoteAgentClient::class.java).warn("Stream connection failed", e)
             throw RemoteAgentException("Stream connection failed: ${e.message}", e)
         }

108-125: Sensitive fields in serializable data classes may be logged.

RemoteAgentRequest contains password and LLMConfig contains apiKey. Since these are @Serializable, they could appear in logs if the objects are printed. Consider implementing custom toString() to mask sensitive fields.

 @Serializable
 data class RemoteAgentRequest(
     val projectId: String,
     val task: String,
     val llmConfig: LLMConfig? = null,
     val gitUrl: String? = null,
     val branch: String? = null,
     val username: String? = null,
     val password: String? = null
-)
+) {
+    override fun toString(): String = "RemoteAgentRequest(projectId=$projectId, task=$task, gitUrl=$gitUrl, branch=$branch, username=$username, password=***)"
+}

 @Serializable
 data class LLMConfig(
     val provider: String,
     val modelName: String,
     val apiKey: String,
     val baseUrl: String? = null
-)
+) {
+    override fun toString(): String = "LLMConfig(provider=$provider, modelName=$modelName, apiKey=***, baseUrl=$baseUrl)"
+}
mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (3)

29-42: Test scope created but not used; tests use runBlocking instead.

The testScope is initialized in @BeforeEach but never used. All tests use runBlocking directly. Either use the test scope for proper structured concurrency testing, or remove the unused setup.

-    private lateinit var testScope: CoroutineScope
-
-    @BeforeEach
-    fun setUp() {
-        testScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
-    }
-
-    @AfterEach
-    fun tearDown() {
-        testScope.cancel()
-    }
+    // Remove if not using test scope, or consider using kotlinx-coroutines-test's runTest

43-59: Test class name suggests ViewModel testing but primarily tests JewelRenderer.

The comment at lines 45-47 correctly notes that full ViewModel testing requires IntelliJ Platform. Consider renaming the test class to JewelRendererRemoteAgentTest or adding separate integration tests when Platform is available.

Based on learnings, these standalone JUnit 5 tests can be run with cd mpp-idea && ../gradlew test --tests "cc.unitmesh.devins.idea.toolwindow.remote.IdeaRemoteAgentViewModelTest".


253-271: Test duplicates getEffectiveProjectId logic.

Line 259 manually implements the same logic as getEffectiveProjectId in IdeaRemoteAgentContent.kt. Consider importing and using the helper to ensure tests align with production behavior.

+import cc.unitmesh.devins.idea.toolwindow.remote.getEffectiveProjectId
+
     @Test
     fun testRemoteAgentRequestWithGitUrl() {
         val gitUrl = "https://github.com/user/repo.git"
         val task = "Fix the bug"

-        val projectId = gitUrl.split('/').lastOrNull()?.removeSuffix(".git") ?: "temp-project"
+        val projectId = getEffectiveProjectId("", gitUrl)

         val request = RemoteAgentRequest(
             projectId = projectId,
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt (3)

148-181: Duplicated Git URL detection logic across codebase.

The buildRequest function reimplements Git URL detection (lines 162-164) that's conceptually similar to getEffectiveProjectId. Consider consolidating this logic into a shared utility to ensure consistent behavior and reduce maintenance burden.


250-256: Simplify cancelTask by removing redundant null check.

The cancel() call on a Job? can be simplified. Kotlin's safe call operator handles null gracefully, and cancelling an already-completed job is a no-op.

     fun cancelTask() {
-        if (_isExecuting.value && currentExecutionJob != null) {
-            currentExecutionJob?.cancel("Task cancelled by user")
-            currentExecutionJob = null
-            _isExecuting.value = false
-        }
+        currentExecutionJob?.cancel("Task cancelled by user")
+        currentExecutionJob = null
+        _isExecuting.value = false
     }

87-91: Replace println with IntelliJ logger.

Using println for logging is not appropriate in an IntelliJ plugin. Use IntelliJ's logging infrastructure for proper log level control and IDE integration.

+import com.intellij.openapi.diagnostic.Logger
+
+private val LOG = Logger.getInstance(IdeaRemoteAgentViewModel::class.java)
+
     fun executeTask(projectId: String, task: String, gitUrl: String = "") {
         if (_isExecuting.value) {
-            println("Agent is already executing")
+            LOG.info("Agent is already executing")
             return
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f140086 and bbddbc8.

📒 Files selected for processing (5)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt (7 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt (1 hunks)
  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

Use expect/actual for platform-specific code in KMP projects (e.g., file I/O on JVM/JS/Wasm)

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt
  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt
🧠 Learnings (1)
📚 Learning: 2025-11-30T02:30:49.805Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-30T02:30:49.805Z
Learning: Use `cd mpp-idea && ../gradlew test --tests "cc.unitmesh.devins.idea.renderer.JewelRendererTest"` for running standalone JUnit 5 tests that do not require IntelliJ Platform

Applied to files:

  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt
🧬 Code graph analysis (3)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/timeline/IdeaTimelineContent.kt (1)
  • IdeaTimelineContent (20-48)
mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1)
mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/test/DevInsTestCase.kt (5)
  • assertTrue (29-33)
  • assertFalse (38-40)
  • assertNull (64-68)
  • assertEquals (45-49)
  • assertNotNull (54-59)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt (2)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/timeline/IdeaTimelineContent.kt (2)
  • IdeaTimelineContent (20-48)
  • IdeaEmptyStateMessage (80-94)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (2)
  • IdeaRemoteAgentContent (30-95)
  • getEffectiveProjectId (311-317)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build (241)
  • GitHub Check: Build (223)
🔇 Additional comments (7)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (2)

101-156: TextFieldState synchronization pattern is well-implemented.

The bidirectional sync between TextFieldState and external String parameters using snapshotFlow with distinctUntilChanged is the correct pattern for Jewel TextField integration. The external-to-internal sync (lines 142-156) properly checks for differences before updating to avoid loops.


266-306: Connection status indicator is well-designed.

The animated color transition and clear status messaging provide good user feedback. The conditional logic for displaying connection state, error messages, or "Not connected" is appropriately prioritized.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt (1)

215-246: Remote input area implementation is correct.

The validation logic properly checks for a valid project identifier before executing tasks, and uses the getEffectiveProjectId helper to derive the project ID from either the direct input or Git URL. Error feedback via the renderer provides appropriate user guidance.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1)

26-44: HTTP client configuration is well-tuned for IDE plugin usage.

The SSE reconnection settings (30s interval, 3 attempts) and connection pool configuration are appropriate for a background agent client. The expectSuccess = false allows custom error handling but requires explicit status checks in API methods (noted separately).

mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1)

273-307: Data class sanity tests are appropriate.

These tests validate proper instantiation and field access for LLMConfig, HealthResponse, and ProjectInfo. While simple, they help catch potential serialization or constructor issues early.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt (2)

186-245: Event handling implementation is comprehensive and well-structured.

The handleRemoteEvent function properly maps all RemoteAgentEvent types to appropriate renderer calls. State transitions for isProcessing are correctly managed (ending LLM response before tool calls, etc.).


273-276: Resource cleanup is properly implemented.

The dispose() method correctly cancels any pending job and closes the HTTP client, preventing resource leaks when the ViewModel is disposed.

Comment on lines +135 to +144
} catch (e: CancellationException) {
renderer.forceStop()
renderer.renderError("Task cancelled by user")
_isExecuting.value = false
currentExecutionJob = null
} catch (e: Exception) {
renderer.renderError(e.message ?: "Unknown error")
_isExecuting.value = false
currentExecutionJob = null
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Catching CancellationException prevents proper coroutine cancellation propagation.

CancellationException is special in Kotlin coroutines and should generally be rethrown to allow proper cancellation propagation. The current handling logs an error message but may interfere with structured concurrency.

-            } catch (e: CancellationException) {
-                renderer.forceStop()
-                renderer.renderError("Task cancelled by user")
-                _isExecuting.value = false
-                currentExecutionJob = null
+            } catch (e: CancellationException) {
+                renderer.forceStop()
+                // Don't render cancellation as an error - it's user-initiated
+                _isExecuting.value = false
+                currentExecutionJob = null
+                throw e  // Re-throw to maintain structured concurrency
             } catch (e: Exception) {

Alternatively, if you want to handle cancellation gracefully without re-throwing, use a finally block for cleanup:

try {
    // ... streaming logic
} finally {
    _isExecuting.value = false
    currentExecutionJob = null
}
🤖 Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt
around lines 135-144, the code explicitly catches CancellationException which
prevents proper coroutine cancellation propagation; remove the dedicated catch
for CancellationException (or if you must catch it, immediately rethrow it) and
move cleanup into a finally block so that _isExecuting.value = false and
currentExecutionJob = null always run, and keep renderer.forceStop() and
renderer.renderError(...) only for non-cancellation exceptions (i.e., catch
Exception for error reporting, rethrow CancellationException or avoid catching
it).

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements remote agent support for the IntelliJ IDEA plugin (mpp-idea) by creating a new HTTP-based client that connects to mpp-server. The implementation closely mirrors the existing RemoteAgentChatInterface from mpp-ui, adapting it for IntelliJ's plugin architecture with Jewel UI components and StateFlow-based state management instead of Compose's mutableStateOf.

Key Changes:

  • New HTTP/SSE client for streaming agent execution from remote server
  • ViewModel with StateFlow-based state management following IntelliJ plugin patterns
  • Jewel-based Compose UI with server configuration panel and connection status display
  • Integration with existing agent infrastructure through the REMOTE tab in IdeaAgentApp

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
IdeaRemoteAgentClient.kt HTTP client using Ktor/SSE for streaming events from mpp-server; includes data classes for request/response serialization
IdeaRemoteAgentViewModel.kt ViewModel managing connection state, task execution, and event routing to JewelRenderer; follows IntelliJ Disposable pattern
IdeaRemoteAgentContent.kt Compose UI with server configuration inputs, connection status indicator, and timeline display; uses Jewel TextField components
IdeaRemoteAgentViewModelTest.kt Unit tests for renderer behavior and data classes; tests run without IntelliJ Platform dependencies
IdeaAgentApp.kt Integration of remote agent into main UI; adds lazy ViewModel creation, disposal logic, and dedicated input area for remote mode

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 311 to 317
fun getEffectiveProjectId(projectId: String, gitUrl: String): String {
return if (gitUrl.isNotBlank()) {
gitUrl.split('/').last().removeSuffix(".git")
} else {
projectId
}
}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The function getEffectiveProjectId duplicates the logic from IdeaRemoteAgentViewModel.buildRequest() for extracting the project name from a Git URL. The implementation here (gitUrl.split('/').last().removeSuffix(".git")) is slightly different from the ViewModel's version which uses .lastOrNull() and provides a fallback. This inconsistency could lead to bugs. Consider either: (1) moving this function to a shared utility class, or (2) removing it entirely and using the ViewModel's logic directly, or (3) ensuring both implementations handle edge cases identically (e.g., what if gitUrl is empty or doesn't contain a '/'?).

Copilot uses AI. Check for mistakes.
remoteAgentViewModel = IdeaRemoteAgentViewModel(
project = project,
coroutineScope = coroutineScope,
serverUrl = "http://localhost:8080"
Copy link

Copilot AI Dec 1, 2025

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 from ConfigManager or providing a UI setting to allow users to change the server URL without modifying code.

Copilot uses AI. Check for mistakes.
Comment on lines +225 to +231
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")
}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The validation logic here only checks if effectiveProjectId is not blank, but doesn't validate whether it's a valid project ID or Git URL format. Additionally, the getEffectiveProjectId function can throw an exception (as noted in another comment). Consider adding more robust validation before calling executeTask, such as checking if the project exists in the available projects list, or validating the Git URL format with a regex pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +156
// Sync server URL state to callback
LaunchedEffect(Unit) {
snapshotFlow { serverUrlState.text.toString() }
.distinctUntilChanged()
.collect { onServerUrlChange(it) }
}

// Sync project ID state to callback
LaunchedEffect(Unit) {
snapshotFlow { projectIdState.text.toString() }
.distinctUntilChanged()
.collect { onProjectIdChange(it) }
}

// Sync git URL state to callback
LaunchedEffect(Unit) {
snapshotFlow { gitUrlState.text.toString() }
.distinctUntilChanged()
.collect { onGitUrlChange(it) }
}

// Sync external changes to text field states
LaunchedEffect(serverUrl) {
if (serverUrlState.text.toString() != serverUrl) {
serverUrlState.setTextAndPlaceCursorAtEnd(serverUrl)
}
}
LaunchedEffect(projectId) {
if (projectIdState.text.toString() != projectId) {
projectIdState.setTextAndPlaceCursorAtEnd(projectId)
}
}
LaunchedEffect(gitUrl) {
if (gitUrlState.text.toString() != gitUrl) {
gitUrlState.setTextAndPlaceCursorAtEnd(gitUrl)
}
}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Potential infinite loop or update cycle: Multiple LaunchedEffect blocks are creating bidirectional synchronization between the TextFieldState and the parent state. When a user types in the TextField, it triggers the snapshotFlow collectors (lines 121-138) which update the parent state via callbacks. The parent state changes then trigger the LaunchedEffect blocks (lines 142-156) which try to update the TextFieldState back. This could cause update cycles. While the if checks help prevent some updates, this pattern is fragile. Consider using a unidirectional data flow pattern or using rememberUpdatedState to avoid stale closures.

Copilot uses AI. Check for mistakes.
Comment on lines +274 to +280
fun testLLMConfigSerialization() {
val config = LLMConfig(
provider = "OpenAI",
modelName = "gpt-4",
apiKey = "test-key",
baseUrl = "https://api.openai.com"
)
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Security concern: The API key is being logged or stored in plaintext in test code (line 278). While this is a test file with a dummy key, it sets a poor precedent. In production code, API keys from LLMConfig are transmitted to the remote server (see IdeaRemoteAgentViewModel.kt line 114-119). Ensure that: (1) API keys are transmitted over HTTPS only, (2) They're not logged in production code, and (3) Consider adding a comment in tests that no real API keys should be used.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +141
if (event is RemoteAgentEvent.Complete) {
_isExecuting.value = false
currentExecutionJob = null
}
}

} catch (e: CancellationException) {
renderer.forceStop()
renderer.renderError("Task cancelled by user")
_isExecuting.value = false
currentExecutionJob = null
} catch (e: Exception) {
renderer.renderError(e.message ?: "Unknown error")
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Potential race condition: The _isExecuting flag is set to false inside the collect block when a Complete event is received (line 130), but it's also set in the catch blocks (lines 138, 142). If an exception occurs after the Complete event but before the coroutine finishes, _isExecuting will be set to false twice. More importantly, if the job is cancelled between checking _isExecuting and setting it, this could lead to inconsistent state. Consider using a mutex or atomic operations, or restructure the code to ensure _isExecuting is always managed in a single place (e.g., using try-finally).

Suggested change
if (event is RemoteAgentEvent.Complete) {
_isExecuting.value = false
currentExecutionJob = null
}
}
} catch (e: CancellationException) {
renderer.forceStop()
renderer.renderError("Task cancelled by user")
_isExecuting.value = false
currentExecutionJob = null
} catch (e: Exception) {
renderer.renderError(e.message ?: "Unknown error")
// No need to set _isExecuting or currentExecutionJob here; handled in finally
// if (event is RemoteAgentEvent.Complete) {
// _isExecuting.value = false
// currentExecutionJob = null
// }
} catch (e: CancellationException) {
renderer.forceStop()
renderer.renderError("Task cancelled by user")
// _isExecuting.value = false
// currentExecutionJob = null
} catch (e: Exception) {
renderer.renderError(e.message ?: "Unknown error")
// _isExecuting.value = false
// currentExecutionJob = null
} finally {

Copilot uses AI. Check for mistakes.
Comment on lines 148 to 172
private fun buildRequest(
projectId: String,
task: String,
gitUrl: String,
llmConfig: LLMConfig?
): RemoteAgentRequest {
return if (gitUrl.isNotBlank()) {
RemoteAgentRequest(
projectId = gitUrl.split('/').lastOrNull()?.removeSuffix(".git") ?: "temp-project",
task = task,
llmConfig = llmConfig,
gitUrl = gitUrl
)
} else {
val isGitUrl = projectId.startsWith("http://") ||
projectId.startsWith("https://") ||
projectId.startsWith("git@")

if (isGitUrl) {
RemoteAgentRequest(
projectId = projectId.split('/').lastOrNull()?.removeSuffix(".git") ?: "temp-project",
task = task,
llmConfig = llmConfig,
gitUrl = projectId
)
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Duplicated logic: The code for extracting project ID from a git URL (gitUrl.split('/').lastOrNull()?.removeSuffix(".git") ?: "temp-project") is duplicated in lines 156 and 168. This logic should be extracted into a private helper function to improve maintainability and reduce the risk of inconsistencies if this logic needs to be changed in the future.

Copilot uses AI. Check for mistakes.
*/
fun getEffectiveProjectId(projectId: String, gitUrl: String): String {
return if (gitUrl.isNotBlank()) {
gitUrl.split('/').last().removeSuffix(".git")
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Potential crash: The function uses .last() instead of .lastOrNull(), which will throw a NoSuchElementException if the gitUrl doesn't contain a '/'. This can happen if the user enters an invalid Git URL. Change to: gitUrl.split('/').lastOrNull()?.removeSuffix(".git") ?: projectId to safely handle edge cases.

Suggested change
gitUrl.split('/').last().removeSuffix(".git")
gitUrl.split('/').lastOrNull()?.removeSuffix(".git") ?: projectId

Copilot uses AI. Check for mistakes.
Comment on lines 29 to 309
class IdeaRemoteAgentViewModelTest {

private lateinit var testScope: CoroutineScope

@BeforeEach
fun setUp() {
testScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
}

@AfterEach
fun tearDown() {
testScope.cancel()
}

@Test
fun testInitialState() = runBlocking {
// Create a mock project-free test by testing the renderer and state directly
// We can't easily test the full ViewModel without IntelliJ Platform,
// but we can test the renderer and state management
val renderer = JewelRenderer()

// Verify initial renderer state
val timeline = renderer.timeline.first()
assertTrue(timeline.isEmpty())

val isProcessing = renderer.isProcessing.first()
assertFalse(isProcessing)

val errorMessage = renderer.errorMessage.first()
assertNull(errorMessage)
}

@Test
fun testRendererHandlesIterationEvent() = runBlocking {
val renderer = JewelRenderer()

// Simulate handling iteration event
renderer.renderIterationHeader(3, 10)

val currentIteration = renderer.currentIteration.first()
assertEquals(3, currentIteration)

val maxIterations = renderer.maxIterations.first()
assertEquals(10, maxIterations)
}

@Test
fun testRendererHandlesLLMChunkEvent() = runBlocking {
val renderer = JewelRenderer()

// Simulate LLM streaming
renderer.renderLLMResponseStart()
assertTrue(renderer.isProcessing.first())

renderer.renderLLMResponseChunk("Hello ")
renderer.renderLLMResponseChunk("world!")

val streamingOutput = renderer.currentStreamingOutput.first()
assertTrue(streamingOutput.contains("Hello"))
assertTrue(streamingOutput.contains("world"))

renderer.renderLLMResponseEnd()
assertFalse(renderer.isProcessing.first())
}

@Test
fun testRendererHandlesToolCallEvent() = runBlocking {
val renderer = JewelRenderer()

// Simulate tool call
renderer.renderToolCall("read-file", "path=\"/test/file.txt\"")

val currentToolCall = renderer.currentToolCall.first()
assertNotNull(currentToolCall)

val timeline = renderer.timeline.first()
assertEquals(1, timeline.size)
assertTrue(timeline.first() is JewelRenderer.TimelineItem.ToolCallItem)
}

@Test
fun testRendererHandlesToolResultEvent() = runBlocking {
val renderer = JewelRenderer()

// Simulate tool call and result
renderer.renderToolCall("read-file", "path=\"/test/file.txt\"")
renderer.renderToolResult(
toolName = "read-file",
success = true,
output = "File content",
fullOutput = "Full file content",
metadata = emptyMap()
)

val currentToolCall = renderer.currentToolCall.first()
assertNull(currentToolCall)

val timeline = renderer.timeline.first()
assertEquals(1, timeline.size)

val toolItem = timeline.first() as JewelRenderer.TimelineItem.ToolCallItem
assertEquals(true, toolItem.success)
}

@Test
fun testRendererHandlesErrorEvent() = runBlocking {
val renderer = JewelRenderer()

renderer.renderError("Connection failed")

val errorMessage = renderer.errorMessage.first()
assertEquals("Connection failed", errorMessage)

val timeline = renderer.timeline.first()
assertEquals(1, timeline.size)
assertTrue(timeline.first() is JewelRenderer.TimelineItem.ErrorItem)
}

@Test
fun testRendererHandlesCompleteEvent() = runBlocking {
val renderer = JewelRenderer()

renderer.renderFinalResult(true, "Task completed", 5)

val timeline = renderer.timeline.first()
assertEquals(1, timeline.size)

val item = timeline.first() as JewelRenderer.TimelineItem.TaskCompleteItem
assertTrue(item.success)
assertEquals("Task completed", item.message)
assertEquals(5, item.iterations)
}

@Test
fun testRendererClearTimeline() = runBlocking {
val renderer = JewelRenderer()

// Add some items
renderer.addUserMessage("User message")
renderer.renderError("An error")

var timeline = renderer.timeline.first()
assertEquals(2, timeline.size)

// Clear timeline
renderer.clearTimeline()

timeline = renderer.timeline.first()
assertTrue(timeline.isEmpty())

val errorMessage = renderer.errorMessage.first()
assertNull(errorMessage)
}

@Test
fun testRendererForceStop() = runBlocking {
val renderer = JewelRenderer()

// Start streaming
renderer.renderLLMResponseStart()
renderer.renderLLMResponseChunk("Partial output")

assertTrue(renderer.isProcessing.first())

// Force stop
renderer.forceStop()

assertFalse(renderer.isProcessing.first())

// Verify interrupted message was added
val timeline = renderer.timeline.first()
assertTrue(timeline.isNotEmpty())
val lastItem = timeline.last()
assertTrue(lastItem is JewelRenderer.TimelineItem.MessageItem)
assertTrue((lastItem as JewelRenderer.TimelineItem.MessageItem).content.contains("[Interrupted]"))
}

@Test
fun testRendererClearError() = runBlocking {
val renderer = JewelRenderer()

// Set error
renderer.renderError("Test error")
assertEquals("Test error", renderer.errorMessage.first())

// Clear error
renderer.clearError()
assertNull(renderer.errorMessage.first())
}

@Test
fun testRendererAddUserMessage() = runBlocking {
val renderer = JewelRenderer()

renderer.addUserMessage("Hello from user")

val timeline = renderer.timeline.first()
assertEquals(1, timeline.size)

val item = timeline.first() as JewelRenderer.TimelineItem.MessageItem
assertEquals(JewelRenderer.MessageRole.USER, item.role)
assertEquals("Hello from user", item.content)
}

@Test
fun testRemoteAgentRequestBuilder() {
// Test the request building logic
val projectId = "test-project"
val task = "Fix the bug"
val gitUrl = ""

// When gitUrl is empty, should use projectId
val request = RemoteAgentRequest(
projectId = projectId,
task = task,
llmConfig = null,
gitUrl = if (gitUrl.isNotBlank()) gitUrl else null
)

assertEquals("test-project", request.projectId)
assertEquals("Fix the bug", request.task)
assertNull(request.gitUrl)
}

@Test
fun testRemoteAgentRequestWithGitUrl() {
// Test the request building logic with git URL
val gitUrl = "https://github.com/user/repo.git"
val task = "Fix the bug"

val projectId = gitUrl.split('/').lastOrNull()?.removeSuffix(".git") ?: "temp-project"

val request = RemoteAgentRequest(
projectId = projectId,
task = task,
llmConfig = null,
gitUrl = gitUrl
)

assertEquals("repo", request.projectId)
assertEquals("Fix the bug", request.task)
assertEquals(gitUrl, request.gitUrl)
}

@Test
fun testLLMConfigSerialization() {
val config = LLMConfig(
provider = "OpenAI",
modelName = "gpt-4",
apiKey = "test-key",
baseUrl = "https://api.openai.com"
)

assertEquals("OpenAI", config.provider)
assertEquals("gpt-4", config.modelName)
assertEquals("test-key", config.apiKey)
assertEquals("https://api.openai.com", config.baseUrl)
}

@Test
fun testHealthResponseParsing() {
val response = HealthResponse(status = "ok")
assertEquals("ok", response.status)
}

@Test
fun testProjectInfoParsing() {
val project = ProjectInfo(
id = "proj-1",
name = "My Project",
path = "/path/to/project",
description = "A test project"
)

assertEquals("proj-1", project.id)
assertEquals("My Project", project.name)
assertEquals("/path/to/project", project.path)
assertEquals("A test project", project.description)
}
}

Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing test coverage for the IdeaRemoteAgentViewModel class itself. The test file only tests the JewelRenderer and data classes, but doesn't test the actual ViewModel methods like updateServerUrl(), checkConnection(), executeTask(), cancelTask(), etc. While these methods may require mocking the HTTP client, at minimum you should test the state management logic (e.g., verify that updateServerUrl properly updates the server URL, resets connection state, and recreates the client).

Copilot uses AI. Check for mistakes.
maxReconnectionAttempts = 3
}

expectSuccess = false
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The expectSuccess = false setting in the HTTP client configuration is concerning. This means that HTTP error responses (4xx, 5xx) won't throw exceptions automatically, which could lead to silent failures. Consider removing this line to enable proper error handling, or add explicit error checking in the response handling code. If there's a specific reason to keep this, it should be documented with a comment explaining why.

Suggested change
expectSuccess = false

Copilot uses AI. Check for mistakes.
- Handle trailing slashes in Git URLs when extracting project ID
- Add HTTP response status validation in healthCheck() and getProjects()
- Add comment explaining expectSuccess = false for SSE connections
- Add comprehensive tests for getEffectiveProjectId edge cases
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1)

29-232: Tests focus on JewelRenderer rather than ViewModel behavior.

This duplicates a previous review comment. The tests validate JewelRenderer functionality but don't test IdeaRemoteAgentViewModel methods like updateServerUrl(), checkConnection(), cancelTask(), or state transitions. While full integration tests require mocking the HTTP client, you could add unit tests for:

  • updateServerUrl resets connection state
  • cancelTask sets isExecuting to false
  • Initial state of isConnected, connectionError, etc.

Consider adding a separate test class with a mocked IdeaRemoteAgentClient for more comprehensive coverage.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1)

103-103: Replace printStackTrace() with IntelliJ's logging framework.

This duplicates a previous review comment. Use com.intellij.openapi.diagnostic.Logger for consistent error tracking in IntelliJ plugins.

+    private val LOG = com.intellij.openapi.diagnostic.Logger.getInstance(IdeaRemoteAgentClient::class.java)
+
     fun executeStream(request: RemoteAgentRequest): Flow<RemoteAgentEvent> = flow {
         try {
             // ...
         } catch (e: Exception) {
-            e.printStackTrace()
+            LOG.error("Stream connection failed", e)
             throw RemoteAgentException("Stream connection failed: ${e.message}", e)
         }
     }
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt (2)

88-91: Replace println with IntelliJ's logging framework.

This duplicates a previous review comment. Use Logger.getInstance() for consistent logging.

+    private val LOG = com.intellij.openapi.diagnostic.Logger.getInstance(IdeaRemoteAgentViewModel::class.java)
+
     fun executeTask(projectId: String, task: String, gitUrl: String = "") {
         if (_isExecuting.value) {
-            println("Agent is already executing")
+            LOG.warn("Agent is already executing")
             return
         }

135-144: Catching CancellationException prevents proper coroutine cancellation propagation.

This duplicates a previous review comment. CancellationException should be re-thrown to maintain structured concurrency. Consider using a finally block for cleanup:

             client.executeStream(request).collect { event ->
                 handleRemoteEvent(event)
-
-                if (event is RemoteAgentEvent.Complete) {
-                    _isExecuting.value = false
-                    currentExecutionJob = null
-                }
             }
-
-            } catch (e: CancellationException) {
-                renderer.forceStop()
-                renderer.renderError("Task cancelled by user")
-                _isExecuting.value = false
-                currentExecutionJob = null
             } catch (e: Exception) {
+                if (e is CancellationException) {
+                    renderer.forceStop()
+                    throw e  // Re-throw to maintain structured concurrency
+                }
                 renderer.renderError(e.message ?: "Unknown error")
+            } finally {
                 _isExecuting.value = false
                 currentExecutionJob = null
             }
🧹 Nitpick comments (3)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1)

116-125: Consider adding @SerialName annotations for API stability.

If the server API uses different JSON field names (e.g., git_url vs gitUrl), deserialization will fail silently with ignoreUnknownKeys = true. Adding explicit @SerialName annotations documents the contract and prevents issues if field naming conventions differ.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1)

48-54: Connection check triggered on every server URL change may cause excessive API calls.

The LaunchedEffect(serverUrl) block calls viewModel.checkConnection() whenever serverUrl changes, including during typing. This could result in many failed health check requests as the user types the URL character by character.

Consider debouncing the connection check or only triggering it via the "Connect" button:

-    // Check connection on initial load and when server URL changes
-    LaunchedEffect(serverUrl) {
-        if (serverUrl.isNotBlank()) {
-            viewModel.updateServerUrl(serverUrl)
-            viewModel.checkConnection()
-        }
-    }
+    // Only update the server URL; let user trigger connection via Connect button
+    LaunchedEffect(serverUrl) {
+        if (serverUrl.isNotBlank()) {
+            viewModel.updateServerUrl(serverUrl)
+        }
+    }

Alternatively, add debouncing if auto-connect is desired.

mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1)

253-271: Test duplicates request building logic instead of testing the ViewModel's buildRequest method.

The test manually constructs the project ID extraction logic (gitUrl.split('/').lastOrNull()?.removeSuffix(".git")) rather than testing the actual buildRequest method in IdeaRemoteAgentViewModel. This means the test could pass even if the ViewModel's logic differs.

Since buildRequest is private, consider either:

  1. Making it internal for testing, or
  2. Testing through the public executeTask API with a mocked client
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbddbc8 and 2218c97.

📒 Files selected for processing (4)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt (1 hunks)
  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

Use expect/actual for platform-specific code in KMP projects (e.g., file I/O on JVM/JS/Wasm)

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt
  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt
🧠 Learnings (1)
📚 Learning: 2025-11-30T02:30:49.805Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-30T02:30:49.805Z
Learning: Use `cd mpp-idea && ../gradlew test --tests "cc.unitmesh.devins.idea.renderer.JewelRendererTest"` for running standalone JUnit 5 tests that do not require IntelliJ Platform

Applied to files:

  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt
🧬 Code graph analysis (2)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/timeline/IdeaTimelineContent.kt (1)
  • IdeaTimelineContent (20-48)
mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (2)
mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/test/DevInsTestCase.kt (5)
  • assertTrue (29-33)
  • assertFalse (38-40)
  • assertNull (64-68)
  • assertEquals (45-49)
  • assertNotNull (54-59)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1)
  • getEffectiveProjectId (312-323)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build (223)
  • GitHub Check: Build (241)
🔇 Additional comments (4)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentContent.kt (1)

312-323: LGTM! Edge cases for Git URL parsing are now properly handled.

The implementation correctly handles trailing slashes, empty segments, and falls back to projectId when extraction fails. The tests in IdeaRemoteAgentViewModelTest.kt provide good coverage for these edge cases.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModel.kt (2)

186-192: Good refactor: Centralized URL parsing logic.

The extractProjectIdFromUrl helper properly handles trailing slashes and empty segments, addressing the previous code duplication concerns. This is now consistent with getEffectiveProjectId in IdeaRemoteAgentContent.kt.


284-287: The dispose method correctly handles cleanup for this ViewModel.

The coroutineScope is passed as an external parameter from the caller (IdeaAgentApp) and is not owned by the ViewModel. Cancelling it in dispose() would be inappropriate since the scope lifecycle is managed externally and may be used by other components. The current cleanup—cancelling currentExecutionJob and closing client—is correct.

However, note that checkConnection() at line 67 launches a coroutine that is not tracked in a variable. If this becomes a concern for resource cleanup, consider tracking it similarly to currentExecutionJob.

Likely an incorrect or invalid review comment.

mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1)

311-359: Good coverage of getEffectiveProjectId edge cases.

The tests thoroughly cover various edge cases including trailing slashes, multiple slashes, empty URLs, blank URLs, and .git suffix handling. This provides confidence that the URL parsing logic is robust.

- Add HTTP status validation in executeStream before processing SSE events
- Add comprehensive tests for data classes and state management
- Test RemoteAgentRequest with all fields and default values
- Test LLMConfig with null baseUrl
- Test ProjectListResponse with empty and multiple projects
- Test RemoteAgentException with and without cause
- Test getEffectiveProjectId with SSH URLs and deep paths
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (2)

274-286: Document that test API keys should never be real secrets.

While using hardcoded test keys like "test-key" is acceptable in tests, consider adding a comment to establish best practices: (1) never use real API keys in tests, (2) ensure production code transmits API keys over HTTPS only, (3) verify API keys aren't logged in production.

Add a comment at the top of the test class:

 /**
  * Unit tests for IdeaRemoteAgentViewModel.
  *
+ * Note: All API keys in these tests are dummy values.
+ * Production code must never log API keys and should only transmit them over HTTPS.
+ *
  * Tests the ViewModel's functionality including:

Also applies to: 382-387, 409-421


15-28: Test class doesn't match its name and documentation.

The class is named IdeaRemoteAgentViewModelTest and documented as "Unit tests for IdeaRemoteAgentViewModel," but it only tests JewelRenderer and data classes—not the actual ViewModel methods like updateServerUrl(), checkConnection(), executeTask(), etc.

Consider either:

  1. Renaming to reflect actual scope: IdeaRemoteAgentComponentsTest or JewelRendererAndDataClassesTest
  2. Adding actual ViewModel tests (may require mocking the HTTP client)

Misleading test class names create maintenance burden and false confidence in test coverage.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1)

107-109: Replace printStackTrace() with IntelliJ Platform Logger.

Using printStackTrace() writes directly to stderr and isn't integrated with the IDE's logging infrastructure. IntelliJ plugins should use com.intellij.openapi.diagnostic.Logger for consistent error tracking and debugging.

Apply this diff:

+import com.intellij.openapi.diagnostic.Logger
+
 class IdeaRemoteAgentClient(
     private val baseUrl: String = "http://localhost:8080"
 ) {
+    companion object {
+        private val LOG = Logger.getInstance(IdeaRemoteAgentClient::class.java)
+    }
+
     private val httpClient: HttpClient = HttpClient(CIO) {
         } catch (e: Exception) {
-            e.printStackTrace()
+            LOG.error("Stream connection failed", e)
             throw RemoteAgentException("Stream connection failed: ${e.message}", e)
         }
🧹 Nitpick comments (1)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1)

23-46: Consider implementing Closeable for idiomatic resource management.

The HttpClient is created in the constructor and has a close() method, but the class doesn't implement Closeable or AutoCloseable. Implementing Closeable would enable use of Kotlin's .use {} pattern and make resource cleanup more explicit.

Apply this diff:

-class IdeaRemoteAgentClient(
+class IdeaRemoteAgentClient(
     private val baseUrl: String = "http://localhost:8080"
-) {
+) : Closeable {

Then the close method becomes an override:

-    fun close() {
+    override fun close() {
         httpClient.close()
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2218c97 and e8d68f6.

📒 Files selected for processing (2)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt (1 hunks)
  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

Use expect/actual for platform-specific code in KMP projects (e.g., file I/O on JVM/JS/Wasm)

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt
  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt
🧠 Learnings (1)
📚 Learning: 2025-11-30T02:30:49.805Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-30T02:30:49.805Z
Learning: Use `cd mpp-idea && ../gradlew test --tests "cc.unitmesh.devins.idea.renderer.JewelRendererTest"` for running standalone JUnit 5 tests that do not require IntelliJ Platform

Applied to files:

  • mpp-idea/src/test/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentViewModelTest.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build (223)
  • GitHub Check: Build (241)

@phodal phodal merged commit 22757ff into master Dec 1, 2025
5 of 6 checks passed
@phodal phodal deleted the feat/mpp-idea-remote-agent branch December 2, 2025 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments