Skip to content
Closed
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
9 changes: 8 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
dokka = "1.9.10"
kotlin = "2.2.0"
changelog = "2.2.1"
gradleIntelliJPlugin = "2.1.0"
gradleIntelliJPlugin = "2.10.2"
qodana = "0.1.13"
kover = "0.7.5"
compose = "1.9.2"
Expand All @@ -12,6 +12,10 @@ ktor = "3.3.0"
chapi = "2.1.2"
jtokkit = "1.1.0"

# test libraries
junit = "4.13.2"
opentest4j = "1.3.0"

[libraries]
jtokkit = { module = "com.knuddels:jtokkit", version.ref = "jtokkit" }

Expand All @@ -23,6 +27,9 @@ ktor-serverCors = { module = "io.ktor:ktor-server-cors-jvm", version.ref = "ktor
ktor-serverSse = { module = "io.ktor:ktor-server-sse-jvm", version.ref = "ktor" }
ktor-serverTestHost = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor" }

junit = { group = "junit", name = "junit", version.ref = "junit" }
opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" }

[plugins]
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }
compose = { id = "org.jetbrains.compose", version.ref = "compose" }
Expand Down
83 changes: 83 additions & 0 deletions mpp-idea/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import org.jetbrains.intellij.platform.gradle.TestFrameworkType

plugins {
id("java")
alias(libs.plugins.kotlin)
alias(libs.plugins.gradleIntelliJPlugin)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.serialization)
}

group = "cc.unitmesh.devins"
version = project.findProperty("mppVersion") as String? ?: "0.3.2"

kotlin {
jvmToolchain(21)

compilerOptions {
freeCompilerArgs.addAll(
listOf(
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
)
)
}
}

repositories {
mavenCentral()

intellijPlatform {
defaultRepositories()
}
google()
}

dependencies {
// Kotlinx serialization for JSON
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")

testImplementation(kotlin("test"))
testImplementation(libs.junit)
testImplementation(libs.opentest4j)
Comment on lines +40 to +42
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's read the mpp-idea/build.gradle.kts file to understand the test setup
cat -n mpp-idea/build.gradle.kts | head -100

Repository: unit-mesh/auto-dev

Length of output: 2587


🏁 Script executed:

#!/bin/bash
# Check around line 80 for useJUnitPlatform context
sed -n '70,90p' mpp-idea/build.gradle.kts

Repository: unit-mesh/auto-dev

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Search for test-related configurations in the build file
rg -n "test|JUnit|Platform" mpp-idea/build.gradle.kts | head -30

Repository: unit-mesh/auto-dev

Length of output: 355


🏁 Script executed:

#!/bin/bash
# Find and read the libs.versions.toml or similar to see junit and opentest4j versions
find . -name "libs.versions.toml" -o -name "gradle.properties" | head -5

Repository: unit-mesh/auto-dev

Length of output: 109


🏁 Script executed:

#!/bin/bash
# Search for junit and opentest4j definitions in the gradle catalog
rg -n "junit|opentest4j" . --type toml | head -20

Repository: unit-mesh/auto-dev

Length of output: 381


🏁 Script executed:

#!/bin/bash
# Check what TestFrameworkType.Platform entails by searching for its usage
rg -n "TestFrameworkType" . --type gradle --type kts | head -20

Repository: unit-mesh/auto-dev

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Check what kotlin("test") typically includes
cat gradle/libs.versions.toml | grep -A 5 -B 5 "kotlin"

Repository: unit-mesh/auto-dev

Length of output: 2029


🏁 Script executed:

#!/bin/bash
# Search for any test configuration or comments in the build files
rg -n "JUnit|test|Platform" mpp-idea/ --type gradle --type toml | grep -v Binary

Repository: unit-mesh/auto-dev

Length of output: 44


🌐 Web query:

opentest4j 1.3.0 purpose JUnit compatibility

💡 Result:

opentest4j 1.3.0 purpose and JUnit compatibility — short answer

  • Purpose: OpenTest4J provides a minimal, framework‑agnostic foundation of test exception types (e.g., AssertionFailedError, TestAbortedException) so testing frameworks, assertion libraries, IDEs and build tools can interoperate and report test results consistently. [1][2]

  • JUnit compatibility: JUnit 5 (and its APIs) use OpenTest4J exception types (for example assumption failures/aborts use org.opentest4j.TestAbortedException and assertion failures map to AssertionFailedError), so projects using JUnit depend on/openly interoperate with opentest4j (including the 1.3.0 release). [2][3]

Sources

  • OpenTest4J project description. [1]
  • OpenTest4J 1.3.0 API docs (AssertionFailedError, TestAbortedException). [2]
  • Example references showing JUnit methods throwing OpenTest4J exceptions. [3]

🏁 Script executed:

#!/bin/bash
# Search for documentation or comments about the test setup in the repository
rg -n "test|Test" mpp-idea/build.gradle.kts -A 2 -B 2

Repository: unit-mesh/auto-dev

Length of output: 538


🏁 Script executed:

#!/bin/bash
# Check if there are other test files or configurations that show how tests are actually run
find mpp-idea -name "*.kt" -path "*/test/*" | head -10

Repository: unit-mesh/auto-dev

Length of output: 44


🌐 Web query:

kotlin("test") Kotlin 2.2.0 JUnit dependencies transitive

💡 Result:

Short answer: kotlin("test") (Kotlin 2.2.0) is the multiplatform kotlin-test umbrella — it does not force a specific JUnit implementation by itself. The concrete JVM adapters are provided by separate artifacts (kotlin-test-junit for JUnit 4 and kotlin-test-junit5 for JUnit 5); those adapter modules are the ones that bring JUnit transitively. [1][2][3]

Notes / sources:

  • kotlin.test modules and platform-specific adapters (kotlin-test-junit, kotlin-test-junit5). [1][2]
  • Gradle/compatibility note: mixing multiple kotlin-test JVM variants brought transitively can cause conflicts; choose or exclude the adapter you want. [3]
  • Kotlin Test 2.2.0 artifact listing. [4]

If you want, I can show the exact transitive coordinates (group:artifact:version) that kotlin-test-junit and kotlin-test-junit5 bring for Kotlin 2.2.0. References:
[1] https://kotlinlang.org/api/core/kotlin-test/index.html
[2] https://kotlinlang.org/api/core/kotlin-test/kotlin.test/-test/
[3] https://kotlinlang.org/docs/compatibility-guide-15.html
[4] https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-test/2.2.0


🌐 Web query:

IntelliJ Platform TestFrameworkType.Platform useJUnitPlatform() compatibility

💡 Result:

Short answer

  • TestFrameworkType.Platform is the "classic" IntelliJ Platform test-framework and is wired for the platform’s legacy test setup (JUnit4/JUnit3-style BasePlatformTestCase). It is not guaranteed to be JUnit‑Platform (JUnit‑5) ready out of the box — there are known gaps (missing opentest4j, references to JUnit4) and compatibility issues when mixing Platform tests with useJUnitPlatform(). (plugins.jetbrains.com)

  • If you need JUnit‑5 (useJUnitPlatform()), use the Starter/JUnit5 tooling (TestFrameworkType.Starter / integration test starter) or explicitly add the JUnit‑Jupiter and platform launcher/vintage artifacts to the test classpath. The Starter framework is explicitly JUnit‑5 first and examples use useJUnitPlatform(). (plugins.jetbrains.com)

Practical guidance

  • For pure platform unit tests that extend BasePlatformTestCase, keep using TestFrameworkType.Platform with JUnit4 (don’t switch test task to useJUnitPlatform() unless you add vintage/bridge engines and fix missing deps). (plugins.jetbrains.com)
  • For JUnit‑5 tests, prefer TestFrameworkType.Starter (or add junit-jupiter + junit‑platform‑launcher + junit‑vintage and opentest4j manually) and configure the Gradle Test task with useJUnitPlatform(). Example integration test usage shows useJUnitPlatform() together with Starter and junit‑jupiter. (plugins.jetbrains.com)

If you want, tell me: which IntelliJ Platform Gradle Plugin version and which test classes you’re running (BasePlatformTestCase vs JUnit‑5) and I’ll give exact Gradle snippets to make useJUnitPlatform() work or a migration path.

Citations:


Based on my verification, the review comment is valid and identifies a real configuration issue. Here's what I found:

Key findings:

  1. kotlin("test") (Kotlin 2.2.0) is an umbrella that does NOT transitively bring any JUnit implementation—it requires either kotlin-test-junit (for JUnit 4) or kotlin-test-junit5 (for JUnit 5) to work.
  2. testFramework(TestFrameworkType.Platform) is IntelliJ's legacy test framework designed for JUnit 4-style tests (BasePlatformTestCase).
  3. useJUnitPlatform() is specifically for JUnit 5 (Jupiter/Platform) and is incompatible with TestFrameworkType.Platform without additional bridge/vintage engines.
  4. opentest4j is a JUnit 5 interoperability library and shouldn't be used with pure JUnit 4.

The current configuration is mismatched and incomplete. Either use the IntelliJ Platform test framework (keep TestFrameworkType.Platform, remove useJUnitPlatform(), add kotlin-test-junit) OR migrate to JUnit 5 (add kotlin-test-junit5 and JUnit 5 dependencies, optionally switch to TestFrameworkType.Starter).


Clarify JUnit 4/5 test framework setup — configuration mismatch detected.

The build declares testFramework(TestFrameworkType.Platform) (line 60, IntelliJ's legacy JUnit 4 framework) but the test task uses useJUnitPlatform() (line 80, JUnit 5/Jupiter). These are incompatible without additional bridge engines. Additionally:

  • kotlin("test") does not automatically provide JUnit; you need either kotlin-test-junit (JUnit 4) or kotlin-test-junit5 (JUnit 5).
  • opentest4j is a JUnit 5 interoperability library and conflicts with pure JUnit 4 setup.

Choose one path: Either remove useJUnitPlatform() and add kotlin-test-junit for JUnit 4, or add kotlin-test-junit5 and JUnit 5 dependencies (junit-jupiter, junit-platform-launcher) and optionally switch testFramework(TestFrameworkType.Starter) for JUnit 5 support.

🤖 Prompt for AI Agents
In mpp-idea/build.gradle.kts around lines 40 to 42, the test dependencies and
test runner are mismatched: kotlin("test") does not bring a JUnit implementation
and you either must configure JUnit 4 or JUnit 5 consistently. Fix by choosing
one path and updating dependencies and test task: for JUnit 4 keep
testFramework(TestFrameworkType.Platform), remove useJUnitPlatform(), replace
kotlin("test") with/add kotlin("test-junit") and remove opentest4j; OR migrate
to JUnit 5 by adding kotlin("test-junit5") plus junit-jupiter (and
junit-platform-launcher as needed), keep useJUnitPlatform(), remove
TestFrameworkType.Platform (or switch to Starter), and remove opentest4j only if
not needed separately.


intellijPlatform {
// Target IntelliJ IDEA 2025.2+ for Compose support
create("IC", "2025.2.1")

bundledPlugins("com.intellij.java")

// Compose support dependencies (bundled in IDEA 252+)
bundledModules(
"intellij.libraries.skiko",
"intellij.libraries.compose.foundation.desktop",
"intellij.platform.jewel.foundation",
"intellij.platform.jewel.ui",
"intellij.platform.jewel.ideLafBridge",
"intellij.platform.compose"
)

testFramework(TestFrameworkType.Platform)
}
}

intellijPlatform {
pluginConfiguration {
name = "AutoDev Compose UI"
version = project.findProperty("mppVersion") as String? ?: "0.3.2"

ideaVersion {
sinceBuild = "252"
}
}

buildSearchableOptions = false
instrumentCode = false
}

tasks {
test {
useJUnitPlatform()
}
}

23 changes: 23 additions & 0 deletions mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/AutoDevIcons.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package cc.unitmesh.devins.idea

import com.intellij.openapi.util.IconLoader
import javax.swing.Icon

/**
* Icon provider for AutoDev Compose module.
* Icons are loaded from resources for use in toolbars, tool windows, etc.
*/
object AutoDevIcons {
/**
* Tool window icon (13x13 for tool window, 16x16 for actions)
*/
@JvmField
val ToolWindow: Icon = IconLoader.getIcon("/icons/autodev-toolwindow.svg", AutoDevIcons::class.java)

/**
* Main AutoDev icon
*/
@JvmField
val AutoDev: Icon = IconLoader.getIcon("/icons/autodev.svg", AutoDevIcons::class.java)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cc.unitmesh.devins.idea.services

import com.intellij.openapi.components.Service
import com.intellij.openapi.components.Service.Level
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.CoroutineScope

/**
* A service-level class that provides and manages coroutine scopes for a given project.
*
* @constructor Initializes the [CoroutineScopeHolder] with a project-wide coroutine scope.
* @param projectWideCoroutineScope A [CoroutineScope] defining the lifecycle of project-wide coroutines.
*/
@Service(Level.PROJECT)
class CoroutineScopeHolder(private val projectWideCoroutineScope: CoroutineScope) {
/**
* Creates a new coroutine scope as a child of the project-wide coroutine scope with the specified name.
*
* @param name The name for the newly created coroutine scope.
* @return a scope with a Job which parent is the Job of projectWideCoroutineScope scope.
*
* The returned scope can be completed only by cancellation.
* projectWideCoroutineScope scope will cancel the returned scope when canceled.
* If the child scope has a narrower lifecycle than projectWideCoroutineScope scope,
* then it should be canceled explicitly when not needed,
* otherwise, it will continue to live in the Job hierarchy until termination of the CoroutineScopeHolder service.
*/
@Suppress("UnstableApiUsage")
fun createScope(name: String): CoroutineScope = projectWideCoroutineScope.childScope(name)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package cc.unitmesh.devins.idea.toolwindow

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.flow.distinctUntilChanged
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.Orientation
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.theme.defaultBannerStyle

/**
* Main Compose application for AutoDev Chat.
*
* Uses Jewel theme for native IntelliJ IDEA integration.
*/
@Composable
fun AutoDevChatApp(viewModel: AutoDevChatViewModel) {
val messages by viewModel.chatMessages.collectAsState()
val inputState by viewModel.inputState.collectAsState()
val listState = rememberLazyListState()

// Auto-scroll to bottom when new messages arrive
LaunchedEffect(messages.size) {
if (messages.isNotEmpty()) {
listState.animateScrollToItem(messages.lastIndex)
}
}

Column(
modifier = Modifier
.fillMaxSize()
.background(JewelTheme.globalColors.panelBackground)
) {
// Header
ChatHeader(
onNewConversation = { viewModel.onNewConversation() }
)

Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().height(1.dp))

// Message list
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
if (messages.isEmpty()) {
EmptyStateMessage()
} else {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(messages, key = { it.id }) { message ->
MessageBubble(message)
}
}
}
}

Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().height(1.dp))

// Input area
ChatInput(
inputState = inputState,
onInputChanged = { viewModel.onInputChanged(it) },
onSend = { viewModel.onSendMessage() },
onAbort = { viewModel.onAbortMessage() }
)
}
}

@Composable
private fun ChatHeader(
onNewConversation: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "AutoDev Chat",
style = JewelTheme.defaultTextStyle.copy(
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
)

IconButton(onClick = onNewConversation) {
Text("+", style = JewelTheme.defaultTextStyle)
}
}
}

@Composable
private fun EmptyStateMessage() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Start a conversation with your AI Assistant!",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 16.sp,
color = JewelTheme.globalColors.text.info
)
)
}
}

@Composable
private fun MessageBubble(message: ChatMessage) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
) {
Box(
modifier = Modifier
.widthIn(max = 300.dp)
.background(
if (message.isUser)
JewelTheme.defaultBannerStyle.information.colors.background.copy(alpha = 0.75f)
else
JewelTheme.globalColors.panelBackground
)
.padding(8.dp)
) {
Text(
text = message.content,
style = JewelTheme.defaultTextStyle
)
}
}
}
Comment on lines +127 to +150
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

Remove hardcoded alpha value to comply with coding guidelines.

Line 138 uses .copy(alpha = 0.75f), which hardcodes a color property value. As per coding guidelines, colors (including opacity) should use design tokens for consistency.

Consider these alternatives:

  1. Define a proper color token in the theme system for message bubbles
  2. Use an existing Jewel banner style without modification
  3. Create a semantic color in your design system for "user message background"

Example approach:

.background(
    if (message.isUser)
        JewelTheme.defaultBannerStyle.information.colors.background
    else
        JewelTheme.globalColors.panelBackground
)

Based on coding guidelines: DO NOT hardcode colors in .kt files.

🤖 Prompt for AI Agents
In mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/AutoDevChatApp.kt
around lines 127 to 150, the MessageBubble composable hardcodes a color alpha
via .copy(alpha = 0.75f); remove this hardcoded opacity and instead use a design
token or existing theme semantic color for the user message background. Replace
the .copy(...) usage with a reference to a theme color (either add a new
JewelTheme token like userMessageBackground or use
JewelTheme.defaultBannerStyle.information.colors.background or another
appropriate semantic banner style), and if needed add the new token to the theme
definitions so opacity is defined centrally in the design system rather than in
this file.


@Composable
private fun ChatInput(
inputState: MessageInputState,
onInputChanged: (String) -> Unit,
onSend: () -> Unit,
onAbort: () -> Unit
) {
val textFieldState = rememberTextFieldState()
val isSending = inputState is MessageInputState.Sending

LaunchedEffect(Unit) {
snapshotFlow { textFieldState.text.toString() }
.distinctUntilChanged()
.collect { onInputChanged(it) }
}

Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
state = textFieldState,
placeholder = { Text("Type your message...") },
modifier = Modifier
.weight(1f)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.key == Key.Enter && keyEvent.type == KeyEventType.KeyDown && !isSending) {
onSend()
textFieldState.edit { replace(0, length, "") }
true
} else {
false
}
},
enabled = !isSending
)

if (isSending) {
DefaultButton(onClick = onAbort) {
Text("Stop")
}
} else {
DefaultButton(
onClick = {
onSend()
textFieldState.edit { replace(0, length, "") }
},
enabled = inputState is MessageInputState.Enabled
) {
Text("Send")
}
}
}
}

Loading
Loading