-
Notifications
You must be signed in to change notification settings - Fork 480
feat(mpp-idea): add Compose UI module for IntelliJ IDEA 2025.2+ #469
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 |
|---|---|---|
| @@ -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) | ||
|
|
||
| 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() | ||
| } | ||
| } | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove hardcoded alpha value to comply with coding guidelines. Line 138 uses Consider these alternatives:
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 |
||
|
|
||
| @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") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 2587
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 229
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 355
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 109
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 381
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 89
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 2029
🏁 Script executed:
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
🏁 Script executed:
Repository: unit-mesh/auto-dev
Length of output: 538
🏁 Script executed:
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:
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
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:
kotlin("test")(Kotlin 2.2.0) is an umbrella that does NOT transitively bring any JUnit implementation—it requires eitherkotlin-test-junit(for JUnit 4) orkotlin-test-junit5(for JUnit 5) to work.testFramework(TestFrameworkType.Platform)is IntelliJ's legacy test framework designed for JUnit 4-style tests (BasePlatformTestCase).useJUnitPlatform()is specifically for JUnit 5 (Jupiter/Platform) and is incompatible withTestFrameworkType.Platformwithout additional bridge/vintage engines.opentest4jis 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, removeuseJUnitPlatform(), addkotlin-test-junit) OR migrate to JUnit 5 (addkotlin-test-junit5and JUnit 5 dependencies, optionally switch toTestFrameworkType.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 usesuseJUnitPlatform()(line 80, JUnit 5/Jupiter). These are incompatible without additional bridge engines. Additionally:kotlin("test")does not automatically provide JUnit; you need eitherkotlin-test-junit(JUnit 4) orkotlin-test-junit5(JUnit 5).opentest4jis a JUnit 5 interoperability library and conflicts with pure JUnit 4 setup.Choose one path: Either remove
useJUnitPlatform()and addkotlin-test-junitfor JUnit 4, or addkotlin-test-junit5and JUnit 5 dependencies (junit-jupiter, junit-platform-launcher) and optionally switchtestFramework(TestFrameworkType.Starter)for JUnit 5 support.🤖 Prompt for AI Agents