Skip to content

Commit 748db30

Browse files
authored
Merge pull request #40 from phodal/feat/vscode-plan-functionality
feat(vscode): implement plan functionality for mpp-vscode
2 parents 1f09174 + bce4dfc commit 748db30

File tree

17 files changed

+1267
-8
lines changed

17 files changed

+1267
-8
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgent.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ class CodingAgent(
8585
private val policyEngine = DefaultPolicyEngine()
8686
private val toolOrchestrator = ToolOrchestrator(toolRegistry, policyEngine, renderer, mcpConfigService = mcpToolConfigService)
8787

88+
/**
89+
* Get the PlanStateService for observing plan state changes.
90+
* Returns null if no plan tool is registered.
91+
*/
92+
fun getPlanStateService(): cc.unitmesh.agent.plan.PlanStateService? {
93+
return toolOrchestrator.getPlanStateService()
94+
}
95+
8896
private val errorRecoveryAgent = ErrorRecoveryAgent(projectPath, llmService)
8997
private val analysisAgent = AnalysisAgent(llmService, contentThreshold = 15000)
9098
private val mcpToolsInitializer = McpToolsInitializer()

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/PlanStateService.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,25 +94,31 @@ class PlanStateService {
9494
val plan = _currentPlan.value ?: return
9595
val task = plan.getTask(taskId) ?: return
9696
task.updateStatus(status)
97+
// Trigger StateFlow update by reassigning the plan object
98+
_currentPlan.value = plan
9799
notifyTaskUpdated(task)
98100
}
99-
101+
100102
/**
101103
* Complete a step within a task.
102104
*/
103105
fun completeStep(taskId: String, stepId: String) {
104106
val plan = _currentPlan.value ?: return
105107
plan.completeStep(taskId, stepId)
108+
// Trigger StateFlow update by reassigning the plan object
109+
_currentPlan.value = plan
106110
notifyStepCompleted(taskId, stepId)
107111
}
108-
112+
109113
/**
110114
* Update a step's status.
111115
*/
112116
fun updateStepStatus(taskId: String, stepId: String, status: TaskStatus) {
113117
val plan = _currentPlan.value ?: return
114118
val task = plan.getTask(taskId) ?: return
115119
task.updateStepStatus(stepId, status)
120+
// Trigger StateFlow update by reassigning the plan object
121+
_currentPlan.value = plan
116122
notifyTaskUpdated(task)
117123
}
118124

mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/CodingAgentExports.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cc.unitmesh.agent.config.JsToolConfigFile
44
import cc.unitmesh.agent.render.DefaultCodingAgentRenderer
55
import cc.unitmesh.llm.JsMessage
66
import kotlinx.coroutines.GlobalScope
7+
import kotlinx.coroutines.launch
78
import kotlinx.coroutines.promise
89
import kotlin.js.Promise
910

@@ -182,6 +183,34 @@ class JsCodingAgent(
182183
JsMessage(msg.role.name.lowercase(), msg.content)
183184
}.toTypedArray()
184185
}
186+
187+
/**
188+
* Observe plan state changes and call the callback with plan summary data.
189+
* Returns a function to stop observing.
190+
*/
191+
@JsName("observePlanState")
192+
fun observePlanState(callback: (JsPlanSummaryData?) -> Unit): () -> Unit {
193+
val planStateService = agent.getPlanStateService()
194+
if (planStateService == null) {
195+
return { }
196+
}
197+
198+
var job: kotlinx.coroutines.Job? = null
199+
job = GlobalScope.launch {
200+
planStateService.currentPlan.collect { plan ->
201+
if (plan != null) {
202+
val summary = cc.unitmesh.agent.plan.PlanSummaryData.from(plan)
203+
callback(JsPlanSummaryData.from(summary))
204+
} else {
205+
callback(null)
206+
}
207+
}
208+
}
209+
210+
return {
211+
job.cancel()
212+
}
213+
}
185214
}
186215

187216

mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,57 @@
11
package cc.unitmesh.agent
22

33
import cc.unitmesh.agent.plan.PlanSummaryData
4+
import cc.unitmesh.agent.plan.StepSummary
5+
import cc.unitmesh.agent.plan.TaskSummary
46
import cc.unitmesh.agent.render.CodingAgentRenderer
57
import kotlin.js.JsExport
68

9+
/**
10+
* JS-friendly step summary data
11+
*/
12+
@JsExport
13+
data class JsStepSummary(
14+
val id: String,
15+
val description: String,
16+
val status: String
17+
) {
18+
companion object {
19+
fun from(step: StepSummary): JsStepSummary {
20+
return JsStepSummary(
21+
id = step.id,
22+
description = step.description,
23+
status = step.status.name
24+
)
25+
}
26+
}
27+
}
28+
29+
/**
30+
* JS-friendly task summary data
31+
*/
32+
@JsExport
33+
data class JsTaskSummary(
34+
val id: String,
35+
val title: String,
36+
val status: String,
37+
val completedSteps: Int,
38+
val totalSteps: Int,
39+
val steps: Array<JsStepSummary>
40+
) {
41+
companion object {
42+
fun from(task: TaskSummary): JsTaskSummary {
43+
return JsTaskSummary(
44+
id = task.id,
45+
title = task.title,
46+
status = task.status.name,
47+
completedSteps = task.completedSteps,
48+
totalSteps = task.totalSteps,
49+
steps = task.steps.map { JsStepSummary.from(it) }.toTypedArray()
50+
)
51+
}
52+
}
53+
}
54+
755
/**
856
* JS-friendly plan summary data
957
*/
@@ -16,7 +64,8 @@ data class JsPlanSummaryData(
1664
val failedSteps: Int,
1765
val progressPercent: Int,
1866
val status: String,
19-
val currentStepDescription: String?
67+
val currentStepDescription: String?,
68+
val tasks: Array<JsTaskSummary>
2069
) {
2170
companion object {
2271
fun from(summary: PlanSummaryData): JsPlanSummaryData {
@@ -28,7 +77,8 @@ data class JsPlanSummaryData(
2877
failedSteps = summary.failedSteps,
2978
progressPercent = summary.progressPercent,
3079
status = summary.status.name,
31-
currentStepDescription = summary.currentStepDescription
80+
currentStepDescription = summary.currentStepDescription,
81+
tasks = summary.tasks.map { JsTaskSummary.from(it) }.toTypedArray()
3282
)
3383
}
3484
}

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import cc.unitmesh.agent.render.ToolCallInfo
1414
import cc.unitmesh.agent.tool.ToolType
1515
import cc.unitmesh.agent.tool.toToolType
1616
import cc.unitmesh.llm.compression.TokenInfo
17+
import com.intellij.openapi.diagnostic.Logger
1718
import kotlinx.coroutines.flow.MutableStateFlow
1819
import kotlinx.coroutines.flow.StateFlow
1920
import kotlinx.coroutines.flow.asStateFlow
2021
import kotlinx.coroutines.flow.update
2122

23+
private val jewelRendererLogger = Logger.getInstance("JewelRenderer")
24+
2225
/**
2326
* Jewel-compatible Renderer for IntelliJ IDEA plugin.
2427
*
@@ -84,6 +87,15 @@ class JewelRenderer : BaseRenderer() {
8487
private val _currentPlan = MutableStateFlow<AgentPlan?>(null)
8588
val currentPlan: StateFlow<AgentPlan?> = _currentPlan.asStateFlow()
8689

90+
/**
91+
* Set the current plan directly.
92+
* Used to sync with PlanStateService from CodingAgent.
93+
*/
94+
fun setPlan(plan: AgentPlan?) {
95+
jewelRendererLogger.info("setPlan: plan=${plan != null}, tasks=${plan?.tasks?.size ?: 0}")
96+
_currentPlan.value = plan
97+
}
98+
8799
// BaseRenderer implementation
88100

89101
override fun renderIterationHeader(current: Int, max: Int) {
@@ -137,17 +149,22 @@ class JewelRenderer : BaseRenderer() {
137149
}
138150

139151
override fun renderToolCall(toolName: String, paramsStr: String) {
152+
jewelRendererLogger.info("renderToolCall: toolName=$toolName, paramsStr length=${paramsStr.length}")
153+
140154
val toolInfo = formatToolCallDisplay(toolName, paramsStr)
141155
val params = parseParamsString(paramsStr)
142156
val toolType = toolName.toToolType()
143157

158+
jewelRendererLogger.info("renderToolCall: parsed params keys=${params.keys}")
159+
144160
// Handle task-boundary tool - update task list
145161
if (toolName == "task-boundary") {
146162
updateTaskFromToolCall(params)
147163
}
148164

149165
// Handle plan management tool - update plan state
150166
if (toolName == "plan") {
167+
jewelRendererLogger.info("renderToolCall: detected plan tool, calling updatePlanFromToolCall")
151168
updatePlanFromToolCall(params)
152169
// Skip rendering plan tool to timeline - it's shown in PlanSummaryBar
153170
return
@@ -302,10 +319,18 @@ class JewelRenderer : BaseRenderer() {
302319
* Internal method to update plan state
303320
*/
304321
private fun updatePlanState(action: String, planMarkdown: String, taskIndex: Int?, stepIndex: Int?) {
322+
jewelRendererLogger.info("updatePlanState: action=$action, planMarkdown length=${planMarkdown.length}")
305323
when (action) {
306324
"CREATE", "UPDATE" -> {
307325
if (planMarkdown.isNotBlank()) {
308-
_currentPlan.value = MarkdownPlanParser.parseToPlan(planMarkdown)
326+
try {
327+
val plan = MarkdownPlanParser.parseToPlan(planMarkdown)
328+
jewelRendererLogger.info("Parsed plan: ${plan.tasks.size} tasks")
329+
_currentPlan.value = plan
330+
} catch (e: Exception) {
331+
jewelRendererLogger.warn("Failed to parse plan markdown", e)
332+
// Keep previous valid plan on parse failure
333+
}
309334
}
310335
}
311336
"COMPLETE_STEP" -> {

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaTerminalRenderer.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ fun IdeaTerminalRenderer(
7676

7777
// Command display
7878
CommandDisplay(command = command, isDangerous = isDangerous, dangerReason = dangerReason)
79-
8079
// Output (if available)
8180
if (showOutput && executionResult != null) {
8281
OutputDisplay(result = executionResult!!)

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,32 @@ class IdeaAgentViewModel(
262262
enableLLMStreaming = true
263263
)
264264
agentInitialized = true
265+
266+
// Start observing PlanStateService and sync to renderer
267+
startPlanStateObserver(codingAgent!!)
265268
}
266269
return codingAgent!!
267270
}
268271

272+
// Job for observing PlanStateService
273+
private var planStateObserverJob: Job? = null
274+
275+
/**
276+
* Start observing PlanStateService and sync plan state to renderer.
277+
*/
278+
private fun startPlanStateObserver(agent: CodingAgent) {
279+
// Cancel any existing observer
280+
planStateObserverJob?.cancel()
281+
282+
val planStateService = agent.getPlanStateService() ?: return
283+
284+
planStateObserverJob = coroutineScope.launch {
285+
planStateService.currentPlan.collect { plan ->
286+
renderer.setPlan(plan)
287+
}
288+
}
289+
}
290+
269291
/**
270292
* Check if LLM service is configured.
271293
*/
@@ -554,6 +576,7 @@ class IdeaAgentViewModel(
554576

555577
override fun dispose() {
556578
currentJob?.cancel()
579+
planStateObserverJob?.cancel()
557580
coroutineScope.cancel()
558581
}
559582
}

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaDevInInputArea.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
1414
import cc.unitmesh.agent.plan.AgentPlan
1515
import cc.unitmesh.devins.idea.editor.*
1616
import cc.unitmesh.devins.idea.toolwindow.plan.IdeaPlanSummaryBar
17+
import cc.unitmesh.devins.idea.toolwindow.changes.IdeaFileChangeSummary
1718
import cc.unitmesh.llm.NamedModelConfig
1819
import com.intellij.openapi.Disposable
1920
import com.intellij.openapi.application.ApplicationManager
@@ -111,6 +112,12 @@ fun IdeaDevInInputArea(
111112
modifier = Modifier.fillMaxWidth()
112113
)
113114

115+
// File change summary - shown when there are file changes
116+
IdeaFileChangeSummary(
117+
project = project,
118+
modifier = Modifier.fillMaxWidth()
119+
)
120+
114121
// Top toolbar with file selection (no individual border)
115122
IdeaTopToolbar(
116123
project = project,

0 commit comments

Comments
 (0)