Skip to content

Commit e03337e

Browse files
committed
feat(mpp-idea): implement CodingAgent UI with Jewel theme
- Add IdeaAgentRenderer for timeline state management - TimelineItem sealed class (Message, ToolCall, Error, TaskComplete, TerminalOutput) - TokenInfo and ToolCallInfo data classes - Streaming output and tool call state management - Add IdeaCodingAgentViewModel for agent execution - MessageInputState sealed class - Task execution with cancellation support - Built-in commands (/clear, /help) - Simulated agent execution (pending mpp-core integration) - Add Jewel-themed UI components - CodingAgentPanel: Main panel with header, message list, and input - AgentMessageList: Timeline display with auto-scroll - MessageItems: Message, streaming, task complete, and error displays - ToolCallItems: Tool call and terminal output with expand/collapse - Update AutoDevToolWindowFactory to use CodingAgentPanel - Add unit tests for IdeaAgentRenderer - Update build.gradle.kts with required dependencies This implementation mirrors the mpp-ui CodingAgentViewModel architecture but uses Jewel components for native IntelliJ IDEA integration.
1 parent b7eeeae commit e03337e

File tree

9 files changed

+1196
-8
lines changed

9 files changed

+1196
-8
lines changed

mpp-idea/build.gradle.kts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ kotlin {
1717
compilerOptions {
1818
freeCompilerArgs.addAll(
1919
listOf(
20-
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
20+
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
21+
"-opt-in=org.jetbrains.jewel.foundation.ExperimentalJewelApi"
2122
)
2223
)
2324
}
@@ -30,14 +31,18 @@ repositories {
3031
defaultRepositories()
3132
}
3233
google()
34+
maven("https://packages.jetbrains.team/maven/p/kpm/public/")
3335
}
3436

3537
dependencies {
3638
// Kotlinx serialization for JSON
3739
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
3840
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
41+
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
3942

4043
testImplementation(kotlin("test"))
44+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
45+
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
4146

4247
intellijPlatform {
4348
// Target IntelliJ IDEA 2025.2+ for Compose support
@@ -52,7 +57,9 @@ dependencies {
5257
"intellij.platform.jewel.foundation",
5358
"intellij.platform.jewel.ui",
5459
"intellij.platform.jewel.ideLafBridge",
55-
"intellij.platform.compose"
60+
"intellij.platform.compose",
61+
"intellij.platform.jewel.markdown.core",
62+
"intellij.platform.jewel.markdown.extension.gfmAlerts"
5663
)
5764

5865
testFramework(TestFrameworkType.Platform)
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package cc.unitmesh.devins.idea.agent
2+
3+
import androidx.compose.runtime.*
4+
import kotlinx.datetime.Clock
5+
6+
/**
7+
* Timeline item types for the agent chat interface.
8+
* Simplified version for IntelliJ IDEA integration.
9+
*/
10+
sealed class TimelineItem(val timestamp: Long = Clock.System.now().toEpochMilliseconds()) {
11+
data class MessageItem(
12+
val content: String,
13+
val isUser: Boolean,
14+
val tokenInfo: TokenInfo? = null,
15+
val itemTimestamp: Long = Clock.System.now().toEpochMilliseconds()
16+
) : TimelineItem(itemTimestamp)
17+
18+
data class ToolCallItem(
19+
val toolName: String,
20+
val details: String?,
21+
val fullParams: String? = null,
22+
val success: Boolean? = null, // null means still executing
23+
val summary: String? = null,
24+
val output: String? = null,
25+
val fullOutput: String? = null,
26+
val executionTimeMs: Long? = null,
27+
val itemTimestamp: Long = Clock.System.now().toEpochMilliseconds()
28+
) : TimelineItem(itemTimestamp)
29+
30+
data class ErrorItem(
31+
val error: String,
32+
val itemTimestamp: Long = Clock.System.now().toEpochMilliseconds()
33+
) : TimelineItem(itemTimestamp)
34+
35+
data class TaskCompleteItem(
36+
val success: Boolean,
37+
val message: String,
38+
val itemTimestamp: Long = Clock.System.now().toEpochMilliseconds()
39+
) : TimelineItem(itemTimestamp)
40+
41+
data class TerminalOutputItem(
42+
val command: String,
43+
val output: String,
44+
val exitCode: Int,
45+
val executionTimeMs: Long,
46+
val itemTimestamp: Long = Clock.System.now().toEpochMilliseconds()
47+
) : TimelineItem(itemTimestamp)
48+
}
49+
50+
/**
51+
* Token usage information for LLM responses.
52+
*/
53+
data class TokenInfo(
54+
val inputTokens: Int = 0,
55+
val outputTokens: Int = 0,
56+
val totalTokens: Int = inputTokens + outputTokens
57+
)
58+
59+
/**
60+
* Current tool call information for displaying in-progress operations.
61+
*/
62+
data class ToolCallInfo(
63+
val toolName: String,
64+
val description: String,
65+
val details: String? = null
66+
)
67+
68+
/**
69+
* Renderer for the IDEA CodingAgent interface.
70+
* Manages timeline state and provides reactive updates for Compose UI.
71+
*/
72+
class IdeaAgentRenderer {
73+
private val _timeline = mutableStateListOf<TimelineItem>()
74+
val timeline: List<TimelineItem> = _timeline
75+
76+
var currentStreamingOutput by mutableStateOf("")
77+
private set
78+
79+
var currentToolCall by mutableStateOf<ToolCallInfo?>(null)
80+
private set
81+
82+
var isProcessing by mutableStateOf(false)
83+
private set
84+
85+
var errorMessage by mutableStateOf<String?>(null)
86+
private set
87+
88+
fun addUserMessage(content: String) {
89+
_timeline.add(TimelineItem.MessageItem(content = content, isUser = true))
90+
}
91+
92+
fun addAssistantMessage(content: String, tokenInfo: TokenInfo? = null) {
93+
_timeline.add(TimelineItem.MessageItem(content = content, isUser = false, tokenInfo = tokenInfo))
94+
}
95+
96+
fun startStreaming() {
97+
isProcessing = true
98+
currentStreamingOutput = ""
99+
}
100+
101+
fun appendStreamingContent(content: String) {
102+
currentStreamingOutput += content
103+
}
104+
105+
fun endStreaming() {
106+
if (currentStreamingOutput.isNotBlank()) {
107+
_timeline.add(TimelineItem.MessageItem(content = currentStreamingOutput.trim(), isUser = false))
108+
}
109+
currentStreamingOutput = ""
110+
isProcessing = false
111+
}
112+
113+
fun startToolCall(toolName: String, description: String, details: String? = null) {
114+
currentToolCall = ToolCallInfo(toolName, description, details)
115+
_timeline.add(
116+
TimelineItem.ToolCallItem(
117+
toolName = toolName,
118+
details = details,
119+
success = null // Indicates in-progress
120+
)
121+
)
122+
}
123+
124+
fun completeToolCall(success: Boolean, summary: String?, output: String? = null, executionTimeMs: Long? = null) {
125+
currentToolCall = null
126+
// Update the last tool call item with results
127+
val lastIndex = _timeline.indexOfLast { it is TimelineItem.ToolCallItem && it.success == null }
128+
if (lastIndex >= 0) {
129+
val item = _timeline[lastIndex] as TimelineItem.ToolCallItem
130+
_timeline[lastIndex] = item.copy(
131+
success = success,
132+
summary = summary,
133+
output = output,
134+
executionTimeMs = executionTimeMs
135+
)
136+
}
137+
}
138+
139+
fun addError(message: String) {
140+
errorMessage = message
141+
_timeline.add(TimelineItem.ErrorItem(error = message))
142+
}
143+
144+
fun clearError() {
145+
errorMessage = null
146+
}
147+
148+
fun addTaskComplete(success: Boolean, message: String) {
149+
_timeline.add(TimelineItem.TaskCompleteItem(success = success, message = message))
150+
isProcessing = false
151+
}
152+
153+
fun addTerminalOutput(command: String, output: String, exitCode: Int, executionTimeMs: Long) {
154+
_timeline.add(TimelineItem.TerminalOutputItem(command, output, exitCode, executionTimeMs))
155+
}
156+
157+
fun clear() {
158+
_timeline.clear()
159+
currentStreamingOutput = ""
160+
currentToolCall = null
161+
errorMessage = null
162+
isProcessing = false
163+
}
164+
165+
fun forceStop() {
166+
currentStreamingOutput = ""
167+
currentToolCall = null
168+
isProcessing = false
169+
}
170+
}
171+
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package cc.unitmesh.devins.idea.agent
2+
3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.setValue
6+
import com.intellij.openapi.Disposable
7+
import com.intellij.openapi.project.Project
8+
import kotlinx.coroutines.*
9+
import kotlinx.coroutines.flow.MutableStateFlow
10+
import kotlinx.coroutines.flow.StateFlow
11+
import kotlinx.coroutines.flow.asStateFlow
12+
13+
/**
14+
* Represents the state of the message input.
15+
*/
16+
sealed class MessageInputState {
17+
abstract val inputText: String
18+
19+
data object Disabled : MessageInputState() {
20+
override val inputText: String = ""
21+
}
22+
23+
data class Enabled(override val inputText: String) : MessageInputState()
24+
data class Sending(override val inputText: String) : MessageInputState()
25+
data class SendFailed(override val inputText: String, val error: Throwable) : MessageInputState()
26+
}
27+
28+
/**
29+
* ViewModel for the IDEA CodingAgent interface.
30+
*
31+
* Manages the agent execution, timeline rendering, and UI state.
32+
* This is the IntelliJ IDEA version of CodingAgentViewModel from mpp-ui.
33+
*/
34+
class IdeaCodingAgentViewModel(
35+
private val project: Project,
36+
private val coroutineScope: CoroutineScope,
37+
private val maxIterations: Int = 100
38+
) : Disposable {
39+
40+
val renderer = IdeaAgentRenderer()
41+
42+
private val _inputState = MutableStateFlow<MessageInputState>(MessageInputState.Disabled)
43+
val inputState: StateFlow<MessageInputState> = _inputState.asStateFlow()
44+
45+
var isExecuting by mutableStateOf(false)
46+
private set
47+
48+
private var currentExecutionJob: Job? = null
49+
50+
// Configuration state - TODO: integrate with actual LLM service
51+
private var isConfigured = false
52+
53+
/**
54+
* Update the input text.
55+
*/
56+
fun onInputChanged(text: String) {
57+
_inputState.value = when {
58+
_inputState.value is MessageInputState.Sending -> MessageInputState.Sending(text)
59+
text.isEmpty() -> MessageInputState.Disabled
60+
else -> MessageInputState.Enabled(text)
61+
}
62+
}
63+
64+
/**
65+
* Execute a task with the given prompt.
66+
*/
67+
fun executeTask(task: String, onConfigRequired: (() -> Unit)? = null) {
68+
if (isExecuting) return
69+
70+
if (task.isBlank()) return
71+
72+
// Handle built-in commands
73+
if (task.trim().startsWith("/")) {
74+
handleBuiltinCommand(task.trim())
75+
return
76+
}
77+
78+
isExecuting = true
79+
renderer.addUserMessage(task)
80+
81+
currentExecutionJob = coroutineScope.launch {
82+
try {
83+
// TODO: Integrate with mpp-core CodingAgent
84+
// For now, simulate agent execution
85+
simulateAgentExecution(task)
86+
} catch (e: CancellationException) {
87+
renderer.forceStop()
88+
renderer.addError("Task cancelled by user")
89+
} catch (e: Exception) {
90+
renderer.addError(e.message ?: "Unknown error")
91+
} finally {
92+
isExecuting = false
93+
currentExecutionJob = null
94+
}
95+
}
96+
}
97+
98+
/**
99+
* Cancel the current task execution.
100+
*/
101+
fun cancelTask() {
102+
currentExecutionJob?.cancel()
103+
currentExecutionJob = null
104+
isExecuting = false
105+
renderer.forceStop()
106+
}
107+
108+
/**
109+
* Handle built-in commands like /clear, /help.
110+
*/
111+
private fun handleBuiltinCommand(command: String) {
112+
when {
113+
command == "/clear" -> {
114+
renderer.clear()
115+
}
116+
command == "/help" -> {
117+
renderer.addUserMessage(command)
118+
renderer.addAssistantMessage("""
119+
|**Available Commands:**
120+
|
121+
|- `/clear` - Clear the chat history
122+
|- `/help` - Show this help message
123+
|
124+
|**Tips:**
125+
|- Describe your coding task in natural language
126+
|- The agent will analyze, plan, and execute the task
127+
""".trimMargin())
128+
}
129+
else -> {
130+
renderer.addUserMessage(command)
131+
renderer.addError("Unknown command: $command")
132+
}
133+
}
134+
}
135+
136+
/**
137+
* Simulate agent execution for testing.
138+
* TODO: Replace with actual mpp-core CodingAgent integration.
139+
*/
140+
private suspend fun simulateAgentExecution(task: String) {
141+
renderer.startStreaming()
142+
143+
// Simulate thinking
144+
val thinking = "I'll analyze your request and create a plan..."
145+
for (char in thinking) {
146+
renderer.appendStreamingContent(char.toString())
147+
delay(20)
148+
}
149+
renderer.endStreaming()
150+
151+
// Simulate tool call
152+
renderer.startToolCall("ReadFile", "Reading project files", "src/main/kotlin/...")
153+
delay(500)
154+
renderer.completeToolCall(true, "Read 3 files", "File contents...", 450)
155+
156+
// Simulate response
157+
renderer.startStreaming()
158+
val response = "\n\nBased on my analysis, I've completed the following:\n\n1. Analyzed the codebase\n2. Identified the relevant files\n3. Made the necessary changes\n\nThe task has been completed successfully."
159+
for (char in response) {
160+
renderer.appendStreamingContent(char.toString())
161+
delay(15)
162+
}
163+
renderer.endStreaming()
164+
165+
renderer.addTaskComplete(true, "Task completed in 2 iterations")
166+
}
167+
168+
/**
169+
* Create a new conversation.
170+
*/
171+
fun onNewConversation() {
172+
cancelTask()
173+
renderer.clear()
174+
_inputState.value = MessageInputState.Disabled
175+
}
176+
177+
override fun dispose() {
178+
currentExecutionJob?.cancel()
179+
coroutineScope.cancel()
180+
}
181+
}
182+

0 commit comments

Comments
 (0)